@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,70 @@
1
+ /**
2
+ * Patch semantics, frozen (brief §1.13).
3
+ *
4
+ * A delta is a list of `{"o":"r","p":[…],"val":…}` (replace) and
5
+ * `{"o":"x","p":[…]}` (remove) operations over **own property-name
6
+ * segments**. There is no JSON Pointer escaping, no arbitrary
7
+ * operation, and nothing executable: the op set is two entries because
8
+ * a patch language is an interpreter, and an interpreter on untrusted
9
+ * input is a much larger promise than this protocol needs to make.
10
+ *
11
+ * Three properties carry the weight, and each is a witness rather than
12
+ * a comment:
13
+ *
14
+ * - **Atomicity.** The whole patch is validated, applied to a draft,
15
+ * and the *final* state validated by the definition's `state()`
16
+ * before anything is committed. A half-applied patch is never
17
+ * published, and a refused patch leaves the previous revision
18
+ * byte-for-byte intact — the caller then resynchronizes (§1.8),
19
+ * because a replica whose view disagrees with the owner's must not
20
+ * guess.
21
+ * - **Prototype safety.** `__proto__`, `constructor` and `prototype`
22
+ * are refused as segments, traversal is own-property only, and every
23
+ * node the patch builds has a **null prototype**. Refusing the names
24
+ * alone would not be enough — a draft built from `{}` inherits a
25
+ * `constructor` that `in` would find — so the two rules are both
26
+ * present and both witnessed.
27
+ * - **Identity.** The committed state goes through
28
+ * {@link reconcile}, so untouched subtrees keep their references and
29
+ * a Three.js loop can diff by reference. An equivalent patch is a
30
+ * no-op, not a whole-world change.
31
+ */
32
+ import type { StoreErrorCode } from './errors.js';
33
+ import type { Parse } from './types.js';
34
+ import { type WireOp } from './wire.js';
35
+ /** Segments that would reach the prototype chain. */
36
+ export declare const FORBIDDEN_SEGMENTS: readonly string[];
37
+ /** Why a patch was refused. Stable and countable. */
38
+ export type PatchRefusal = 'ops-too-many' | 'path-too-deep' | 'segment-too-long' | 'segment-forbidden' | 'root-remove' | 'missing-parent' | 'parent-not-object' | 'missing-key' | 'invalid-state';
39
+ export interface PatchRejected {
40
+ readonly ok: false;
41
+ readonly code: StoreErrorCode;
42
+ readonly reason: PatchRefusal;
43
+ /**
44
+ * Whether the replica should resynchronize. True for the divergence
45
+ * refusals (`missing-parent`, `missing-key`, `parent-not-object`,
46
+ * `invalid-state`): the views disagree and only the owner can settle
47
+ * it. False for the bound refusals, which are the sender's fault.
48
+ */
49
+ readonly resync: boolean;
50
+ }
51
+ export interface PatchApplied<S> {
52
+ readonly ok: true;
53
+ readonly next: S;
54
+ /**
55
+ * False when the patch moved nothing — `next === current`. An
56
+ * unchanged update must not create a revision or a notification
57
+ * (design §4), and that decision is made here, once.
58
+ */
59
+ readonly changed: boolean;
60
+ }
61
+ export type PatchOutcome<S> = PatchApplied<S> | PatchRejected;
62
+ /**
63
+ * Apply a delta's operations atomically.
64
+ *
65
+ * `current` is never mutated, whatever the outcome: on refusal the
66
+ * caller still holds exactly the state it had, which is what makes
67
+ * "a failed patch leaves the previous revision intact" a property of
68
+ * this function rather than a rule its callers have to remember.
69
+ */
70
+ export declare function applyPatch<S extends object>(current: S, ops: readonly WireOp[], validate: Parse<S>): PatchOutcome<S>;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Patch semantics, frozen (brief §1.13).
3
+ *
4
+ * A delta is a list of `{"o":"r","p":[…],"val":…}` (replace) and
5
+ * `{"o":"x","p":[…]}` (remove) operations over **own property-name
6
+ * segments**. There is no JSON Pointer escaping, no arbitrary
7
+ * operation, and nothing executable: the op set is two entries because
8
+ * a patch language is an interpreter, and an interpreter on untrusted
9
+ * input is a much larger promise than this protocol needs to make.
10
+ *
11
+ * Three properties carry the weight, and each is a witness rather than
12
+ * a comment:
13
+ *
14
+ * - **Atomicity.** The whole patch is validated, applied to a draft,
15
+ * and the *final* state validated by the definition's `state()`
16
+ * before anything is committed. A half-applied patch is never
17
+ * published, and a refused patch leaves the previous revision
18
+ * byte-for-byte intact — the caller then resynchronizes (§1.8),
19
+ * because a replica whose view disagrees with the owner's must not
20
+ * guess.
21
+ * - **Prototype safety.** `__proto__`, `constructor` and `prototype`
22
+ * are refused as segments, traversal is own-property only, and every
23
+ * node the patch builds has a **null prototype**. Refusing the names
24
+ * alone would not be enough — a draft built from `{}` inherits a
25
+ * `constructor` that `in` would find — so the two rules are both
26
+ * present and both witnessed.
27
+ * - **Identity.** The committed state goes through
28
+ * {@link reconcile}, so untouched subtrees keep their references and
29
+ * a Three.js loop can diff by reference. An equivalent patch is a
30
+ * no-op, not a whole-world change.
31
+ */
32
+ import { reconcile } from './state.js';
33
+ import { MAX_PATCH_OPS, MAX_PATH_SEGMENTS, MAX_PATH_SEGMENT_BYTES, utf8Length } from './wire.js';
34
+ /** Segments that would reach the prototype chain. */
35
+ export const FORBIDDEN_SEGMENTS = ['__proto__', 'constructor', 'prototype'];
36
+ const FORBIDDEN = new Set(FORBIDDEN_SEGMENTS);
37
+ function reject(reason, resync, code = 'invalid-data') {
38
+ return { ok: false, code, reason, resync };
39
+ }
40
+ /**
41
+ * Apply a delta's operations atomically.
42
+ *
43
+ * `current` is never mutated, whatever the outcome: on refusal the
44
+ * caller still holds exactly the state it had, which is what makes
45
+ * "a failed patch leaves the previous revision intact" a property of
46
+ * this function rather than a rule its callers have to remember.
47
+ */
48
+ export function applyPatch(current, ops, validate) {
49
+ // Bounds first, and over the whole list: nothing is applied until
50
+ // every operation is admissible, so a patch cannot be half-refused.
51
+ if (ops.length > MAX_PATCH_OPS)
52
+ return reject('ops-too-many', false, 'capacity');
53
+ for (const op of ops) {
54
+ if (op.p.length > MAX_PATH_SEGMENTS)
55
+ return reject('path-too-deep', false, 'capacity');
56
+ for (const segment of op.p) {
57
+ if (utf8Length(segment) > MAX_PATH_SEGMENT_BYTES) {
58
+ return reject('segment-too-long', false, 'capacity');
59
+ }
60
+ // Prototype-named segments are refused outright. Traversal below
61
+ // is own-property only as well; either alone would be a hole.
62
+ if (FORBIDDEN.has(segment))
63
+ return reject('segment-forbidden', false);
64
+ }
65
+ // A state must exist: replacing the root is permitted, removing it
66
+ // is not.
67
+ if (op.o === 'x' && op.p.length === 0)
68
+ return reject('root-remove', false);
69
+ }
70
+ // Apply to a draft. Nodes are copied along the touched path only, so
71
+ // an untouched subtree is the *same object* in the draft and is then
72
+ // recognised by `reconcile` as unchanged.
73
+ let draft = current;
74
+ const copied = new WeakSet();
75
+ for (const op of ops) {
76
+ if (op.p.length === 0) {
77
+ // Root replace; root remove was refused above.
78
+ if (op.o === 'r')
79
+ draft = op.val;
80
+ continue;
81
+ }
82
+ if (!isTraversable(draft))
83
+ return reject('parent-not-object', true);
84
+ const root = copyOnce(draft, copied);
85
+ draft = root;
86
+ const parent = descend(root, op.p.slice(0, -1), copied);
87
+ if (parent === MISSING)
88
+ return reject('missing-parent', true);
89
+ if (parent === NOT_OBJECT)
90
+ return reject('parent-not-object', true);
91
+ const container = parent;
92
+ const leaf = op.p[op.p.length - 1];
93
+ if (op.o === 'x') {
94
+ // A remove whose target is absent means the views have already
95
+ // diverged: refuse the whole patch and resynchronize.
96
+ if (!Object.prototype.hasOwnProperty.call(container, leaf)) {
97
+ return reject('missing-key', true);
98
+ }
99
+ delete container[leaf];
100
+ continue;
101
+ }
102
+ container[leaf] = op.val;
103
+ }
104
+ // The definition validates the **final** state, not each step: an
105
+ // intermediate shape may legitimately be invalid, and a patch is one
106
+ // transition.
107
+ let validated;
108
+ try {
109
+ validated = validate(draft);
110
+ }
111
+ catch {
112
+ return reject('invalid-state', true);
113
+ }
114
+ const next = reconcile(current, validated);
115
+ return { ok: true, next, changed: !Object.is(next, current) };
116
+ }
117
+ const MISSING = Symbol('missing-parent');
118
+ const NOT_OBJECT = Symbol('parent-not-object');
119
+ /**
120
+ * Walk to the parent of the target, copying each node on the way.
121
+ *
122
+ * `root` is already a draft copy. Each child is copied once and written
123
+ * back into its (copied) holder, so a second operation through the same
124
+ * parent mutates the draft's node instead of copying it again — which
125
+ * is what makes overlapping operations apply in array order with the
126
+ * later one winning.
127
+ */
128
+ function descend(root, path, copied) {
129
+ let node = root;
130
+ for (const segment of path) {
131
+ const holder = node;
132
+ // Own-property only: the prototype chain is never followed, so a
133
+ // path segment can never resolve to an inherited member.
134
+ if (!Object.prototype.hasOwnProperty.call(holder, segment))
135
+ return MISSING;
136
+ const child = holder[segment];
137
+ if (!isTraversable(child))
138
+ return NOT_OBJECT;
139
+ const fresh = copyOnce(child, copied);
140
+ holder[segment] = fresh;
141
+ node = fresh;
142
+ }
143
+ return node;
144
+ }
145
+ /**
146
+ * Arrays are replaced whole in v1, so they are not traversable: a path
147
+ * that descends into one is a refusal, not an index.
148
+ */
149
+ function isTraversable(value) {
150
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
151
+ }
152
+ /**
153
+ * Copy a node into the draft once.
154
+ *
155
+ * The clone has a **null prototype**: refusing `__proto__`,
156
+ * `constructor` and `prototype` as segments stops the names, and this
157
+ * stops the inheritance, so no patched node can reach `Object.prototype`
158
+ * at all.
159
+ */
160
+ function copyOnce(node, copied) {
161
+ if (copied.has(node))
162
+ return node;
163
+ const clone = Object.assign(Object.create(null), node);
164
+ copied.add(clone);
165
+ return clone;
166
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `hostPlayer` — the hosting node's own player, as a replica-shaped
3
+ * handle.
4
+ *
5
+ * A node cannot join its own store: `openStream({peer})` needs a
6
+ * session with that peer and a node has none with itself, so
7
+ * `joinStore({ host: self })` is refused `invalid-data`. Every game
8
+ * whose host is also a player therefore wrote the same wrapper — the
9
+ * fleet demo's was forty lines — and each one had to remember to hold
10
+ * the host's player to the same `authorize` a replica gets.
11
+ *
12
+ * This is that wrapper, made part of the store, with parity as its
13
+ * contract rather than a habit:
14
+ *
15
+ * - **The same policy.** Reads, actions and inputs go to the host's
16
+ * `authorize` with the host's own node id as `peer`, in the same
17
+ * 16-hex spelling a replica's frames are handed.
18
+ * - **The same transaction.** Input parse, handler, output validation
19
+ * and the result's message budget run in one transaction on the
20
+ * owner, exactly as for a replica's `act` after the wire
21
+ * (`StoreOwner.localAct`), and the change reaches every replica.
22
+ * - **The same values.** Inputs are refused and normalized by the
23
+ * wire's own rules (`encodeMessage`, then `parseStoreJson`), so a
24
+ * value that works for the host works for its players.
25
+ * - **The same view.** `getState()` is the projection for this
26
+ * handle's audience, not the authoritative document: game code that
27
+ * renders from a replica renders the same thing on the host.
28
+ *
29
+ * What is NOT the same, deliberately: there is no wire, so no handle,
30
+ * lease, sequence or replay ledger, and no `reconnect` to perform.
31
+ * Calls still settle asynchronously, as a replica's do, so a handler
32
+ * never runs nested inside the caller's own code.
33
+ *
34
+ * **The trust boundary is unchanged.** The hosting player's page holds
35
+ * the authoritative document; the projection here keeps the host's
36
+ * own UI honest, it does not hide anything from a player who opens the
37
+ * developer tools. Games with real stakes need a dedicated host.
38
+ */
39
+ import { type HostedStoreHandle } from './host.js';
40
+ import type { JoinedStoreHandle } from './join.js';
41
+ import type { ActionSpec, InputSpec } from './types.js';
42
+ /** What the host's own player reads as. */
43
+ export interface HostPlayerOptions {
44
+ /**
45
+ * The audience this player reads, as a replica's `joinStore({
46
+ * audience })`. Authorized by the host's policy like any other read.
47
+ */
48
+ readonly audience: readonly string[];
49
+ /** The interest set, as `joinStore({ interest })`. Omit it to see every entity. */
50
+ readonly interest?: readonly string[];
51
+ }
52
+ /**
53
+ * The hosting node's own player on `host`.
54
+ *
55
+ * ```ts
56
+ * const host = hostStore({ definition: world, transport: node, … });
57
+ * const me = hostPlayer(host, { audience: ['crew'] });
58
+ * await me.ready();
59
+ * await me.act('enlist', { colour: 2 }); // same authorize, same handler
60
+ * ```
61
+ *
62
+ * Game code can then treat `me` and a `joinStore` replica the same way.
63
+ * Throws `invalid-data` for a handle `hostStore` did not return.
64
+ */
65
+ export declare function hostPlayer<S extends object, A extends ActionSpec, I extends InputSpec>(host: HostedStoreHandle<S, A, I>, options: HostPlayerOptions): JoinedStoreHandle<S, A, I>;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * `hostPlayer` — the hosting node's own player, as a replica-shaped
3
+ * handle.
4
+ *
5
+ * A node cannot join its own store: `openStream({peer})` needs a
6
+ * session with that peer and a node has none with itself, so
7
+ * `joinStore({ host: self })` is refused `invalid-data`. Every game
8
+ * whose host is also a player therefore wrote the same wrapper — the
9
+ * fleet demo's was forty lines — and each one had to remember to hold
10
+ * the host's player to the same `authorize` a replica gets.
11
+ *
12
+ * This is that wrapper, made part of the store, with parity as its
13
+ * contract rather than a habit:
14
+ *
15
+ * - **The same policy.** Reads, actions and inputs go to the host's
16
+ * `authorize` with the host's own node id as `peer`, in the same
17
+ * 16-hex spelling a replica's frames are handed.
18
+ * - **The same transaction.** Input parse, handler, output validation
19
+ * and the result's message budget run in one transaction on the
20
+ * owner, exactly as for a replica's `act` after the wire
21
+ * (`StoreOwner.localAct`), and the change reaches every replica.
22
+ * - **The same values.** Inputs are refused and normalized by the
23
+ * wire's own rules (`encodeMessage`, then `parseStoreJson`), so a
24
+ * value that works for the host works for its players.
25
+ * - **The same view.** `getState()` is the projection for this
26
+ * handle's audience, not the authoritative document: game code that
27
+ * renders from a replica renders the same thing on the host.
28
+ *
29
+ * What is NOT the same, deliberately: there is no wire, so no handle,
30
+ * lease, sequence or replay ledger, and no `reconnect` to perform.
31
+ * Calls still settle asynchronously, as a replica's do, so a handler
32
+ * never runs nested inside the caller's own code.
33
+ *
34
+ * **The trust boundary is unchanged.** The hosting player's page holds
35
+ * the authoritative document; the projection here keeps the host's
36
+ * own UI honest, it does not hide anything from a player who opens the
37
+ * developer tools. Games with real stakes need a dedicated host.
38
+ */
39
+ import { StoreCore } from './core.js';
40
+ import { StoreError } from './errors.js';
41
+ import { hostInternals } from './host.js';
42
+ import { parseStoreJson } from './json.js';
43
+ import { encodeMessage } from './wire.js';
44
+ const PLACEHOLDER_Q = '0'.repeat(16);
45
+ const PLACEHOLDER_H = '0'.repeat(32);
46
+ /**
47
+ * The wire's admission for one caller value: refused exactly as a
48
+ * replica's `encodeMessage` refuses it, and parsed back exactly as the
49
+ * owner's parser hands it to a handler.
50
+ */
51
+ function admit(name, value, kind) {
52
+ let frame;
53
+ try {
54
+ frame =
55
+ kind === 'act'
56
+ ? encodeMessage({ k: 'act', q: PLACEHOLDER_Q, h: PLACEHOLDER_H, s: '1', name, in: value })
57
+ : encodeMessage({ k: 'in', h: PLACEHOLDER_H, s: '1', name, in: value });
58
+ }
59
+ catch (error) {
60
+ throw new StoreError('invalid-data', `the input for '${name}' cannot be encoded`, { cause: error });
61
+ }
62
+ const parsed = parseStoreJson(frame);
63
+ if (!parsed.ok || typeof parsed.value !== 'object' || parsed.value === null || Array.isArray(parsed.value)) {
64
+ throw new StoreError('invalid-data', `the input for '${name}' cannot be encoded`);
65
+ }
66
+ return parsed.value.in;
67
+ }
68
+ /** A value as a replica would receive it: JSON, detached from the host's objects. */
69
+ function detach(value) {
70
+ const parsed = parseStoreJson(JSON.stringify(value));
71
+ return parsed.ok ? parsed.value : value;
72
+ }
73
+ /**
74
+ * The hosting node's own player on `host`.
75
+ *
76
+ * ```ts
77
+ * const host = hostStore({ definition: world, transport: node, … });
78
+ * const me = hostPlayer(host, { audience: ['crew'] });
79
+ * await me.ready();
80
+ * await me.act('enlist', { colour: 2 }); // same authorize, same handler
81
+ * ```
82
+ *
83
+ * Game code can then treat `me` and a `joinStore` replica the same way.
84
+ * Throws `invalid-data` for a handle `hostStore` did not return.
85
+ */
86
+ export function hostPlayer(host, options) {
87
+ const inner = hostInternals(host);
88
+ if (inner === undefined) {
89
+ throw new StoreError('invalid-data', 'hostPlayer needs a handle returned by hostStore');
90
+ }
91
+ const { owner, peer } = inner;
92
+ const definition = host.definition;
93
+ const view = new StoreCore({ definition, initialState: definition.empty(), phase: 'syncing' });
94
+ let audience = [...options.audience];
95
+ let interest = options.interest === undefined ? null : [...options.interest];
96
+ let closed = false;
97
+ /** Terminal: the read was refused, the projection failed, or the host closed. */
98
+ let ended = false;
99
+ const pendingInputs = new Map();
100
+ function end(phase, error) {
101
+ ended = true;
102
+ if (error?.code === 'forbidden')
103
+ queueMicrotask(() => depart('refused'));
104
+ // As a replica does on a terminal refusal: the view is cleared, so
105
+ // a revoked read stops showing what it no longer may.
106
+ view.applySnapshot(definition.empty());
107
+ view.setStatus({ phase, stale: false, error });
108
+ }
109
+ /** Re-read the view — authorized afresh, as a replica's feed is. */
110
+ function refresh() {
111
+ if (closed || ended)
112
+ return;
113
+ if (inner.isClosed()) {
114
+ end('closed', null);
115
+ return;
116
+ }
117
+ if (!owner.localRead(peer, audience)) {
118
+ end('failed', new StoreError('forbidden', 'the store refused: forbidden'));
119
+ return;
120
+ }
121
+ const projected = owner.localView(peer, audience, interest);
122
+ if (projected === null) {
123
+ end('failed', new StoreError('capacity', 'the store refused: capacity'));
124
+ return;
125
+ }
126
+ view.applySnapshot(projected);
127
+ view.setStatus({ phase: 'ready', stale: false, error: null });
128
+ }
129
+ const stopWatching = owner.subscribe(() => {
130
+ refresh();
131
+ });
132
+ inner.onClose(() => {
133
+ stopWatching();
134
+ if (!closed && !ended)
135
+ end('closed', null);
136
+ });
137
+ refresh();
138
+ // A player, as far as `onEvent` is concerned: `join` now if the read
139
+ // was granted, `leave` when this handle ends.
140
+ let joined = false;
141
+ if (view.getStatus().phase === 'ready') {
142
+ joined = true;
143
+ owner.localPresence(peer, true, audience);
144
+ inner.dispatched({ out: [], refused: null, deferred: null });
145
+ }
146
+ function depart(reason) {
147
+ if (!joined || inner.isClosed())
148
+ return;
149
+ joined = false;
150
+ owner.localPresence(peer, false, audience, reason);
151
+ inner.dispatched({ out: [], refused: null, deferred: null });
152
+ }
153
+ function refusal() {
154
+ if (closed)
155
+ return new StoreError('closed', 'this store handle is closed');
156
+ if (inner.isClosed())
157
+ return new StoreError('owner-lost', 'the store owner closed this handle');
158
+ if (ended)
159
+ return view.getStatus().error ?? new StoreError('owner-lost', 'the store owner closed this handle');
160
+ return null;
161
+ }
162
+ function flushInputs() {
163
+ const batch = [...pendingInputs];
164
+ pendingInputs.clear();
165
+ if (refusal() !== null)
166
+ return;
167
+ for (const [name, value] of batch)
168
+ inner.dispatched(owner.localInput(name, value, peer));
169
+ }
170
+ function subscribe(first, second, selectorOptions) {
171
+ return second === undefined
172
+ ? view.subscribe(first)
173
+ : view.subscribe(first, second, selectorOptions);
174
+ }
175
+ return {
176
+ getState: () => view.getState(),
177
+ subscribe,
178
+ getStatus: () => view.getStatus(),
179
+ subscribeStatus: listener => view.subscribeStatus(listener),
180
+ ready: () => {
181
+ const status = view.getStatus();
182
+ if (status.phase === 'ready')
183
+ return Promise.resolve();
184
+ return Promise.reject(status.error ?? new StoreError(closed ? 'closed' : 'owner-lost', 'this store handle is not ready'));
185
+ },
186
+ act: async (name, input) => {
187
+ const early = refusal();
188
+ if (early !== null)
189
+ throw early;
190
+ const value = admit(name, input, 'act');
191
+ // A turn later, as a replica's answer is: a handler never runs
192
+ // inside the caller's own frame — including inside another
193
+ // handler, where it would nest a transaction.
194
+ await Promise.resolve();
195
+ const late = refusal();
196
+ if (late !== null)
197
+ throw late;
198
+ const { outcome, dispatched } = owner.localAct(name, value, peer);
199
+ inner.dispatched(dispatched);
200
+ if (outcome.kind === 'refusal') {
201
+ throw new StoreError(outcome.code, `the store refused: ${outcome.code}`);
202
+ }
203
+ return detach(outcome.out);
204
+ },
205
+ input: (name, value) => {
206
+ if (refusal() !== null || view.getStatus().phase !== 'ready') {
207
+ return { type: 'dropped', reason: 'not-ready' };
208
+ }
209
+ const admitted = admit(name, value, 'in');
210
+ // Newest wins, as a replica's inputs do: one pending value per
211
+ // name, applied a turn later.
212
+ const idle = pendingInputs.size === 0;
213
+ const replaced = pendingInputs.has(name);
214
+ pendingInputs.set(name, admitted);
215
+ if (idle)
216
+ queueMicrotask(flushInputs);
217
+ return { type: replaced ? 'replaced' : 'queued' };
218
+ },
219
+ setAudience: async (names) => {
220
+ const early = refusal();
221
+ if (early !== null)
222
+ throw early;
223
+ try {
224
+ encodeMessage({ k: 'aud', q: PLACEHOLDER_Q, h: PLACEHOLDER_H, aud: [...names] });
225
+ }
226
+ catch (error) {
227
+ throw new StoreError('invalid-data', 'the audience cannot be encoded', { cause: error });
228
+ }
229
+ await Promise.resolve();
230
+ const late = refusal();
231
+ if (late !== null)
232
+ throw late;
233
+ if (!owner.localRead(peer, names)) {
234
+ throw new StoreError('forbidden', 'the store refused: forbidden');
235
+ }
236
+ audience = [...names];
237
+ refresh();
238
+ },
239
+ setInterest: async (keys) => {
240
+ const early = refusal();
241
+ if (early !== null)
242
+ throw early;
243
+ const next = [...keys];
244
+ await Promise.resolve();
245
+ interest = next;
246
+ refresh();
247
+ },
248
+ // Nothing to resume: there is no session under this handle.
249
+ reconnect: () => Promise.resolve(),
250
+ close: () => {
251
+ if (!closed) {
252
+ closed = true;
253
+ depart('left');
254
+ pendingInputs.clear();
255
+ stopWatching();
256
+ view.close();
257
+ }
258
+ return Promise.resolve();
259
+ },
260
+ };
261
+ }