@lunora/wire 0.0.0-stage → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ declare const stableStringify: (value: unknown) => string;
2
+ /**
3
+ * The wire codec's definition of a plain object: an `Object.prototype` or
4
+ * null-prototype object. Arrays fail it (their prototype is `Array.prototype`),
5
+ * as do `Date`, `Map`, `Set`, `ArrayBuffer` and every typed-array view.
6
+ *
7
+ * Exported because it is the boundary between "a document" and "a value that
8
+ * merely decoded to an object". Callers that parse a stored/received blob need
9
+ * exactly this test, and testing for `!Array.isArray` instead is a trap: it is
10
+ * sufficient for a bare `JSON.parse` (whose only non-plain root is an array) but
11
+ * NOT after `decodeWire`, which turns a root-level tagged array into a
12
+ * `Uint8Array`/`Date`/`Map` that passes an array-only check.
13
+ *
14
+ * Bundler-inlined; `@lunora/wire` re-exports it so server packages get the same function.
15
+ */
16
+ declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
17
+ /**
18
+ * Self-delimiting tag. A JSON array is significant to the codec **only** when its
19
+ * first element is exactly this string, so the collision surface is a user array
20
+ * literally starting with the sentinel — handled by the `"arr"` escape below. The
21
+ * `$...$` fence makes an accidental collision with real app data essentially
22
+ * impossible while staying valid inside a JSON string.
23
+ */
24
+ declare const TAG = "$lunora.wire$";
25
+ /**
26
+ * Would {@link encodeWire} change this value, or is it already the JSON it will
27
+ * become?
28
+ *
29
+ * `encodeWire` is identity for pure JSON — it says so, and the row store depends
30
+ * on it: a document with no wire-typed leaf must encode byte-identically to
31
+ * plain `JSON.stringify`, or the OCC compare-and-swap stops matching stored
32
+ * rows. But identity is not free. It walks the tree and rebuilds every object and
33
+ * array along the way, and the caller almost always throws that copy straight at
34
+ * `JSON.stringify`. Profiling the DO write path put `encodeDocJson` at 8.2% of
35
+ * it, most of that the discarded copy.
36
+ *
37
+ * This answers the same question with a read-only walk, so the common case can
38
+ * stringify the original and allocate nothing.
39
+ *
40
+ * **It is deliberately conservative.** Every branch that is not *provably*
41
+ * identity answers `true` — a false positive costs one wasted encode, while a
42
+ * false negative writes different bytes than the store expects, which is silent
43
+ * corruption. It lives next to `encodeWire` for the same reason: the two must
44
+ * agree, so they have to be read together.
45
+ * `packages/shard-engine/__tests__/needs-wire-encoding.test.ts` fuzzes the
46
+ * property that matters — `needsWireEncoding(v) === false` implies
47
+ * `JSON.stringify(encodeWire(v)) === JSON.stringify(v)`.
48
+ * @returns `true` when the value must go through {@link encodeWire}
49
+ */
50
+ declare const needsWireEncoding: (value: unknown, depth?: number) => boolean;
51
+ /**
52
+ * Encode `value` into a JSON-safe tree, tagging the leaves JSON cannot represent.
53
+ * Pure and recursive; no I/O. Trees only (no cycle detection — a cyclic input
54
+ * throws via the recursion, same as `JSON.stringify`).
55
+ */
56
+ declare const encodeWire: (value: unknown, depth?: number) => unknown;
57
+ /** Inverse of {@link encodeWire}: revive tagged leaves back to their JS values. */
58
+ declare const decodeWire: (value: unknown, depth?: number) => unknown;
59
+ /**
60
+ * Parse a stored JSON blob back into a document: `JSON.parse`, then
61
+ * {@link decodeWire}, then {@link isPlainObject}. Returns `undefined` for
62
+ * anything that is not a document — unparseable text, a malformed tag
63
+ * (`decodeWire` throws on an over-long bigint or past the nesting cap), or a
64
+ * root that decodes to a non-plain object.
65
+ *
66
+ * Never throws, because its callers are read paths that must degrade rather
67
+ * than fail: a display surface shows the row unexpanded, an editor falls back
68
+ * to the row's own columns. A caller that needs to distinguish "bad JSON" from
69
+ * "not a document" — to report which — should use {@link decodeWire} and
70
+ * {@link isPlainObject} directly and keep its own `try`.
71
+ */
72
+ declare const decodeDocument: (text: string) => Record<string, unknown> | undefined;
73
+ /**
74
+ * Encode an outbound call's `args` with {@link encodeWire}, re-throwing the bare codec
75
+ * error with the calling surface and the target function attached.
76
+ *
77
+ * Every producer of a Lunora call envelope encodes here — `@lunora/client`'s
78
+ * service binding, `@lunora/dispatch`'s runner, `ctx.scheduler` (from a shard and
79
+ * from an HTTP action), and the workpool. `encodeWire` REJECTS any non-plain
80
+ * object (a `RegExp`, a `Headers`, a class instance, one with a working
81
+ * `toJSON()`), where `JSON.stringify` used to swallow it into `{}`. Loud beats
82
+ * silently wrong, but the codec's own message names only the offending type: an
83
+ * unattributed `TypeError` out of `pool.enqueue` or a scheduled job's log line
84
+ * cannot be traced back to the call that carried the bad argument. `label` is the
85
+ * surface (`@lunora/queue`, `ctx.scheduler.runAt`), `path` the target function.
86
+ */
87
+ declare const encodeArgsOrThrow: (label: string, path: string, args: unknown) => unknown;
88
+ /**
89
+ * Stable cache/dedup key for a (possibly wire-typed) `value`: the sorted-key
90
+ * stable encoding of its wire form. Byte-identical to `stableStringify(value)`
91
+ * for pure-JSON values; deterministic tagged tokens for `bigint`/`Date`/`Map`/
92
+ * `Set`/`URL`/bytes; throws a `TypeError` on values the wire refuses.
93
+ */
94
+ declare const stableWireKey: (value: unknown) => string;
95
+ export { TAG as WIRE_TAG, decodeDocument, decodeWire, encodeArgsOrThrow, encodeWire, isPlainObject, needsWireEncoding, stableStringify, stableWireKey };
@@ -0,0 +1,95 @@
1
+ declare const stableStringify: (value: unknown) => string;
2
+ /**
3
+ * The wire codec's definition of a plain object: an `Object.prototype` or
4
+ * null-prototype object. Arrays fail it (their prototype is `Array.prototype`),
5
+ * as do `Date`, `Map`, `Set`, `ArrayBuffer` and every typed-array view.
6
+ *
7
+ * Exported because it is the boundary between "a document" and "a value that
8
+ * merely decoded to an object". Callers that parse a stored/received blob need
9
+ * exactly this test, and testing for `!Array.isArray` instead is a trap: it is
10
+ * sufficient for a bare `JSON.parse` (whose only non-plain root is an array) but
11
+ * NOT after `decodeWire`, which turns a root-level tagged array into a
12
+ * `Uint8Array`/`Date`/`Map` that passes an array-only check.
13
+ *
14
+ * Bundler-inlined; `@lunora/wire` re-exports it so server packages get the same function.
15
+ */
16
+ declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
17
+ /**
18
+ * Self-delimiting tag. A JSON array is significant to the codec **only** when its
19
+ * first element is exactly this string, so the collision surface is a user array
20
+ * literally starting with the sentinel — handled by the `"arr"` escape below. The
21
+ * `$...$` fence makes an accidental collision with real app data essentially
22
+ * impossible while staying valid inside a JSON string.
23
+ */
24
+ declare const TAG = "$lunora.wire$";
25
+ /**
26
+ * Would {@link encodeWire} change this value, or is it already the JSON it will
27
+ * become?
28
+ *
29
+ * `encodeWire` is identity for pure JSON — it says so, and the row store depends
30
+ * on it: a document with no wire-typed leaf must encode byte-identically to
31
+ * plain `JSON.stringify`, or the OCC compare-and-swap stops matching stored
32
+ * rows. But identity is not free. It walks the tree and rebuilds every object and
33
+ * array along the way, and the caller almost always throws that copy straight at
34
+ * `JSON.stringify`. Profiling the DO write path put `encodeDocJson` at 8.2% of
35
+ * it, most of that the discarded copy.
36
+ *
37
+ * This answers the same question with a read-only walk, so the common case can
38
+ * stringify the original and allocate nothing.
39
+ *
40
+ * **It is deliberately conservative.** Every branch that is not *provably*
41
+ * identity answers `true` — a false positive costs one wasted encode, while a
42
+ * false negative writes different bytes than the store expects, which is silent
43
+ * corruption. It lives next to `encodeWire` for the same reason: the two must
44
+ * agree, so they have to be read together.
45
+ * `packages/shard-engine/__tests__/needs-wire-encoding.test.ts` fuzzes the
46
+ * property that matters — `needsWireEncoding(v) === false` implies
47
+ * `JSON.stringify(encodeWire(v)) === JSON.stringify(v)`.
48
+ * @returns `true` when the value must go through {@link encodeWire}
49
+ */
50
+ declare const needsWireEncoding: (value: unknown, depth?: number) => boolean;
51
+ /**
52
+ * Encode `value` into a JSON-safe tree, tagging the leaves JSON cannot represent.
53
+ * Pure and recursive; no I/O. Trees only (no cycle detection — a cyclic input
54
+ * throws via the recursion, same as `JSON.stringify`).
55
+ */
56
+ declare const encodeWire: (value: unknown, depth?: number) => unknown;
57
+ /** Inverse of {@link encodeWire}: revive tagged leaves back to their JS values. */
58
+ declare const decodeWire: (value: unknown, depth?: number) => unknown;
59
+ /**
60
+ * Parse a stored JSON blob back into a document: `JSON.parse`, then
61
+ * {@link decodeWire}, then {@link isPlainObject}. Returns `undefined` for
62
+ * anything that is not a document — unparseable text, a malformed tag
63
+ * (`decodeWire` throws on an over-long bigint or past the nesting cap), or a
64
+ * root that decodes to a non-plain object.
65
+ *
66
+ * Never throws, because its callers are read paths that must degrade rather
67
+ * than fail: a display surface shows the row unexpanded, an editor falls back
68
+ * to the row's own columns. A caller that needs to distinguish "bad JSON" from
69
+ * "not a document" — to report which — should use {@link decodeWire} and
70
+ * {@link isPlainObject} directly and keep its own `try`.
71
+ */
72
+ declare const decodeDocument: (text: string) => Record<string, unknown> | undefined;
73
+ /**
74
+ * Encode an outbound call's `args` with {@link encodeWire}, re-throwing the bare codec
75
+ * error with the calling surface and the target function attached.
76
+ *
77
+ * Every producer of a Lunora call envelope encodes here — `@lunora/client`'s
78
+ * service binding, `@lunora/dispatch`'s runner, `ctx.scheduler` (from a shard and
79
+ * from an HTTP action), and the workpool. `encodeWire` REJECTS any non-plain
80
+ * object (a `RegExp`, a `Headers`, a class instance, one with a working
81
+ * `toJSON()`), where `JSON.stringify` used to swallow it into `{}`. Loud beats
82
+ * silently wrong, but the codec's own message names only the offending type: an
83
+ * unattributed `TypeError` out of `pool.enqueue` or a scheduled job's log line
84
+ * cannot be traced back to the call that carried the bad argument. `label` is the
85
+ * surface (`@lunora/queue`, `ctx.scheduler.runAt`), `path` the target function.
86
+ */
87
+ declare const encodeArgsOrThrow: (label: string, path: string, args: unknown) => unknown;
88
+ /**
89
+ * Stable cache/dedup key for a (possibly wire-typed) `value`: the sorted-key
90
+ * stable encoding of its wire form. Byte-identical to `stableStringify(value)`
91
+ * for pure-JSON values; deterministic tagged tokens for `bigint`/`Date`/`Map`/
92
+ * `Set`/`URL`/bytes; throws a `TypeError` on values the wire refuses.
93
+ */
94
+ declare const stableWireKey: (value: unknown) => string;
95
+ export { TAG as WIRE_TAG, decodeDocument, decodeWire, encodeArgsOrThrow, encodeWire, isPlainObject, needsWireEncoding, stableStringify, stableWireKey };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{default as o}from"./packem_shared/stableStringify-KwgJVih9.mjs";import{WIRE_TAG as d,decodeDocument as i,decodeWire as n,encodeArgsOrThrow as a,encodeWire as c,needsWireEncoding as f}from"./packem_shared/WIRE_TAG-DIesFsHE.mjs";import{default as l}from"./packem_shared/stableWireKey-B2We0yhx.mjs";import{isPlainObject as W}from"./packem_shared/isPlainObject-UM7p1XXO.mjs";export{d as WIRE_TAG,i as decodeDocument,n as decodeWire,a as encodeArgsOrThrow,c as encodeWire,W as isPlainObject,f as needsWireEncoding,o as stableStringify,l as stableWireKey};
@@ -0,0 +1 @@
1
+ import{isPlainObject as u}from"./isPlainObject-UM7p1XXO.mjs";const f=r=>{let e="";for(let n=0;n<r.length;n+=32768)e+=String.fromCharCode(...r.subarray(n,n+32768));return btoa(e)},l=r=>{const e=atob(r),t=new Uint8Array(e.length);for(let n=0;n<e.length;n+=1)t[n]=e.codePointAt(n)??0;return t},i="$lunora.wire$",s=64,d=1024,w="__proto__",O=/^-?\d+$/,b={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},g={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},y=(r,e,t)=>{e===w?Object.defineProperty(r,e,{configurable:!0,enumerable:!0,value:t,writable:!0}):r[e]=t},p=(r,e=0)=>{if(e>s)return!0;if(r===null)return!1;const t=typeof r;if(t==="string"||t==="boolean")return!1;if(t==="number")return!Number.isFinite(r);if(t!=="object")return!0;if(Array.isArray(r))return r.length>0&&r[0]===i?!0:r.some(n=>n===void 0||p(n,e+1));if(!u(r)||Object.hasOwn(r,w))return!0;for(const n of Object.keys(r)){const o=r[n];if(o!==void 0&&p(o,e+1))return!0}return!1},T=r=>{const e=r.toString();if(e.length>d)throw new RangeError(`wire-codec: over-long bigint (max ${String(d)} digits)`);return[i,"bigint",e]},j=r=>Number.isNaN(r)?[i,"nan"]:r===1/0?[i,"inf"]:r===-1/0?[i,"-inf"]:r,R=(r,e)=>{const t=e(Object.fromEntries(Object.keys(r).map(c=>[c,r[c]]))),n=r,o=[i,"error",String(n.name),String(n.message),t];return r.cause!==void 0&&o.push(e(r.cause)),o},S=r=>{const e=r.constructor.name,t=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);return e==="Uint8Array"?[i,"bytes",f(t)]:[i,"bytes",f(t),e]},U=(r,e)=>r instanceof Date?[i,"date",e(r.getTime())]:r instanceof Error?R(r,e):r instanceof URL?[i,"url",r.href]:r instanceof Map?[i,"map",[...r.entries()].map(([t,n])=>[e(t),e(n)])]:r instanceof Set?[i,"set",[...r].map(t=>e(t))]:r instanceof ArrayBuffer?[i,"bytes",f(new Uint8Array(r)),"ArrayBuffer"]:ArrayBuffer.isView(r)?S(r):void 0,I=(r,e)=>{const t=r.map(n=>e(n));return t.length>0&&t[0]===i?[i,"arr",t]:t},B=(r,e)=>{const t={};for(const n of Object.keys(r)){const o=r[n];o!==void 0&&y(t,n,e(o))}return t},m=(r,e=0)=>{if(e>s)throw new RangeError(`wire-codec: value nesting exceeds the ${String(s)}-level limit`);if(r===void 0)return[i,"undefined"];if(r===null)return r;if(typeof r=="bigint")return T(r);if(typeof r=="number")return j(r);if(typeof r!="object")return r;const t=o=>m(o,e+1),n=U(r,t);if(n!==void 0)return n;if(Array.isArray(r))return I(r,t);if(!u(r)){const o=r.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${o} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}return B(r,t)},N=r=>{const e=r[2];if(typeof e!="string"||e.length>d||!O.test(e))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${String(d)} digits)`);return BigInt(e)},_=(r,e)=>{const t=e(r[2]);if(typeof t!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(t)},$=(r,e)=>{const t=r[2];return new Map(t.map(n=>{if(!Array.isArray(n)||n.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[e(n[0]),e(n[1])]}))},k=r=>{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(e)},P=(r,e)=>{const t=r[2],n=r[3];if(typeof t!="string"||typeof n!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const o=(Object.hasOwn(g,t)?g[t]:void 0)??Error,c=new o(n);c.name!==t&&Object.defineProperty(c,"name",{configurable:!0,value:t,writable:!0});const a=e(r[4]);if(a===null||typeof a!="object"||Array.isArray(a))throw new TypeError("wire-codec: malformed error — props must be an object");for(const[h,E]of Object.entries(a))y(c,h,E);return r.length>5&&Object.defineProperty(c,"cause",{configurable:!0,value:e(r[5]),writable:!0}),c},D=r=>{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const t=l(e);if(f(t)!==e)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const n=r[3]??"Uint8Array";if(n==="ArrayBuffer")return t.buffer.byteLength===t.byteLength?t.buffer:new Uint8Array(t).buffer;const o=Object.hasOwn(b,n)?b[n]:void 0;return o?new o(new Uint8Array(t).buffer):t},C=(r,e)=>{switch(r[1]){case"-inf":return-1/0;case"arr":return r[2].map(n=>e(n));case"bigint":return N(r);case"bytes":return D(r);case"date":return _(r,e);case"error":return P(r,e);case"inf":return 1/0;case"map":return $(r,e);case"nan":return Number.NaN;case"set":return new Set(r[2].map(n=>e(n)));case"undefined":return;case"url":return k(r);default:return r.map(n=>e(n))}},L=(r,e)=>{const t={};for(const n of Object.keys(r))y(t,n,e(r[n]));return t},A=(r,e=0)=>{if(e>s)throw new RangeError(`wire-codec: value nesting exceeds the ${String(s)}-level limit`);if(r===null||typeof r!="object")return r;const t=n=>A(n,e+1);if(Array.isArray(r)){const n=r;return n[0]===i?C(n,t):n.map(o=>t(o))}return L(r,t)},F=r=>{try{const e=A(JSON.parse(r));return u(e)?e:void 0}catch{return}},G=(r,e,t)=>{try{return m(t)}catch(n){throw new TypeError(`${r}: cannot encode args for '${e}' — ${n instanceof Error?n.message:String(n)}`,n instanceof Error?{cause:n}:void 0)}};export{i as WIRE_TAG,F as decodeDocument,A as decodeWire,G as encodeArgsOrThrow,m as encodeWire,u as isPlainObject,p as needsWireEncoding};
@@ -0,0 +1 @@
1
+ const e=t=>{if(t===null||typeof t!="object")return!1;const o=Object.getPrototypeOf(t);return o===null||o===Object.prototype};export{e as isPlainObject};
@@ -0,0 +1 @@
1
+ const b=/["\\\u0000-\u001F\uD800-\uDFFF]/,y=t=>b.test(t)?JSON.stringify(t):`"${t}"`,p=t=>Number.isNaN(t)?"nan":t===1/0?"inf":t===-1/0?"-inf":Object.is(t,-0)?"-0":void 0,u=(t,r)=>{const o=Object.getPrototypeOf(t);if(o!==null&&o!==Object.prototype){const i=t.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${i} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const e=t,a=Object.keys(e).sort();let n="{",s=!0;for(const i of a){const f=e[i];f!==void 0&&(s?s=!1:n+=",",n+=y(i),n+=":",n+=r(f))}return`${n}}`},c=t=>{if(t===void 0)return"null";if(typeof t=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof t=="number"){const r=p(t);if(r!==void 0)return r}return typeof t=="string"?y(t):t===null||typeof t!="object"?JSON.stringify(t):Array.isArray(t)?`[${Array.from({length:t.length},(o,e)=>c(t[e])).join(",")}]`:u(t,c)};export{c as default};
@@ -0,0 +1 @@
1
+ import r from"./stableStringify-KwgJVih9.mjs";import{encodeWire as t}from"./WIRE_TAG-DIesFsHE.mjs";import"./isPlainObject-UM7p1XXO.mjs";const a=e=>r(t(e));export{a as default};
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@lunora/wire",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Wire codec and stable keys shared by every Lunora tier",
5
+ "keywords": [
6
+ "cloudflare",
7
+ "codec",
8
+ "durable-objects",
9
+ "lunora",
10
+ "serialization",
11
+ "wire",
12
+ "workers"
13
+ ],
14
+ "homepage": "https://lunora.sh",
15
+ "bugs": "https://github.com/anolilab/lunora/issues",
16
+ "license": "FSL-1.1-Apache-2.0",
17
+ "author": {
18
+ "name": "Daniel Bannert",
19
+ "email": "d.bannert@anolilab.de"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/anolilab/lunora.git",
24
+ "directory": "packages/wire"
25
+ },
26
+ "files": [
27
+ "./dist",
28
+ "README.md",
29
+ "LICENSE.md",
30
+ "__assets__"
31
+ ],
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "main": "./dist/index.mjs",
35
+ "module": "./dist/index.mjs",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.mjs"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "engines": {
48
+ "node": "^22.15.0 || >=24.11.0"
49
+ }
6
50
  }