@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.
- package/CHANGELOG.md +517 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +666 -0
- package/dist/async-queue.d.ts +26 -0
- package/dist/async-queue.js +62 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.js +95 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.js +726 -0
- package/dist/events.d.ts +232 -0
- package/dist/events.js +298 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +78 -0
- package/dist/index.bundle.js +2 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/leader/events.d.ts +111 -0
- package/dist/leader/events.js +116 -0
- package/dist/leader/index.d.ts +14 -0
- package/dist/leader/index.js +11 -0
- package/dist/leader/session.d.ts +539 -0
- package/dist/leader/session.js +770 -0
- package/dist/leader/wasm.d.ts +123 -0
- package/dist/leader/wasm.js +39 -0
- package/dist/lobby.d.ts +183 -0
- package/dist/lobby.js +504 -0
- package/dist/local.d.ts +80 -0
- package/dist/local.js +160 -0
- package/dist/net_leaf.d.ts +1494 -0
- package/dist/net_leaf.js +3753 -0
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +137 -0
- package/dist/netcode/client.d.ts +102 -0
- package/dist/netcode/client.js +296 -0
- package/dist/netcode/clock.d.ts +45 -0
- package/dist/netcode/clock.js +54 -0
- package/dist/netcode/host.d.ts +118 -0
- package/dist/netcode/host.js +276 -0
- package/dist/netcode/index.d.ts +19 -0
- package/dist/netcode/index.js +14 -0
- package/dist/netcode/interpolate.d.ts +50 -0
- package/dist/netcode/interpolate.js +100 -0
- package/dist/netcode/wire.d.ts +125 -0
- package/dist/netcode/wire.js +117 -0
- package/dist/node.d.ts +908 -0
- package/dist/node.js +1085 -0
- package/dist/org.d.ts +412 -0
- package/dist/org.js +567 -0
- package/dist/peer-driver.d.ts +117 -0
- package/dist/peer-driver.js +224 -0
- package/dist/store/assembly.d.ts +176 -0
- package/dist/store/assembly.js +325 -0
- package/dist/store/chunker.d.ts +83 -0
- package/dist/store/chunker.js +190 -0
- package/dist/store/core.d.ts +136 -0
- package/dist/store/core.js +477 -0
- package/dist/store/definition.d.ts +23 -0
- package/dist/store/definition.js +83 -0
- package/dist/store/errors.d.ts +77 -0
- package/dist/store/errors.js +94 -0
- package/dist/store/host.d.ts +247 -0
- package/dist/store/host.js +675 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.js +40 -0
- package/dist/store/interest.d.ts +40 -0
- package/dist/store/interest.js +77 -0
- package/dist/store/inventory.d.ts +81 -0
- package/dist/store/inventory.js +140 -0
- package/dist/store/join.d.ts +118 -0
- package/dist/store/join.js +724 -0
- package/dist/store/json.d.ts +50 -0
- package/dist/store/json.js +283 -0
- package/dist/store/ledger.d.ts +187 -0
- package/dist/store/ledger.js +292 -0
- package/dist/store/owner.d.ts +616 -0
- package/dist/store/owner.js +1711 -0
- package/dist/store/patch.d.ts +70 -0
- package/dist/store/patch.js +166 -0
- package/dist/store/player.d.ts +65 -0
- package/dist/store/player.js +261 -0
- package/dist/store/replica.d.ts +187 -0
- package/dist/store/replica.js +740 -0
- package/dist/store/state.d.ts +42 -0
- package/dist/store/state.js +165 -0
- package/dist/store/types.d.ts +200 -0
- package/dist/store/types.js +25 -0
- package/dist/store/visibility.d.ts +118 -0
- package/dist/store/visibility.js +282 -0
- package/dist/store/wire.d.ts +309 -0
- package/dist/store/wire.js +834 -0
- package/dist/stream.d.ts +174 -0
- package/dist/stream.js +238 -0
- package/dist/three/index.d.ts +109 -0
- package/dist/three/index.js +130 -0
- package/dist/udp-probe.d.ts +159 -0
- package/dist/udp-probe.js +245 -0
- package/dist/wasm.d.ts +642 -0
- package/dist/wasm.js +98 -0
- package/dist/world/border.d.ts +84 -0
- package/dist/world/border.js +99 -0
- package/dist/world/handoff.d.ts +187 -0
- package/dist/world/handoff.js +222 -0
- package/dist/world/index.d.ts +13 -0
- package/dist/world/index.js +9 -0
- package/dist/world/link.d.ts +179 -0
- package/dist/world/link.js +476 -0
- package/dist/world/regions.d.ts +154 -0
- package/dist/world/regions.js +314 -0
- 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;
|