@net-mesh/browser 0.37.0

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +666 -0
  5. package/dist/async-queue.d.ts +26 -0
  6. package/dist/async-queue.js +62 -0
  7. package/dist/credential.d.ts +70 -0
  8. package/dist/credential.js +95 -0
  9. package/dist/errors.d.ts +400 -0
  10. package/dist/errors.js +726 -0
  11. package/dist/events.d.ts +232 -0
  12. package/dist/events.js +298 -0
  13. package/dist/identity.d.ts +40 -0
  14. package/dist/identity.js +78 -0
  15. package/dist/index.bundle.js +2 -0
  16. package/dist/index.d.ts +38 -0
  17. package/dist/index.js +46 -0
  18. package/dist/leader/events.d.ts +111 -0
  19. package/dist/leader/events.js +116 -0
  20. package/dist/leader/index.d.ts +14 -0
  21. package/dist/leader/index.js +11 -0
  22. package/dist/leader/session.d.ts +539 -0
  23. package/dist/leader/session.js +770 -0
  24. package/dist/leader/wasm.d.ts +123 -0
  25. package/dist/leader/wasm.js +39 -0
  26. package/dist/lobby.d.ts +183 -0
  27. package/dist/lobby.js +504 -0
  28. package/dist/local.d.ts +80 -0
  29. package/dist/local.js +160 -0
  30. package/dist/net_leaf.d.ts +1494 -0
  31. package/dist/net_leaf.js +3753 -0
  32. package/dist/net_leaf_bg.wasm +0 -0
  33. package/dist/net_leaf_bg.wasm.d.ts +137 -0
  34. package/dist/netcode/client.d.ts +102 -0
  35. package/dist/netcode/client.js +296 -0
  36. package/dist/netcode/clock.d.ts +45 -0
  37. package/dist/netcode/clock.js +54 -0
  38. package/dist/netcode/host.d.ts +118 -0
  39. package/dist/netcode/host.js +276 -0
  40. package/dist/netcode/index.d.ts +19 -0
  41. package/dist/netcode/index.js +14 -0
  42. package/dist/netcode/interpolate.d.ts +50 -0
  43. package/dist/netcode/interpolate.js +100 -0
  44. package/dist/netcode/wire.d.ts +125 -0
  45. package/dist/netcode/wire.js +117 -0
  46. package/dist/node.d.ts +908 -0
  47. package/dist/node.js +1085 -0
  48. package/dist/org.d.ts +412 -0
  49. package/dist/org.js +567 -0
  50. package/dist/peer-driver.d.ts +117 -0
  51. package/dist/peer-driver.js +224 -0
  52. package/dist/store/assembly.d.ts +176 -0
  53. package/dist/store/assembly.js +325 -0
  54. package/dist/store/chunker.d.ts +83 -0
  55. package/dist/store/chunker.js +190 -0
  56. package/dist/store/core.d.ts +136 -0
  57. package/dist/store/core.js +477 -0
  58. package/dist/store/definition.d.ts +23 -0
  59. package/dist/store/definition.js +83 -0
  60. package/dist/store/errors.d.ts +77 -0
  61. package/dist/store/errors.js +94 -0
  62. package/dist/store/host.d.ts +247 -0
  63. package/dist/store/host.js +675 -0
  64. package/dist/store/index.d.ts +50 -0
  65. package/dist/store/index.js +40 -0
  66. package/dist/store/interest.d.ts +40 -0
  67. package/dist/store/interest.js +77 -0
  68. package/dist/store/inventory.d.ts +81 -0
  69. package/dist/store/inventory.js +140 -0
  70. package/dist/store/join.d.ts +118 -0
  71. package/dist/store/join.js +724 -0
  72. package/dist/store/json.d.ts +50 -0
  73. package/dist/store/json.js +283 -0
  74. package/dist/store/ledger.d.ts +187 -0
  75. package/dist/store/ledger.js +292 -0
  76. package/dist/store/owner.d.ts +616 -0
  77. package/dist/store/owner.js +1711 -0
  78. package/dist/store/patch.d.ts +70 -0
  79. package/dist/store/patch.js +166 -0
  80. package/dist/store/player.d.ts +65 -0
  81. package/dist/store/player.js +261 -0
  82. package/dist/store/replica.d.ts +187 -0
  83. package/dist/store/replica.js +740 -0
  84. package/dist/store/state.d.ts +42 -0
  85. package/dist/store/state.js +165 -0
  86. package/dist/store/types.d.ts +200 -0
  87. package/dist/store/types.js +25 -0
  88. package/dist/store/visibility.d.ts +118 -0
  89. package/dist/store/visibility.js +282 -0
  90. package/dist/store/wire.d.ts +309 -0
  91. package/dist/store/wire.js +834 -0
  92. package/dist/stream.d.ts +174 -0
  93. package/dist/stream.js +238 -0
  94. package/dist/three/index.d.ts +109 -0
  95. package/dist/three/index.js +130 -0
  96. package/dist/udp-probe.d.ts +159 -0
  97. package/dist/udp-probe.js +245 -0
  98. package/dist/wasm.d.ts +642 -0
  99. package/dist/wasm.js +98 -0
  100. package/dist/world/border.d.ts +84 -0
  101. package/dist/world/border.js +99 -0
  102. package/dist/world/handoff.d.ts +187 -0
  103. package/dist/world/handoff.js +222 -0
  104. package/dist/world/index.d.ts +13 -0
  105. package/dist/world/index.js +9 -0
  106. package/dist/world/link.d.ts +179 -0
  107. package/dist/world/link.js +476 -0
  108. package/dist/world/regions.d.ts +154 -0
  109. package/dist/world/regions.js +314 -0
  110. package/package.json +72 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Declarative visibility: say which parts of the state are secret, and
3
+ * the host removes them from every player's view before anything is
4
+ * sent.
5
+ *
6
+ * ```ts
7
+ * defineStore({
8
+ * …,
9
+ * visibility: {
10
+ * 'players.*.hand': 'owner', // only the player whose id is that key
11
+ * 'deck': 'nobody', // the host only
12
+ * 'deck.length': 'everyone', // …but everyone may count it
13
+ * 'waypoint': ['command'], // players reading the `command` audience
14
+ * },
15
+ * });
16
+ * ```
17
+ *
18
+ * Why this exists: a hand-written `project` fails SILENTLY — the game
19
+ * works and the secret is readable in the browser. Rules declared here
20
+ * are enforced by the SDK, after any hand-written projection, so code
21
+ * can narrow what a rule allows but never widen it.
22
+ *
23
+ * **The rules.**
24
+ *
25
+ * - A path is dot-separated keys; `*` matches any key of an object (or
26
+ * index of an array). `deck.length` — a path ending in `.length` —
27
+ * reveals only the length of an array its parent rule hides.
28
+ * - `'everyone'`, `'nobody'` (the host only), `'owner'` (the player
29
+ * whose peer id is the key matched by the path's FIRST `*` — the
30
+ * `ships[context.peer]` pattern needs no setup), or a list of
31
+ * audiences (any one of them suffices).
32
+ * - Unlisted paths are visible, as without rules. A rule covers
33
+ * everything beneath its path, and a deeper rule cannot re-open what
34
+ * a shallower one hid (except `.length`).
35
+ *
36
+ * **What hidden looks like.** An entry of a collection (a path ending
37
+ * in `*`) is removed. A field becomes the {@link HIDDEN} marker — never
38
+ * `0`, `""` or `null`, which a game would read as real values — so the
39
+ * state validator must accept it there: wrap that field's parser with
40
+ * {@link hiddenOr}. A counted array becomes an array of markers of the
41
+ * same length.
42
+ *
43
+ * **Presets**, for the common shapes: `'open'` (everyone sees
44
+ * everything — an explicit decision rather than an accident) and
45
+ * `'card-game'` (`players.*.hand` owner, `deck` nobody, `deck.length`
46
+ * everyone). A preset takes overrides: `{ preset: 'card-game',
47
+ * 'players.*.score': 'nobody' }`.
48
+ *
49
+ * **What it cannot do.** The host itself holds everything: a player who
50
+ * hosts can read every secret in their own tab. Games with real stakes
51
+ * need a host that is not a player.
52
+ */
53
+ import { StoreError } from './errors.js';
54
+ /** The one hidden value. Plain JSON, so it survives the wire. */
55
+ export const HIDDEN = Object.freeze({ $hidden: true });
56
+ /** Whether a value is the hidden marker (the original, or one parsed off the wire). */
57
+ export function isHidden(value) {
58
+ return (typeof value === 'object' &&
59
+ value !== null &&
60
+ !Array.isArray(value) &&
61
+ Object.keys(value).length === 1 &&
62
+ value.$hidden === true);
63
+ }
64
+ /** Wrap a field's parser so the field may also be {@link HIDDEN}. */
65
+ export function hiddenOr(parse) {
66
+ return value => (isHidden(value) ? HIDDEN : parse(value));
67
+ }
68
+ const PRESETS = Object.freeze({
69
+ 'card-game': Object.freeze({
70
+ 'players.*.hand': 'owner',
71
+ deck: 'nobody',
72
+ 'deck.length': 'everyone',
73
+ }),
74
+ });
75
+ const SEGMENT = /^(\*|[^.*]+)$/;
76
+ /**
77
+ * Check and compile a visibility declaration. Throws `invalid-data` for
78
+ * anything that could not be enforced as written — at definition time,
79
+ * not at the first projection.
80
+ */
81
+ export function compileVisibility(visibility) {
82
+ let declared;
83
+ if (visibility === 'open') {
84
+ declared = {};
85
+ }
86
+ else if (typeof visibility === 'string') {
87
+ const preset = PRESETS[visibility];
88
+ if (preset === undefined)
89
+ throw new StoreError('invalid-data', `unknown visibility preset '${visibility}'`);
90
+ declared = { ...preset };
91
+ }
92
+ else if (typeof visibility === 'object' && visibility !== null) {
93
+ const { preset, ...rest } = visibility;
94
+ declared = {};
95
+ if (preset !== undefined) {
96
+ const base = PRESETS[preset];
97
+ if (base === undefined)
98
+ throw new StoreError('invalid-data', `unknown visibility preset '${String(preset)}'`);
99
+ Object.assign(declared, base);
100
+ }
101
+ Object.assign(declared, rest);
102
+ }
103
+ else {
104
+ throw new StoreError('invalid-data', 'visibility is a preset name or an object of path → rule');
105
+ }
106
+ const rules = [];
107
+ let perPlayer = false;
108
+ for (const [path, rule] of Object.entries(declared)) {
109
+ const segments = path.split('.');
110
+ if (path.length === 0 || !segments.every(segment => SEGMENT.test(segment))) {
111
+ throw new StoreError('invalid-data', `visibility path '${path}' is not dot-separated keys and '*'`);
112
+ }
113
+ const counts = segments.length > 1 && segments[segments.length - 1] === 'length';
114
+ const valid = rule === 'everyone' ||
115
+ rule === 'nobody' ||
116
+ rule === 'owner' ||
117
+ (Array.isArray(rule) && rule.length > 0 && rule.every(label => typeof label === 'string' && label.length > 0));
118
+ if (!valid) {
119
+ throw new StoreError('invalid-data', `visibility rule for '${path}' must be 'everyone', 'nobody', 'owner' or a non-empty list of audiences`);
120
+ }
121
+ if (rule === 'owner') {
122
+ if (!segments.includes('*')) {
123
+ throw new StoreError('invalid-data', `'owner' needs a '*' in the path to name the owner: '${path}'`);
124
+ }
125
+ perPlayer = true;
126
+ }
127
+ rules.push(Object.freeze({ path, segments: Object.freeze(segments), rule, counts }));
128
+ }
129
+ // Shallow first: a hidden parent removes its subtree before a child
130
+ // rule could be looked at.
131
+ rules.sort((a, b) => a.segments.length - b.segments.length);
132
+ return Object.freeze({ rules: Object.freeze(rules), perPlayer });
133
+ }
134
+ function allows(rule, captures, viewer) {
135
+ if (rule === 'everyone')
136
+ return true;
137
+ if (rule === 'nobody' || viewer === null)
138
+ return false;
139
+ if (rule === 'owner')
140
+ return captures[0] === viewer.peer;
141
+ return rule.some(label => viewer.audience.includes(label));
142
+ }
143
+ /** Every concrete location a pattern names in `value`. */
144
+ function matches(value, segments) {
145
+ const out = [];
146
+ const walk = (node, depth, keys, captures) => {
147
+ if (depth === segments.length) {
148
+ out.push({ keys: [...keys], captures: [...captures], value: node });
149
+ return;
150
+ }
151
+ if (typeof node !== 'object' || node === null)
152
+ return;
153
+ const segment = segments[depth];
154
+ if (segment === '*') {
155
+ const entries = Array.isArray(node)
156
+ ? node.map((child, index) => [index, child])
157
+ : Object.entries(node);
158
+ for (const [key, child] of entries) {
159
+ walk(child, depth + 1, [...keys, key], [...captures, String(key)]);
160
+ }
161
+ return;
162
+ }
163
+ if (Array.isArray(node) || !Object.prototype.hasOwnProperty.call(node, segment))
164
+ return;
165
+ walk(node[segment], depth + 1, [...keys, segment], captures);
166
+ };
167
+ walk(value, 0, [], []);
168
+ return out;
169
+ }
170
+ const DROP = Symbol('drop');
171
+ function record(edits, keys, value) {
172
+ let node = edits;
173
+ for (const key of keys) {
174
+ let child = node.children.get(key);
175
+ if (child === undefined) {
176
+ child = { children: new Map() };
177
+ node.children.set(key, child);
178
+ }
179
+ node = child;
180
+ }
181
+ node.value = value;
182
+ }
183
+ function rebuild(node, edits) {
184
+ if ('value' in edits)
185
+ return edits.value;
186
+ if (edits.children.size === 0 || typeof node !== 'object' || node === null)
187
+ return node;
188
+ if (Array.isArray(node)) {
189
+ const out = [];
190
+ node.forEach((child, index) => {
191
+ const edit = edits.children.get(index);
192
+ const next = edit === undefined ? child : rebuild(child, edit);
193
+ if (next !== DROP)
194
+ out.push(next);
195
+ });
196
+ return out;
197
+ }
198
+ const out = {};
199
+ for (const [key, child] of Object.entries(node)) {
200
+ const edit = edits.children.get(key);
201
+ const next = edit === undefined ? child : rebuild(child, edit);
202
+ if (next !== DROP)
203
+ out[key] = next;
204
+ }
205
+ return out;
206
+ }
207
+ const SEP = '\u0000';
208
+ /**
209
+ * Apply compiled rules for one viewer (`null`: nobody in particular —
210
+ * sees only what `'everyone'` may).
211
+ */
212
+ export function applyVisibility(compiled, state, viewer) {
213
+ if (compiled.rules.length === 0)
214
+ return state;
215
+ const edits = { children: new Map() };
216
+ // Every hidden location, by its joined path, so "is this under a
217
+ // hidden ancestor" is a lookup per prefix, not a scan of all of them.
218
+ const hidden = new Set();
219
+ let touched = false;
220
+ for (const rule of compiled.rules) {
221
+ if (rule.counts)
222
+ continue;
223
+ // A `.length` rule on this path may reveal the count.
224
+ const countRule = compiled.rules.find(candidate => candidate.counts &&
225
+ candidate.segments.length === rule.segments.length + 1 &&
226
+ candidate.segments.slice(0, -1).every((segment, index) => segment === rule.segments[index]));
227
+ // Last pattern segment a wildcard: an entry of a collection, removed.
228
+ const entry = rule.segments[rule.segments.length - 1] === '*';
229
+ for (const match of matches(state, rule.segments)) {
230
+ let prefix = '';
231
+ let under = false;
232
+ for (let i = 0; i < match.keys.length - 1 && !under; i += 1) {
233
+ prefix = i === 0 ? String(match.keys[0]) : `${prefix}${SEP}${String(match.keys[i])}`;
234
+ under = hidden.has(prefix);
235
+ }
236
+ if (under)
237
+ continue;
238
+ if (allows(rule.rule, match.captures, viewer))
239
+ continue;
240
+ let replacement = entry ? DROP : HIDDEN;
241
+ if (countRule !== undefined && Array.isArray(match.value) && allows(countRule.rule, match.captures, viewer)) {
242
+ replacement = match.value.map(() => HIDDEN);
243
+ }
244
+ record(edits, match.keys, replacement);
245
+ hidden.add(match.keys.map(String).join(SEP));
246
+ touched = true;
247
+ }
248
+ }
249
+ return (touched ? rebuild(state, edits) : state);
250
+ }
251
+ /**
252
+ * The view a viewer would receive under a definition's declared rules,
253
+ * for tests and tools. (A host also applies its hand-written `project`
254
+ * / `projectFor` first; this is the rules alone.)
255
+ */
256
+ export function projectVisible(definition, state, viewer) {
257
+ if (definition.visibility === undefined)
258
+ return state;
259
+ return applyVisibility(compileVisibility(definition.visibility), state, viewer);
260
+ }
261
+ /**
262
+ * Throw if `viewer` could read any of `paths` (concrete dot paths, e.g.
263
+ * `players.bob.hand`) under the definition's rules. Absent, or the
264
+ * hidden marker, is hidden; anything else is a leak.
265
+ */
266
+ export function assertHidden(definition, state, viewer, paths) {
267
+ const view = projectVisible(definition, state, viewer);
268
+ for (const path of paths) {
269
+ let node = view;
270
+ for (const key of path.split('.')) {
271
+ if (typeof node !== 'object' || node === null || !Object.prototype.hasOwnProperty.call(node, key)) {
272
+ node = undefined;
273
+ break;
274
+ }
275
+ node = node[key];
276
+ }
277
+ const leaked = node !== undefined && !isHidden(node) && !(Array.isArray(node) && node.every(element => isHidden(element)));
278
+ if (leaked) {
279
+ throw new Error(`'${path}' is visible to ${viewer === null ? 'nobody in particular' : viewer.peer}`);
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * The store's wire protocol: shapes, the parser ladder, canonical
3
+ * encodings (brief §1.4, §1.12, §2).
4
+ *
5
+ * Two entry points, and they share every rule: {@link decodeMessage}
6
+ * refuses anything the protocol does not define, and
7
+ * {@link encodeMessage} refuses to *produce* anything it would refuse
8
+ * to accept. One validator, both directions — a producer that can emit
9
+ * a frame the parser rejects is a bug that only shows up on the far
10
+ * side of a network.
11
+ *
12
+ * ## The ladder, and where it stops here
13
+ *
14
+ * §1.12 is ordered, cheap checks first, and none of them may run after
15
+ * a mutation. This module implements the rungs that are decidable from
16
+ * the frame alone:
17
+ *
18
+ * 1. **byte length** against the runtime's admissible size → `capacity`
19
+ * (a declared bound was reached), refused **before** parsing;
20
+ * 2. **JSON** under `json.ts` — depth, duplicate keys, finite numbers;
21
+ * 3. **envelope** — `v` known, `k` in the closed set, `q`/`h` present
22
+ * exactly where the kind requires them;
23
+ * 4. **direction** — a replica that receives `act`, or an owner that
24
+ * receives `delta`, refuses `invalid-data`;
25
+ * 6. **payload** — per-kind required/optional fields, unknown fields
26
+ * refused, and every value in its canonical spelling.
27
+ *
28
+ * Rung 4's *admission class* (`not-ready` for gameplay before
29
+ * readiness) and rung 5 (**binding**: handle active, bound to this
30
+ * authenticated peer, per-kind generation checks) are **not here**.
31
+ * They are decidable only against handle state, which is owner dispatch
32
+ * — a later slice. This module deliberately answers "is this a legal
33
+ * frame", never "may this caller do this now", and callers must not
34
+ * read a successful decode as authorization.
35
+ */
36
+ import { type StoreErrorCode } from './errors.js';
37
+ import { type JsonObject, type JsonValue } from './json.js';
38
+ /** The only protocol version this build speaks. */
39
+ export declare const WIRE_VERSION = 1;
40
+ /** Exact hex lengths (brief §1.12: lowercase only, exact length). */
41
+ export declare const HANDLE_HEX_LENGTH = 32;
42
+ export declare const REQUEST_HEX_LENGTH = 16;
43
+ export declare const INCARNATION_HEX_LENGTH = 16;
44
+ /** Audience bounds (brief §2). */
45
+ export declare const MAX_AUDIENCE_LABELS = 32;
46
+ export declare const MAX_AUDIENCE_LABEL_BYTES = 128;
47
+ /**
48
+ * Interest keys per request, and bytes per key. Interest is a spatial
49
+ * filter, not a permission, so its bound is separate from — and larger
50
+ * than — the audience's; the frame budget is what really caps it.
51
+ */
52
+ export declare const MAX_INTEREST_KEYS = 256;
53
+ export declare const MAX_INTEREST_KEY_BYTES = 64;
54
+ /** Snapshot bounds (brief §2). */
55
+ export declare const MAX_SNAPSHOT_BYTES: number;
56
+ export declare const MAX_SNAPSHOT_CHUNKS = 255;
57
+ /** Patch bounds (brief §2, applied structurally here, semantically in `patch.ts`). */
58
+ export declare const MAX_PATCH_OPS = 256;
59
+ export declare const MAX_PATH_SEGMENTS = 8;
60
+ export declare const MAX_PATH_SEGMENT_BYTES = 64;
61
+ /** `no.detail` is truncated at the owner to this (brief §2). */
62
+ export declare const MAX_DETAIL_BYTES = 256;
63
+ /** The largest value a canonical decimal may carry. */
64
+ export declare const MAX_DECIMAL = 18446744073709551615n;
65
+ /** Caller → owner kinds (brief §1.4). */
66
+ export declare const CALLER_KINDS: readonly ["join", "resume", "resync", "act", "in", "aud", "alive", "leave", "int"];
67
+ /** Owner → caller kinds (brief §1.4). */
68
+ export declare const OWNER_KINDS: readonly ["man", "snap", "delta", "res", "ok", "no"];
69
+ export type CallerKind = (typeof CALLER_KINDS)[number];
70
+ export type OwnerKind = (typeof OWNER_KINDS)[number];
71
+ export type MessageKind = CallerKind | OwnerKind;
72
+ /**
73
+ * A decimal field, kept as the **wire spelling**.
74
+ *
75
+ * `g`, `r`, `s`, `base`, `have` and `bytes` are `u64`-ranged decimal
76
+ * strings, not JSON numbers, and they are not converted on decode: the
77
+ * canonical spelling is the value's identity here, and re-deriving it
78
+ * from a `number` would silently lose precision above 2^53 and give one
79
+ * value two spellings. Use {@link decimalValue} when arithmetic is
80
+ * actually needed.
81
+ */
82
+ export type Decimal = string;
83
+ /** Lowercase, exact-length hex. */
84
+ export type Hex = string;
85
+ export interface JoinMessage {
86
+ readonly k: 'join';
87
+ readonly q: Hex;
88
+ readonly def: string;
89
+ readonly ver: number;
90
+ /** Which store of this definition — the owner's declared address. */
91
+ readonly store: string;
92
+ readonly key: string;
93
+ readonly aud: readonly string[];
94
+ readonly int?: readonly string[];
95
+ }
96
+ export interface ResumeMessage {
97
+ readonly k: 'resume';
98
+ readonly q: Hex;
99
+ readonly h: Hex;
100
+ readonly aud: readonly string[];
101
+ readonly int?: readonly string[];
102
+ }
103
+ /** Replace the caller's interest set; answered by a delta, then `ok`. */
104
+ export interface InterestMessage {
105
+ readonly k: 'int';
106
+ readonly q: Hex;
107
+ readonly h: Hex;
108
+ readonly int: readonly string[];
109
+ }
110
+ export interface ResyncMessage {
111
+ readonly k: 'resync';
112
+ readonly q: Hex;
113
+ readonly h: Hex;
114
+ readonly g: Decimal;
115
+ readonly have: Decimal;
116
+ }
117
+ export interface AudienceMessage {
118
+ readonly k: 'aud';
119
+ readonly q: Hex;
120
+ readonly h: Hex;
121
+ readonly aud: readonly string[];
122
+ }
123
+ export interface AliveMessage {
124
+ readonly k: 'alive';
125
+ readonly q: Hex;
126
+ readonly h: Hex;
127
+ }
128
+ export interface LeaveMessage {
129
+ readonly k: 'leave';
130
+ readonly q: Hex;
131
+ readonly h: Hex;
132
+ }
133
+ export interface ActMessage {
134
+ readonly k: 'act';
135
+ readonly q: Hex;
136
+ readonly h: Hex;
137
+ readonly s: Decimal;
138
+ readonly name: string;
139
+ readonly in: JsonObject;
140
+ }
141
+ export interface InputMessage {
142
+ readonly k: 'in';
143
+ readonly h: Hex;
144
+ readonly name: string;
145
+ readonly s: Decimal;
146
+ readonly in: JsonObject;
147
+ }
148
+ export interface ManifestMessage {
149
+ readonly k: 'man';
150
+ readonly q?: Hex;
151
+ readonly h: Hex;
152
+ readonly inc: Hex;
153
+ readonly g: Decimal;
154
+ readonly r: Decimal;
155
+ readonly n: number;
156
+ readonly bytes: Decimal;
157
+ }
158
+ export interface SnapMessage {
159
+ readonly k: 'snap';
160
+ readonly h: Hex;
161
+ readonly g: Decimal;
162
+ readonly r: Decimal;
163
+ readonly i: number;
164
+ readonly n: number;
165
+ readonly d: string;
166
+ }
167
+ export interface DeltaMessage {
168
+ readonly k: 'delta';
169
+ readonly h: Hex;
170
+ readonly g: Decimal;
171
+ readonly base: Decimal;
172
+ readonly r: Decimal;
173
+ readonly ops: readonly WireOp[];
174
+ }
175
+ export interface ResultMessage {
176
+ readonly k: 'res';
177
+ readonly q: Hex;
178
+ readonly h: Hex;
179
+ readonly s: Decimal;
180
+ readonly out: JsonObject;
181
+ }
182
+ export interface OkMessage {
183
+ readonly k: 'ok';
184
+ readonly q: Hex;
185
+ readonly h: Hex;
186
+ }
187
+ export interface NoMessage {
188
+ readonly k: 'no';
189
+ readonly q?: Hex;
190
+ readonly h?: Hex;
191
+ readonly code: StoreErrorCode;
192
+ readonly s?: Decimal;
193
+ readonly detail?: string;
194
+ }
195
+ /** A patch operation as it appears on the wire (semantics: `patch.ts`). */
196
+ export type WireOp = {
197
+ readonly o: 'r';
198
+ readonly p: readonly string[];
199
+ readonly val: JsonValue;
200
+ } | {
201
+ readonly o: 'x';
202
+ readonly p: readonly string[];
203
+ };
204
+ /** Every kind a caller sends. What an **owner** can receive. */
205
+ export type CallerMessage = JoinMessage | ResumeMessage | InterestMessage | ResyncMessage | AudienceMessage | AliveMessage | LeaveMessage | ActMessage | InputMessage;
206
+ /** Every kind an owner sends. What a **replica** can receive. */
207
+ export type OwnerMessage = ManifestMessage | SnapMessage | DeltaMessage | ResultMessage | OkMessage | NoMessage;
208
+ /**
209
+ * What a given side can receive.
210
+ *
211
+ * The direction rung (§1.12) is a type here as well as a check: an
212
+ * owner's decode cannot yield a `delta`, so a dispatcher does not need
213
+ * a dead branch to prove it.
214
+ */
215
+ export type MessageFor<T extends Side> = T extends 'owner' ? CallerMessage : OwnerMessage;
216
+ export type StoreMessage = JoinMessage | ResumeMessage | InterestMessage | ResyncMessage | AudienceMessage | AliveMessage | LeaveMessage | ActMessage | InputMessage | ManifestMessage | SnapMessage | DeltaMessage | ResultMessage | OkMessage | NoMessage;
217
+ /** Which side is reading (rung 4: direction). */
218
+ export type Side = 'owner' | 'replica';
219
+ /** Which rung refused the frame. */
220
+ export type DecodeStage = 'bytes' | 'json' | 'envelope' | 'direction' | 'payload';
221
+ export interface DecodeRefusal {
222
+ readonly ok: false;
223
+ /** The wire code a dispatcher answers with. */
224
+ readonly code: StoreErrorCode;
225
+ readonly stage: DecodeStage;
226
+ /**
227
+ * A stable, countable reason. The dispatcher counts these; they are
228
+ * not caller-facing text, and they never carry frame content.
229
+ */
230
+ readonly reason: string;
231
+ }
232
+ export type DecodeResult<T extends Side = Side> = {
233
+ readonly ok: true;
234
+ readonly message: MessageFor<T>;
235
+ } | DecodeRefusal;
236
+ export interface DecodeOptions<T extends Side = Side> {
237
+ /**
238
+ * The admissible frame size, **derived at runtime** from the
239
+ * transport (`LeafNode.maxEventBytes()` minus the kind's envelope
240
+ * reserve) and never hardcoded here — a store that guessed this
241
+ * number would be wrong on the first transport that changed.
242
+ */
243
+ readonly maxBytes: number;
244
+ /** The side reading the frame, for the direction rung. */
245
+ readonly as: T;
246
+ }
247
+ /** UTF-8 byte length without allocating a copy of the bytes. */
248
+ export declare function utf8Length(text: string): number;
249
+ /**
250
+ * Decode one store frame.
251
+ *
252
+ * Never throws for bad input: a refusal is a value, because the
253
+ * dispatcher must count it and answer, and an exception path is how
254
+ * "count it" gets skipped.
255
+ */
256
+ export declare function decodeMessage<T extends Side>(frame: string, options: DecodeOptions<T>): DecodeResult<T>;
257
+ /** Lowercase hex, exact length checked by the caller. */
258
+ export declare function isCanonicalHex(value: string): boolean;
259
+ /**
260
+ * ASCII digits, no sign, no leading zero unless the value is exactly
261
+ * `"0"`, ≤ 20 digits, ≤ 2^64 − 1 (brief §1.12).
262
+ */
263
+ export declare function isCanonicalDecimal(value: string): boolean;
264
+ /**
265
+ * Standard alphabet, correctly padded, no whitespace or line breaks.
266
+ *
267
+ * Also rejects a *decodable but non-canonical* tail: base64 pads to a
268
+ * multiple of four, and the bits before the padding must be zero, so
269
+ * `"QQ=="` is canonical for `A` while `"QR=="` decodes to the same byte
270
+ * and is not.
271
+ */
272
+ export declare function isCanonicalBase64(value: string): boolean;
273
+ /** The numeric value of a canonical decimal field. */
274
+ export declare function decimalValue(value: Decimal): bigint;
275
+ /**
276
+ * Whether a value survives JSON serialization as itself.
277
+ *
278
+ * `JSON.stringify` is **lossy and silent**: `NaN`, `Infinity` and
279
+ * `-Infinity` become `null`, and `undefined`, functions and symbols
280
+ * are dropped from objects and become `null` in arrays. Validating the
281
+ * serialized text therefore cannot see what was asked for — the
282
+ * evidence is already gone — so an action input of `{n: NaN}` encoded
283
+ * happily as `{"n":null}` and sent a value the caller never wrote.
284
+ *
285
+ * It also TRANSFORMS some values and THROWS on others, all before any
286
+ * scan of the serialized text could see anything: a `toJSON` method
287
+ * replaces its object wholesale (a `Date` ships as a string), a
288
+ * `Map`/`Set` ships as `{}` with its entries gone, an own symbol key
289
+ * is never serialized at all, and a cycle or a `BigInt` raises a bare
290
+ * `TypeError` out of the serializer itself.
291
+ *
292
+ * These are ordinary values at the type level; no cast or exotic
293
+ * object is needed to produce them. So the source values are checked
294
+ * **before** serialization, and an inadmissible one is a refusal
295
+ * rather than a substitution or an escape. `null` itself stays
296
+ * admissible: an intentional null is data.
297
+ */
298
+ export declare function inadmissibleValue(value: unknown, path: string): string | null;
299
+ /**
300
+ * Encode a message in its canonical spelling.
301
+ *
302
+ * Validates the **source values** first — because serialization
303
+ * destroys the evidence of a non-finite number — then validates the
304
+ * serialized frame through {@link decodeMessage}'s own rules, so a
305
+ * producer cannot emit a frame the parser would refuse. Throws rather
306
+ * than returning a refusal: this is a local programming error, not
307
+ * untrusted input.
308
+ */
309
+ export declare function encodeMessage(message: StoreMessage): string;