@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,136 @@
1
+ /**
2
+ * The local half of a store: state, subscriptions, status and the
3
+ * synchronous transaction.
4
+ *
5
+ * This is deliberately transport-free. Both the authoritative host and
6
+ * a joined replica are this object plus a network layer, which is why
7
+ * every rule that does *not* need a peer lives here and is testable
8
+ * without a mesh: immutability, identity preservation, selector
9
+ * equality, notification counting, and the transaction contract.
10
+ *
11
+ * **The transaction contract is the sharp part.** A handler runs
12
+ * synchronously, stages changes, and either commits one revision or
13
+ * discards everything:
14
+ *
15
+ * - `getState()` inside a handler reads the *staged* value, so a
16
+ * handler sees its own writes.
17
+ * - A throw discards the staged changes. Nothing is published.
18
+ * - Returning a thenable is refused, because an `async` handler would
19
+ * commit after its transaction closed — the TypeScript signature
20
+ * already says `undefined`, and JavaScript callers bypass types.
21
+ * - The context is valid only for that transaction. A retained context
22
+ * cannot write from a later microtask.
23
+ * - Read-only callbacks (validators, `authorize`, `project`, update
24
+ * functions) may not reenter a setter.
25
+ * - A public owner `setState` called synchronously *inside* a handler
26
+ * joins the same staged transaction rather than committing beside it,
27
+ * so a handler's rollback cannot be defeated by going through the
28
+ * public handle.
29
+ */
30
+ import { type PatchOutcome } from './patch.js';
31
+ import type { WireOp } from './wire.js';
32
+ import type { Cancel, ReadonlyState, SelectorOptions, StateUpdate, StoreDefinition, StorePhase, StoreStatus, ActionContext, ActionSpec, InputSpec } from './types.js';
33
+ /**
34
+ * The transport-free store.
35
+ *
36
+ * `S` is the validated state shape. The definition is held for its
37
+ * `state` validator, which every commit passes through — an owner
38
+ * cannot publish state its own schema rejects.
39
+ */
40
+ export declare class StoreCore<S extends object, A extends ActionSpec, I extends InputSpec> {
41
+ #private;
42
+ constructor(options: {
43
+ definition: StoreDefinition<S, A, I>;
44
+ initialState: S;
45
+ phase?: StorePhase;
46
+ });
47
+ /** The current immutable snapshot. Never initiates I/O. */
48
+ getState(): ReadonlyState<S>;
49
+ /** Monotone count of applied transactions. Unchanged updates do not move it. */
50
+ get revision(): number;
51
+ /**
52
+ * Whether a synchronous transaction is open on this store.
53
+ *
54
+ * The owner's public `commit` reads this to DEFER its delta
55
+ * emission to the transaction's commit: no frame may leave for
56
+ * state the transaction may still discard (#5).
57
+ */
58
+ get inTransaction(): boolean;
59
+ subscribe(listener: (state: ReadonlyState<S>, previous: ReadonlyState<S>) => void): Cancel;
60
+ subscribe<T>(selector: (state: ReadonlyState<S>) => T, listener: (selected: T, previous: T) => void, options?: SelectorOptions<T>): Cancel;
61
+ getStatus(): StoreStatus;
62
+ subscribeStatus(listener: (status: StoreStatus, previous: StoreStatus) => void): Cancel;
63
+ /**
64
+ * Move readiness. A transition to an identical status is not a
65
+ * transition: the object keeps its identity and nobody is notified.
66
+ */
67
+ setStatus(patch: Partial<StoreStatus>): void;
68
+ /**
69
+ * Run one handler as a transaction and commit its staged changes.
70
+ *
71
+ * Returns whatever the handler returned. A throw propagates with
72
+ * nothing published.
73
+ */
74
+ transact<R>(run: (context: ActionContext<S>) => R, peer: string): R;
75
+ /**
76
+ * The owner's public write. Inside a handler it joins that handler's
77
+ * transaction; outside one it is its own single-revision transaction.
78
+ */
79
+ applyOwnerUpdate(update: StateUpdate<S>): void;
80
+ /**
81
+ * Apply a full snapshot: validate, then reconcile.
82
+ *
83
+ * An equivalent snapshot is a no-op — same root reference, no
84
+ * notifications — which is what stops a periodic resynchronization
85
+ * from looking like a world change to every selector.
86
+ *
87
+ * Inside a handler it JOINS the open transaction, exactly as
88
+ * `applyOwnerUpdate` does: the owner's public `setState` reaches
89
+ * here through `commit`, and committing beside the transaction
90
+ * published a handler's world while `transact` then reported
91
+ * `action-rejected` for it — or, on the committing path, silently
92
+ * reverted the nested write after subscribers had seen it.
93
+ */
94
+ applySnapshot(raw: unknown): void;
95
+ /**
96
+ * Write entities of one declared collection: each changed entity
97
+ * validated by its PER-ENTITY parser (`definition.entities`), and the
98
+ * document committed WITHOUT the whole-document validator.
99
+ *
100
+ * `changes` maps an id to its new value, or to `undefined` to remove
101
+ * it. The untouched entities keep their identity, so reconciliation
102
+ * and every downstream diff short-circuit on them. What is not free:
103
+ * rebuilding the collection object, O(entities) shallow and with no
104
+ * parsing.
105
+ *
106
+ * Inside a handler it JOINS the open transaction, like
107
+ * {@link applySnapshot}. The whole-document validator is skipped by
108
+ * contract: declaring the collection in `entities` says `state`
109
+ * imposes nothing on it beyond each entity passing its parser.
110
+ */
111
+ applyEntities(collection: string, changes: Readonly<Record<string, unknown>>): void;
112
+ /**
113
+ * Apply a replica delta: the owner's patch operations, at this
114
+ * document, validated by this definition.
115
+ *
116
+ * This lives here rather than in the replica because the store owns
117
+ * the document, its validator and its revision. A replica that read
118
+ * the state out, patched it outside and pushed the result back would
119
+ * validate twice per delta and would be free to publish something
120
+ * the definition never saw.
121
+ *
122
+ * Refusals are returned, not thrown: a delta that does not apply is
123
+ * a recoverable divergence (§1.8 resynchronization), not a caller
124
+ * error.
125
+ */
126
+ applyDelta(ops: readonly WireOp[]): PatchOutcome<S>;
127
+ /**
128
+ * Run a read-only callback with setters fenced off.
129
+ *
130
+ * The host layer routes `authorize` and `project` through this, so a
131
+ * callback that is documented as read-only cannot quietly mutate.
132
+ */
133
+ runReadOnly<R>(run: () => R): R;
134
+ /** Fence the handle: closed, stale, no listeners retained. */
135
+ close(): void;
136
+ }
@@ -0,0 +1,477 @@
1
+ /**
2
+ * The local half of a store: state, subscriptions, status and the
3
+ * synchronous transaction.
4
+ *
5
+ * This is deliberately transport-free. Both the authoritative host and
6
+ * a joined replica are this object plus a network layer, which is why
7
+ * every rule that does *not* need a peer lives here and is testable
8
+ * without a mesh: immutability, identity preservation, selector
9
+ * equality, notification counting, and the transaction contract.
10
+ *
11
+ * **The transaction contract is the sharp part.** A handler runs
12
+ * synchronously, stages changes, and either commits one revision or
13
+ * discards everything:
14
+ *
15
+ * - `getState()` inside a handler reads the *staged* value, so a
16
+ * handler sees its own writes.
17
+ * - A throw discards the staged changes. Nothing is published.
18
+ * - Returning a thenable is refused, because an `async` handler would
19
+ * commit after its transaction closed — the TypeScript signature
20
+ * already says `undefined`, and JavaScript callers bypass types.
21
+ * - The context is valid only for that transaction. A retained context
22
+ * cannot write from a later microtask.
23
+ * - Read-only callbacks (validators, `authorize`, `project`, update
24
+ * functions) may not reenter a setter.
25
+ * - A public owner `setState` called synchronously *inside* a handler
26
+ * joins the same staged transaction rather than committing beside it,
27
+ * so a handler's rollback cannot be defeated by going through the
28
+ * public handle.
29
+ */
30
+ import { StoreError } from './errors.js';
31
+ import { mergeShallow, reconcile } from './state.js';
32
+ import { isEntityMap } from './definition.js';
33
+ import { applyPatch } from './patch.js';
34
+ /**
35
+ * The transport-free store.
36
+ *
37
+ * `S` is the validated state shape. The definition is held for its
38
+ * `state` validator, which every commit passes through — an owner
39
+ * cannot publish state its own schema rejects.
40
+ */
41
+ export class StoreCore {
42
+ #definition;
43
+ #subscriptions = new Set();
44
+ #statusListeners = new Set();
45
+ #state;
46
+ #status;
47
+ #revision = 0;
48
+ #transaction = null;
49
+ #readOnlyDepth = 0;
50
+ #closed = false;
51
+ constructor(options) {
52
+ this.#definition = options.definition;
53
+ // Validate and freeze once at construction: from here on the only
54
+ // way in is a commit, and every commit reconciles against this.
55
+ this.#state = reconcile(undefined, this.#validate(options.initialState));
56
+ this.#status = Object.freeze({
57
+ phase: options.phase ?? 'ready',
58
+ stale: false,
59
+ error: null,
60
+ });
61
+ }
62
+ /** The current immutable snapshot. Never initiates I/O. */
63
+ getState() {
64
+ return this.#state;
65
+ }
66
+ /** Monotone count of applied transactions. Unchanged updates do not move it. */
67
+ get revision() {
68
+ return this.#revision;
69
+ }
70
+ /**
71
+ * Whether a synchronous transaction is open on this store.
72
+ *
73
+ * The owner's public `commit` reads this to DEFER its delta
74
+ * emission to the transaction's commit: no frame may leave for
75
+ * state the transaction may still discard (#5).
76
+ */
77
+ get inTransaction() {
78
+ return this.#transaction !== null && this.#transaction.active;
79
+ }
80
+ subscribe(first, second, options) {
81
+ const subscription = second === undefined
82
+ ? this.#rootSubscription(first)
83
+ : this.#selectorSubscription(first, second, options);
84
+ this.#subscriptions.add(subscription);
85
+ let live = true;
86
+ return () => {
87
+ if (!live)
88
+ return;
89
+ live = false;
90
+ this.#subscriptions.delete(subscription);
91
+ };
92
+ }
93
+ getStatus() {
94
+ return this.#status;
95
+ }
96
+ subscribeStatus(listener) {
97
+ this.#statusListeners.add(listener);
98
+ let live = true;
99
+ return () => {
100
+ if (!live)
101
+ return;
102
+ live = false;
103
+ this.#statusListeners.delete(listener);
104
+ };
105
+ }
106
+ /**
107
+ * Move readiness. A transition to an identical status is not a
108
+ * transition: the object keeps its identity and nobody is notified.
109
+ */
110
+ setStatus(patch) {
111
+ const previous = this.#status;
112
+ const candidate = {
113
+ phase: patch.phase ?? previous.phase,
114
+ stale: patch.stale ?? previous.stale,
115
+ error: patch.error === undefined ? previous.error : patch.error,
116
+ };
117
+ if (candidate.phase === previous.phase &&
118
+ candidate.stale === previous.stale &&
119
+ Object.is(candidate.error, previous.error)) {
120
+ return;
121
+ }
122
+ this.#status = Object.freeze(candidate);
123
+ for (const listener of [...this.#statusListeners]) {
124
+ // Same rule as the state pass: a cancellation that has already
125
+ // returned is honoured for the rest of this pass.
126
+ if (!this.#statusListeners.has(listener))
127
+ continue;
128
+ dispatch(() => listener(this.#status, previous));
129
+ }
130
+ }
131
+ /**
132
+ * Run one handler as a transaction and commit its staged changes.
133
+ *
134
+ * Returns whatever the handler returned. A throw propagates with
135
+ * nothing published.
136
+ */
137
+ transact(run, peer) {
138
+ this.#refuseWhenClosed();
139
+ if (this.#transaction !== null) {
140
+ throw new StoreError('action-rejected', 'a store transaction is already open on this store; handlers are synchronous and must not nest');
141
+ }
142
+ const transaction = { staged: this.#state, active: true };
143
+ this.#transaction = transaction;
144
+ try {
145
+ const result = run(this.#context(transaction, peer));
146
+ if (isThenable(result)) {
147
+ throw new StoreError('action-rejected', 'a store handler returned a thenable; handlers are synchronous so that a throw can discard the whole transaction');
148
+ }
149
+ this.#commit(transaction.staged);
150
+ return result;
151
+ }
152
+ finally {
153
+ // Invalidate before clearing, so a context captured by the
154
+ // handler is dead even if the handler stashed it somewhere.
155
+ transaction.active = false;
156
+ this.#transaction = null;
157
+ }
158
+ }
159
+ /**
160
+ * The owner's public write. Inside a handler it joins that handler's
161
+ * transaction; outside one it is its own single-revision transaction.
162
+ */
163
+ applyOwnerUpdate(update) {
164
+ this.#refuseWhenClosed();
165
+ const open = this.#transaction;
166
+ if (open !== null && open.active) {
167
+ open.staged = this.#stage(open.staged, update);
168
+ return;
169
+ }
170
+ const staged = this.#stage(this.#state, update);
171
+ this.#commit(staged);
172
+ }
173
+ /**
174
+ * Apply a full snapshot: validate, then reconcile.
175
+ *
176
+ * An equivalent snapshot is a no-op — same root reference, no
177
+ * notifications — which is what stops a periodic resynchronization
178
+ * from looking like a world change to every selector.
179
+ *
180
+ * Inside a handler it JOINS the open transaction, exactly as
181
+ * `applyOwnerUpdate` does: the owner's public `setState` reaches
182
+ * here through `commit`, and committing beside the transaction
183
+ * published a handler's world while `transact` then reported
184
+ * `action-rejected` for it — or, on the committing path, silently
185
+ * reverted the nested write after subscribers had seen it.
186
+ */
187
+ applySnapshot(raw) {
188
+ this.#refuseWhenClosed();
189
+ const open = this.#transaction;
190
+ if (open !== null && open.active) {
191
+ open.staged = this.#validate(raw);
192
+ return;
193
+ }
194
+ this.#commit(this.#validate(raw));
195
+ }
196
+ /**
197
+ * Write entities of one declared collection: each changed entity
198
+ * validated by its PER-ENTITY parser (`definition.entities`), and the
199
+ * document committed WITHOUT the whole-document validator.
200
+ *
201
+ * `changes` maps an id to its new value, or to `undefined` to remove
202
+ * it. The untouched entities keep their identity, so reconciliation
203
+ * and every downstream diff short-circuit on them. What is not free:
204
+ * rebuilding the collection object, O(entities) shallow and with no
205
+ * parsing.
206
+ *
207
+ * Inside a handler it JOINS the open transaction, like
208
+ * {@link applySnapshot}. The whole-document validator is skipped by
209
+ * contract: declaring the collection in `entities` says `state`
210
+ * imposes nothing on it beyond each entity passing its parser.
211
+ */
212
+ applyEntities(collection, changes) {
213
+ this.#refuseWhenClosed();
214
+ const parse = this.#definition.entities?.[collection];
215
+ if (parse === undefined || !Object.prototype.hasOwnProperty.call(this.#definition.entities, collection)) {
216
+ throw new StoreError('invalid-data', `store '${this.#definition.id}' declares no per-entity parser for '${collection}'; declare it in \`entities\` or write through setState`);
217
+ }
218
+ const open = this.#transaction;
219
+ const from = open !== null && open.active ? open.staged : this.#state;
220
+ const current = from[collection];
221
+ if (current !== undefined && !isEntityMap(current)) {
222
+ throw new StoreError('invalid-data', `store '${this.#definition.id}': '${collection}' is not an entity map in the current state`);
223
+ }
224
+ // Every change parsed BEFORE anything is built: a refusal on the last
225
+ // id publishes nothing.
226
+ const parsed = [];
227
+ for (const id of Object.keys(changes)) {
228
+ if (id.length === 0) {
229
+ throw new StoreError('invalid-data', `store '${this.#definition.id}': an entity id in '${collection}' is empty`);
230
+ }
231
+ const raw = changes[id];
232
+ if (raw === undefined) {
233
+ parsed.push([id, undefined]);
234
+ continue;
235
+ }
236
+ try {
237
+ parsed.push([id, this.runReadOnly(() => parse(raw))]);
238
+ }
239
+ catch (error) {
240
+ if (error instanceof StoreError)
241
+ throw error;
242
+ throw new StoreError('invalid-data', `entity '${collection}.${id}' failed the '${this.#definition.id}' entity validator: ${String(error)}`, { cause: error });
243
+ }
244
+ }
245
+ if (open !== null && open.active) {
246
+ // A staged document need not be reconciled yet (a `setState` inside
247
+ // the handler stages the validator's raw output), so the transaction
248
+ // gets the general merge; its commit validates the document anyway.
249
+ const draft = { ...(current ?? {}) };
250
+ for (const [id, entity] of parsed) {
251
+ if (entity === undefined)
252
+ delete draft[id];
253
+ else
254
+ defineData(draft, id, entity);
255
+ }
256
+ open.staged = mergeShallow(from, { [collection]: draft });
257
+ return;
258
+ }
259
+ // Outside a transaction `from` is the committed state: reconciled and
260
+ // frozen all the way down. So only the CHANGED entities are reconciled,
261
+ // the collection is rebuilt by a spread (which defines own properties,
262
+ // `__proto__` included, and parses nothing), and the result is
263
+ // installed without a second whole-tree reconcile — `reconcile` over the
264
+ // collection is exactly the O(world) walk this path exists to skip.
265
+ let changed = false;
266
+ const reconciled = [];
267
+ for (const [id, entity] of parsed) {
268
+ const present = current !== undefined && Object.prototype.hasOwnProperty.call(current, id);
269
+ const before = present ? current[id] : undefined;
270
+ if (entity === undefined) {
271
+ if (present)
272
+ changed = true;
273
+ reconciled.push([id, undefined]);
274
+ continue;
275
+ }
276
+ const next = reconcile(before, entity);
277
+ if (!Object.is(before, next))
278
+ changed = true;
279
+ reconciled.push([id, next]);
280
+ }
281
+ if (!changed)
282
+ return;
283
+ const map = { ...(current ?? {}) };
284
+ for (const [id, entity] of reconciled) {
285
+ if (entity === undefined)
286
+ delete map[id];
287
+ else
288
+ defineData(map, id, entity);
289
+ }
290
+ const root = { ...from };
291
+ defineData(root, collection, Object.freeze(map));
292
+ this.#install(Object.freeze(root));
293
+ }
294
+ /**
295
+ * Apply a replica delta: the owner's patch operations, at this
296
+ * document, validated by this definition.
297
+ *
298
+ * This lives here rather than in the replica because the store owns
299
+ * the document, its validator and its revision. A replica that read
300
+ * the state out, patched it outside and pushed the result back would
301
+ * validate twice per delta and would be free to publish something
302
+ * the definition never saw.
303
+ *
304
+ * Refusals are returned, not thrown: a delta that does not apply is
305
+ * a recoverable divergence (§1.8 resynchronization), not a caller
306
+ * error.
307
+ */
308
+ applyDelta(ops) {
309
+ this.#refuseWhenClosed();
310
+ const outcome = applyPatch(this.#state, ops, raw => this.#validate(raw));
311
+ // `applyPatch` has ALREADY run the validator on the patched
312
+ // document — that is what it was handed one for. Committing
313
+ // through the validating path ran it a second time, and a
314
+ // validator is not required to be idempotent: a parser that
315
+ // derives or normalizes a field applied its transformation twice,
316
+ // so the outcome this returns and the document a subscriber sees
317
+ // were different values. One validation per document.
318
+ if (outcome.ok && outcome.changed)
319
+ this.#publish(outcome.next);
320
+ return outcome;
321
+ }
322
+ /**
323
+ * Run a read-only callback with setters fenced off.
324
+ *
325
+ * The host layer routes `authorize` and `project` through this, so a
326
+ * callback that is documented as read-only cannot quietly mutate.
327
+ */
328
+ runReadOnly(run) {
329
+ this.#readOnlyDepth += 1;
330
+ try {
331
+ return run();
332
+ }
333
+ finally {
334
+ this.#readOnlyDepth -= 1;
335
+ }
336
+ }
337
+ /** Fence the handle: closed, stale, no listeners retained. */
338
+ close() {
339
+ if (this.#closed)
340
+ return;
341
+ this.#closed = true;
342
+ this.setStatus({ phase: 'closed', stale: true });
343
+ this.#subscriptions.clear();
344
+ this.#statusListeners.clear();
345
+ }
346
+ #context(transaction, peer) {
347
+ return {
348
+ peer,
349
+ getState: () => {
350
+ this.#refuseEscapedContext(transaction);
351
+ return transaction.staged;
352
+ },
353
+ setState: (update) => {
354
+ this.#refuseEscapedContext(transaction);
355
+ if (this.#readOnlyDepth > 0) {
356
+ throw new StoreError('action-rejected', 'a read-only store callback may not write state; validators, authorize, project and update functions run with setters fenced');
357
+ }
358
+ transaction.staged = this.#stage(transaction.staged, update);
359
+ },
360
+ };
361
+ }
362
+ #stage(from, update) {
363
+ const patch = typeof update === 'function'
364
+ ? this.runReadOnly(() => update(from))
365
+ : update;
366
+ return mergeShallow(from, patch);
367
+ }
368
+ #commit(candidate) {
369
+ this.#publish(this.#validate(candidate));
370
+ }
371
+ /**
372
+ * Reconcile and publish a document that has ALREADY been validated.
373
+ *
374
+ * Split from {@link #commit} so a caller that validated can say so
375
+ * instead of validating again. Everything below this line is
376
+ * reconciliation, revision and notification — never validation.
377
+ */
378
+ #publish(validated) {
379
+ const next = reconcile(this.#state, validated);
380
+ if (Object.is(next, this.#state))
381
+ return;
382
+ this.#install(next);
383
+ }
384
+ /**
385
+ * Install a document that is ALREADY reconciled against the current one
386
+ * and frozen, and tell the subscribers. Only {@link applyEntities}
387
+ * builds such a document itself; everything else comes through
388
+ * {@link #publish}.
389
+ */
390
+ #install(next) {
391
+ const previous = this.#state;
392
+ this.#state = next;
393
+ this.#revision += 1;
394
+ // The snapshot keeps a listener added DURING this pass out of it,
395
+ // and re-checking membership keeps a listener CANCELLED during it
396
+ // out too. Both are the same question asked at the right moment: a
397
+ // `cancel()` that has already returned must not be followed by a
398
+ // call, or a listener that has torn down its own scene graph gets
399
+ // handed one more frame of state it no longer expects.
400
+ for (const subscription of [...this.#subscriptions]) {
401
+ if (!this.#subscriptions.has(subscription))
402
+ continue;
403
+ subscription.notify(next, previous);
404
+ }
405
+ }
406
+ #validate(raw) {
407
+ try {
408
+ return this.runReadOnly(() => this.#definition.state(raw));
409
+ }
410
+ catch (error) {
411
+ if (error instanceof StoreError)
412
+ throw error;
413
+ throw new StoreError('invalid-data', `state failed the '${this.#definition.id}' validator: ${String(error)}`, { cause: error });
414
+ }
415
+ }
416
+ #rootSubscription(listener) {
417
+ return {
418
+ notify: (next, previous) => {
419
+ dispatch(() => listener(next, previous));
420
+ },
421
+ };
422
+ }
423
+ #selectorSubscription(selector, listener, options) {
424
+ const equality = options?.equality ?? Object.is;
425
+ let last = this.runReadOnly(() => selector(this.#state));
426
+ if (options?.fireImmediately === true) {
427
+ dispatch(() => listener(last, last));
428
+ }
429
+ return {
430
+ notify: next => {
431
+ const selected = this.runReadOnly(() => selector(next));
432
+ const previous = last;
433
+ // An unchanged selection is not an event. This is what makes a
434
+ // 60 Hz render loop cheap, and it is asserted, not assumed.
435
+ if (equality(selected, previous))
436
+ return;
437
+ last = selected;
438
+ dispatch(() => listener(selected, previous));
439
+ },
440
+ };
441
+ }
442
+ #refuseEscapedContext(transaction) {
443
+ if (transaction.active)
444
+ return;
445
+ throw new StoreError('action-rejected', 'this store transaction has already ended; its context cannot read or write from a later turn');
446
+ }
447
+ #refuseWhenClosed() {
448
+ if (!this.#closed)
449
+ return;
450
+ throw new StoreError('closed', 'this store handle is closed');
451
+ }
452
+ }
453
+ function isThenable(value) {
454
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
455
+ return false;
456
+ }
457
+ return typeof value.then === 'function';
458
+ }
459
+ /**
460
+ * Run one listener in isolation.
461
+ *
462
+ * A listener's bug is not the store's: report it where a page can see
463
+ * it and keep the remaining listeners — and the cleanup that follows
464
+ * them — running. Same convention as the event hub in `events.ts`.
465
+ */
466
+ function dispatch(deliver) {
467
+ try {
468
+ deliver();
469
+ }
470
+ catch (error) {
471
+ console.error('[@net-mesh/browser] store listener threw', error);
472
+ }
473
+ }
474
+ /** Store a caller's key as data: an id of `__proto__` is not a prototype. */
475
+ function defineData(target, key, value) {
476
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
477
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `defineStore` — the reusable typed description.
3
+ *
4
+ * It holds validators, never authority handlers, so a client bundle
5
+ * that joins a store does not carry the owner's gameplay code.
6
+ *
7
+ * It is not a pass-through. It checks the two properties whose
8
+ * violation is invisible until much later:
9
+ *
10
+ * 1. `empty()` must itself be a valid state. `empty()` is what a
11
+ * replica installs when it loses visibility, so an `empty()` the
12
+ * schema would reject means audience clearing fails at the worst
13
+ * possible moment — mid-transition, with the old projection already
14
+ * fenced.
15
+ * 2. `id` and `version` must be usable as an incarnation identity.
16
+ * They travel in every envelope and a blank id or a non-integer
17
+ * version turns a version-mismatch refusal into a confusing parse
18
+ * error at the far end.
19
+ */
20
+ import type { ActionSpec, InputSpec, StoreDefinition } from './types.js';
21
+ export declare function defineStore<S extends object, A extends ActionSpec, I extends InputSpec>(definition: StoreDefinition<S, A, I>): StoreDefinition<S, A, I>;
22
+ /** A plain object: what an entity map is. */
23
+ export declare function isEntityMap(value: unknown): value is Readonly<Record<string, unknown>>;