feat: pluggable operations for parse - #173
Conversation
🦋 Changeset detectedLatest commit: 2f71aaf The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
83f17f4 to
5e70203
Compare
|
Pushed 1998fda adding Every other operation implemented cleanly against handles. arrayIndices: (handle) =>
defaultStringifyOperations.arrayIndices(
Object.fromEntries(handle.keys().map((key) => [key, 0])) // throwaway host object
),Materializing a fake object just to hand it back to the default is obviously not the intended usage. With the helper exported it becomes: arrayIndices: (handle) => filterArrayIndices(handle.keys())The change refactors Happy to drop this if you'd rather keep the surface minimal — but some export is probably warranted, since the alternative is every foreign-runtime implementation reimplementing a heuristic-critical helper. |
1998fda to
c20c8a4
Compare
|
Rebased onto main now that #172 has landed — and reworked to match the decisions made in the squash:
790 tests passing (766 from main including the tripwire suite, plus 19 parse-operations tests and 5 helper tests), dts build and publint clean. |
Route every value construction parse/unflatten performs while reviving
(built-in instances, containers, property assignment) through a
ParseOperations interface, overridable via a new options argument:
parse(serialized, revivers, { operations: { ... } })
Omitted members fall back to defaultParseOperations (exported), which
preserve existing behavior exactly.
This is the inverse of the stringify operations: where stringify needs
pluggable introspection, parse needs pluggable construction.
Motivations:
- Cross-realm revival: build values from the intrinsics of another realm
(e.g. a node:vm context) so they satisfy instanceof checks there
- Foreign-runtime revival: build values inside another JS runtime (a
WASM-hosted engine, a remote process) through opaque handles
Containers are created empty then populated (createMap/mapSet,
createObject/setProperty, ...), which is what keeps cyclic values
revivable.
Also renames defaultOperations to defaultStringifyOperations for
symmetry with defaultParseOperations, and extracts the shared
override-merging helper.
The arrayIndices operation encodes the sparse-array heuristic, so a custom implementation either reimplements the filtering or contorts to reuse the default. Foreign-runtime implementations typically already have the keys, so expose the filtering half directly.
c20c8a4 to
163b482
Compare
|
Reworked the
Every extraction op now has a legible inverse:
The families are documented in the |
elliott-with-the-longest-name-on-github
left a comment
There was a problem hiding this comment.
Just some notes for myself, going to implement a few little changes and then we should be good to go
07d6a38
into
sveltejs:main
Stacked on #172 — the diff shown includes that PR's commits until it lands. Review #172 first; the parse-specific changes are in the last commit (83f17f4).
Summary
The inverse of #172: where
stringifyneeds pluggable introspection,parseneeds pluggable construction.Every value
parse/unflattencreates while reviving — built-in instances (Date,RegExp,URL,Temporal.*, typed arrays, boxed primitives, BigInt), containers (Map,Set, arrays, objects, null-prototype objects), and the mutations that populate them — now routes through aParseOperationsinterface. Omitted members fall back todefaultParseOperations, which is the current behavior extracted verbatim, so the default path is unchanged.Motivation
Cross-realm revival. Revived values are currently built from the intrinsics of whichever realm devalue runs in, so a value revived on the host and handed to a
node:vmsandbox fails everyinstanceofcheck inside it. Overriding the constructors fixes that (covered by a test that asserts hostinstanceoffails while the sandbox's own checks pass).Foreign-runtime revival.
parsenever inspects the values it creates — it only feeds them back into other operations — so implementations can build values inside another runtime (WASM-hosted engine, remote process) and return opaque handles. Combined with feat: pluggable operations for stringify #172 this closes the loop: a value can be serialized out of a foreign runtime and revived back into it without either side crossing the boundary as raw data.Design notes
setAdd/mapSet/setProperty/setIndex. This mirrors the existing algorithm and is what keeps cyclic values revivable (the empty container is cached before its contents are built) — documented on the interface so implementors don't "optimize" it away.createArray(length)takes a payload-bounded length;createSparseArray(length)takes an untrusted length and is contractually required not to allocate proportionally to it. This keeps the existing sparse-array DoS mitigation (V8 dictionary-elements trick) intact and, more importantly, makes the requirement explicit for anyone writing their own implementation rather than leaving it as an undocumented invariant of the call site.lengthupfront instead of truncating at the end. I verified with%HasDictionaryElementsacross lengths from 10 to 5e7 that this is equivalent — both orderings stay in dictionary mode, while naivenew Array(len)drops out below ~5e7 (the actual DoS vector).parseneeds to read a revived value — the interface is purely constructive.defaultOperations→defaultStringifyOperationsfor symmetry withdefaultParseOperations. feat: pluggable operations for stringify #172 is unreleased so this is free; happy to revert if you'd rather keep the shorter name.unevalremains out of scope.Tests
20 new tests in
test/parse-operations.test.js(779 total passing;test/operations.test.jsrenamed totest/stringify-operations.test.js):undefinedfallback, frozen defaults,unflattenparity, reviver compositionnode:vm):Date/RegExp/Set/Map/array/null-proto/typed-array construction from a sandbox's intrinsics, asserted from inside the sandbox; cycles link correctly across the boundaryParseOperationsimplementation over an opaque wrapper, round-trip parity against plainparsefor every supported type, shared references, cycles, revivers, plus one test that round-trips through both operation setsPerformance
Within noise.
publintclean.