Skip to content

feat: introduce ArgCodec system with symbol tags for built-in codecs#6

Merged
Lojhan merged 9 commits intomainfrom
feat/codec-system
Mar 18, 2026
Merged

feat: introduce ArgCodec system with symbol tags for built-in codecs#6
Lojhan merged 9 commits intomainfrom
feat/codec-system

Conversation

@Lojhan
Copy link
Collaborator

@Lojhan Lojhan commented Mar 18, 2026

Summary

Replaces the ad-hoc type-switch logic in shared-resources.ts with a composable codec registry, and exposes a public API for users to register custom serialization strategies for class instances and other non-JSON-representable values.

Motivation

The previous implementation handled Date, Map, Set, undefined, BigInt, arrays, and plain objects through a series of if/else branches hardcoded in encodeArg, decodeArg, and writeBackObject/writeBackArray. This made it impossible for users to teach the system about their own types (e.g. class instances), and any new built-in type would require modifying the core file directly.

Three concrete bugs also existed:

  • Class instances lost their prototype after an IPC round-trip (encoded by JSON.stringify as a plain object, decoded as a plain object — no reconstruction).
  • Function-valued properties were deleted from objects during writeBack because the old code iterated all keys without distinguishing functions.
  • Custom class instances passed as RPC arguments threw TypeError instead of degrading gracefully.

Changes

New public API

  • ArgCodec<T> type — defines tag, is, encode, decode, and an optional writeBack hook with a reconcile callback for nested in-place updates.
  • SharedResourcesConfig type — { codecs?: ArgCodec<any>[] } passed to sharedResources().
  • configureCodecs(codecs) — registers codecs globally; new codecs are prepended (checked first) and merge by tag, so a later registration with the same tag replaces the earlier one.
  • resource.configure({ codecs }) — per-resource codec registration that runs in both parent and child via module evaluation.

codecs — seven new files

Each built-in type is now a self-contained codec module. Tags use Symbol.for('sr:*') so they can never be accidentally overwritten by a user codec that picks the same string tag.

File Handles Tag
undefined.ts undefined Symbol.for('sr:u')
bigint.ts bigint Symbol.for('sr:bi')
date.ts Date Symbol.for('sr:d')
map.ts Map Symbol.for('sr:m')
set.ts Set Symbol.for('sr:s')
array.ts Array Symbol.for('sr:a')
object.ts Plain objects + isPlainObject Symbol.for('sr:obj')

shared-resources.ts

  • encodeArg, decodeArg, decodeEncoded, tryReconcileInPlace all simplified to codec-registry loops — no more hardcoded type checks.
  • tagToWire(tag) helper converts symbol tags to their Symbol.keyFor() string for the wire t field.
  • decodeEncoded resolves symbol-tagged codecs by matching Symbol.keyFor(c.tag) === enc.t.
  • Class instances without a registered codec are transparently encoded via objectCodec (own enumerable data properties, functions skipped) and reconciled back onto the original instance on the caller side — prototype is preserved, data is updated.
  • Removed: encodeObjectValues, encodeObject, decodeObjectValues, writeBackArray, writeBackObject, writeBackDate, writeBackMap, writeBackSet, and the collision-escape (esc) mechanism (no longer needed since the outer { __sr_enc, t, v } sentinel wraps encoded data).

Tests

  • New unsupported-args.test.ts — covers codec-based class instance round-trip (prototype reconstructed via pointCodec) and function-property preservation.
  • Expanded encode-decode.test.ts — class instance encoding, nested class instances, function property omission, custom codec sentinel wrapping, codec inside object/array, tag deduplication, unknown-tag error message.
  • Expanded write-back.test.ts — class instance in-place update, function property retention, custom writeBack hook invocation, reconcile callback for nested values.

Wire format compatibility

The wire format for arrays and plain objects has changed:

Value type Before After
Array [...] (raw) { "__sr_enc": "c", "t": "sr:a", "v": [...] }
Plain object {...} (raw) { "__sr_enc": "c", "t": "sr:obj", "v": {...} }

Lojhan added 9 commits March 17, 2026 16:35
- Add ArgCodec<T> type with is/encode/decode/writeBack/reconcile hooks
- Add SharedResourcesConfig type with optional codecs array
- Add configureCodecs() export and resource.configure({ codecs }) API
- Extract all built-in type handling into composable codec files:
  src/codecs/undefined.ts, bigint.ts, date.ts, map.ts, set.ts, array.ts, object.ts
- Use Symbol.for() tags for all built-in codecs to prevent shadowing
- Simplify shared-resources.ts: encodeArg, decodeArg, tryReconcileInPlace
  now fully delegate to the codec registry
- Support class instances without a codec by encoding own enumerable
  data properties and reconciling back onto the original instance
- Preserve function-valued properties during encoding and writeBack
- Remove collision-escape (esc) mechanism — outer sentinel wrap makes it unnecessary
- Add tagToWire() helper for symbol-to-string wire conversion
@Lojhan Lojhan merged commit 80b137b into main Mar 18, 2026
12 checks passed
@Lojhan Lojhan deleted the feat/codec-system branch March 18, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant