@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,83 @@
|
|
|
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 { StoreError } from './errors.js';
|
|
21
|
+
import { compileVisibility } from './visibility.js';
|
|
22
|
+
export function defineStore(definition) {
|
|
23
|
+
if (definition.id.length === 0) {
|
|
24
|
+
throw new StoreError('invalid-data', 'a store definition needs a non-empty id');
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isSafeInteger(definition.version) || definition.version < 1) {
|
|
27
|
+
throw new StoreError('invalid-data', `store '${definition.id}' needs a positive integer version, got ${String(definition.version)}`);
|
|
28
|
+
}
|
|
29
|
+
// `empty()` is state, so it must pass the state validator. Checking
|
|
30
|
+
// it here fails at definition time — at the developer's desk —
|
|
31
|
+
// instead of during an audience transition in front of a player.
|
|
32
|
+
let empty;
|
|
33
|
+
try {
|
|
34
|
+
empty = definition.empty();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new StoreError('invalid-data', `store '${definition.id}' could not build its empty state: ${String(error)}`, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
definition.state(empty);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new StoreError('invalid-data', `store '${definition.id}': empty() is not a valid state, so a replica could not clear a projection with it: ${String(error)}`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
// Visibility rules that could not be enforced as written fail here,
|
|
46
|
+
// at the developer's desk, rather than at the first projection.
|
|
47
|
+
if (definition.visibility !== undefined) {
|
|
48
|
+
try {
|
|
49
|
+
compileVisibility(definition.visibility);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new StoreError('invalid-data', `store '${definition.id}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
53
|
+
cause: error,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (definition.interest !== undefined) {
|
|
58
|
+
for (const [collection, key] of Object.entries(definition.interest)) {
|
|
59
|
+
if (collection.length === 0 || collection.includes('.') || typeof key !== 'function') {
|
|
60
|
+
throw new StoreError('invalid-data', `store '${definition.id}': interest maps a top-level collection name to a key function, got '${collection}'`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (definition.entities !== undefined) {
|
|
65
|
+
for (const [collection, parse] of Object.entries(definition.entities)) {
|
|
66
|
+
if (collection.length === 0 || collection.includes('.') || typeof parse !== 'function') {
|
|
67
|
+
throw new StoreError('invalid-data', `store '${definition.id}': entities maps a top-level collection name to a per-entity parser, got '${collection}'`);
|
|
68
|
+
}
|
|
69
|
+
const inEmpty = empty[collection];
|
|
70
|
+
if (inEmpty !== undefined && !isEntityMap(inEmpty)) {
|
|
71
|
+
throw new StoreError('invalid-data', `store '${definition.id}': entities names '${collection}', but empty().${collection} is not a record of entities`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({ ...definition });
|
|
76
|
+
}
|
|
77
|
+
/** A plain object: what an entity map is. */
|
|
78
|
+
export function isEntityMap(value) {
|
|
79
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
80
|
+
return false;
|
|
81
|
+
const proto = Object.getPrototypeOf(value);
|
|
82
|
+
return proto === Object.prototype || proto === null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store's error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* One code per refusal a caller can act on differently. The codes are
|
|
5
|
+
* the design document's list
|
|
6
|
+
* (`docs/internal/plans/BROWSER_GAME_STORE_API_DESIGN.md` §2), and the
|
|
7
|
+
* one that carries a contract subtlety is {@link StoreErrorCode}
|
|
8
|
+
* `result-expired` — see its note below, because getting it wrong
|
|
9
|
+
* fabricates a success receipt.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Why a store operation was refused.
|
|
13
|
+
*
|
|
14
|
+
* - `invalid-data` — a payload failed its validator.
|
|
15
|
+
* - `version-mismatch` — definition id/version disagreement.
|
|
16
|
+
* - `forbidden` — the owner's `authorize` refused.
|
|
17
|
+
* - `not-ready` — the handle has no live, synchronized view.
|
|
18
|
+
* - `capacity` — a declared bound was reached.
|
|
19
|
+
* - `timeout` / `aborted` — the caller's deadline or signal fired.
|
|
20
|
+
* - `indeterminate` — submitted, and no response established the outcome.
|
|
21
|
+
* - `owner-lost` — the store incarnation ended. Terminal: there is no
|
|
22
|
+
* handle to obtain, because the owner that held the document is gone.
|
|
23
|
+
* A closing host SAYS this, unsolicited, to every handle it holds
|
|
24
|
+
* (`owner.farewell`) — silence is not an answer, and a replica whose
|
|
25
|
+
* host closed used to learn nothing until its own deadline fired and
|
|
26
|
+
* reported `indeterminate`. It is deliberately NOT `closed`: that
|
|
27
|
+
* one is the expiry notice a replica rejoins on, and rejoining an
|
|
28
|
+
* owner that is gone either hangs or silently attaches the caller to
|
|
29
|
+
* a SUCCESSOR's different document under the handle it already had.
|
|
30
|
+
* - `closed` — **this handle is unusable**: unknown, expired, fenced by
|
|
31
|
+
* the owner, or bound to another peer. Deliberately one code for all
|
|
32
|
+
* four, so a refusal cannot disclose whether a handle exists. It is
|
|
33
|
+
* terminal for the handle and for any action in flight on it —
|
|
34
|
+
* nothing is replayed and no outcome is inferred, exactly as for
|
|
35
|
+
* `result-expired` below — while the *subscription* is recoverable by
|
|
36
|
+
* joining afresh, which yields a new handle rather than resuming the
|
|
37
|
+
* old one.
|
|
38
|
+
* - `action-rejected` — the owner refused this action, or a handler broke
|
|
39
|
+
* the synchronous-transaction contract.
|
|
40
|
+
* - `result-expired` — **this request cannot execute again, and its
|
|
41
|
+
* original result is unavailable.** It asserts nothing about whether the
|
|
42
|
+
* original attempt committed: a retired sequence may have been rejected,
|
|
43
|
+
* aborted before commit, or fenced without ever executing. A
|
|
44
|
+
* non-reexecution floor is not evidence of successful execution, so this
|
|
45
|
+
* code must never be read — or reported — as a success receipt.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* The codes, as a runtime set.
|
|
49
|
+
*
|
|
50
|
+
* `no.code` on the wire is **exactly** this list (brief §2), so the
|
|
51
|
+
* parser needs it at runtime and not only in the type system: a code
|
|
52
|
+
* the caller cannot branch on is a refusal it cannot act on.
|
|
53
|
+
*
|
|
54
|
+
* {@link StoreErrorCode} is derived from it, so the two cannot drift —
|
|
55
|
+
* a code added to one and not the other would be a type the parser
|
|
56
|
+
* refuses or a wire value the caller cannot name.
|
|
57
|
+
*/
|
|
58
|
+
export declare const STORE_ERROR_CODES: readonly ["invalid-data", "version-mismatch", "forbidden", "not-ready", "capacity", "timeout", "aborted", "indeterminate", "owner-lost", "closed", "action-rejected", "result-expired"];
|
|
59
|
+
export type StoreErrorCode = (typeof STORE_ERROR_CODES)[number];
|
|
60
|
+
/** A refused store operation, carrying the code a caller branches on. */
|
|
61
|
+
export declare class StoreError extends Error {
|
|
62
|
+
readonly code: StoreErrorCode;
|
|
63
|
+
constructor(code: StoreErrorCode, message: string, options?: {
|
|
64
|
+
cause?: unknown;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a send failed because its stream belongs to a session that
|
|
69
|
+
* has been replaced — the ONE failure a reopen repairs.
|
|
70
|
+
*
|
|
71
|
+
* The leaf names it (`leaf/src/node.rs`): "stale stream handle:
|
|
72
|
+
* opened on incarnation N; reopen the stream". It is matched on the
|
|
73
|
+
* message because that is what crosses the wasm boundary; everything
|
|
74
|
+
* else — an oversized payload, a fenced id, a closed node — is
|
|
75
|
+
* permanent, and reopening for it burns a stream handle per frame.
|
|
76
|
+
*/
|
|
77
|
+
export declare function isStaleStream(error: unknown): boolean;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store's error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* One code per refusal a caller can act on differently. The codes are
|
|
5
|
+
* the design document's list
|
|
6
|
+
* (`docs/internal/plans/BROWSER_GAME_STORE_API_DESIGN.md` §2), and the
|
|
7
|
+
* one that carries a contract subtlety is {@link StoreErrorCode}
|
|
8
|
+
* `result-expired` — see its note below, because getting it wrong
|
|
9
|
+
* fabricates a success receipt.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Why a store operation was refused.
|
|
13
|
+
*
|
|
14
|
+
* - `invalid-data` — a payload failed its validator.
|
|
15
|
+
* - `version-mismatch` — definition id/version disagreement.
|
|
16
|
+
* - `forbidden` — the owner's `authorize` refused.
|
|
17
|
+
* - `not-ready` — the handle has no live, synchronized view.
|
|
18
|
+
* - `capacity` — a declared bound was reached.
|
|
19
|
+
* - `timeout` / `aborted` — the caller's deadline or signal fired.
|
|
20
|
+
* - `indeterminate` — submitted, and no response established the outcome.
|
|
21
|
+
* - `owner-lost` — the store incarnation ended. Terminal: there is no
|
|
22
|
+
* handle to obtain, because the owner that held the document is gone.
|
|
23
|
+
* A closing host SAYS this, unsolicited, to every handle it holds
|
|
24
|
+
* (`owner.farewell`) — silence is not an answer, and a replica whose
|
|
25
|
+
* host closed used to learn nothing until its own deadline fired and
|
|
26
|
+
* reported `indeterminate`. It is deliberately NOT `closed`: that
|
|
27
|
+
* one is the expiry notice a replica rejoins on, and rejoining an
|
|
28
|
+
* owner that is gone either hangs or silently attaches the caller to
|
|
29
|
+
* a SUCCESSOR's different document under the handle it already had.
|
|
30
|
+
* - `closed` — **this handle is unusable**: unknown, expired, fenced by
|
|
31
|
+
* the owner, or bound to another peer. Deliberately one code for all
|
|
32
|
+
* four, so a refusal cannot disclose whether a handle exists. It is
|
|
33
|
+
* terminal for the handle and for any action in flight on it —
|
|
34
|
+
* nothing is replayed and no outcome is inferred, exactly as for
|
|
35
|
+
* `result-expired` below — while the *subscription* is recoverable by
|
|
36
|
+
* joining afresh, which yields a new handle rather than resuming the
|
|
37
|
+
* old one.
|
|
38
|
+
* - `action-rejected` — the owner refused this action, or a handler broke
|
|
39
|
+
* the synchronous-transaction contract.
|
|
40
|
+
* - `result-expired` — **this request cannot execute again, and its
|
|
41
|
+
* original result is unavailable.** It asserts nothing about whether the
|
|
42
|
+
* original attempt committed: a retired sequence may have been rejected,
|
|
43
|
+
* aborted before commit, or fenced without ever executing. A
|
|
44
|
+
* non-reexecution floor is not evidence of successful execution, so this
|
|
45
|
+
* code must never be read — or reported — as a success receipt.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* The codes, as a runtime set.
|
|
49
|
+
*
|
|
50
|
+
* `no.code` on the wire is **exactly** this list (brief §2), so the
|
|
51
|
+
* parser needs it at runtime and not only in the type system: a code
|
|
52
|
+
* the caller cannot branch on is a refusal it cannot act on.
|
|
53
|
+
*
|
|
54
|
+
* {@link StoreErrorCode} is derived from it, so the two cannot drift —
|
|
55
|
+
* a code added to one and not the other would be a type the parser
|
|
56
|
+
* refuses or a wire value the caller cannot name.
|
|
57
|
+
*/
|
|
58
|
+
export const STORE_ERROR_CODES = [
|
|
59
|
+
'invalid-data',
|
|
60
|
+
'version-mismatch',
|
|
61
|
+
'forbidden',
|
|
62
|
+
'not-ready',
|
|
63
|
+
'capacity',
|
|
64
|
+
'timeout',
|
|
65
|
+
'aborted',
|
|
66
|
+
'indeterminate',
|
|
67
|
+
'owner-lost',
|
|
68
|
+
'closed',
|
|
69
|
+
'action-rejected',
|
|
70
|
+
'result-expired',
|
|
71
|
+
];
|
|
72
|
+
/** A refused store operation, carrying the code a caller branches on. */
|
|
73
|
+
export class StoreError extends Error {
|
|
74
|
+
code;
|
|
75
|
+
constructor(code, message, options) {
|
|
76
|
+
super(message, options);
|
|
77
|
+
this.name = 'StoreError';
|
|
78
|
+
this.code = code;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a send failed because its stream belongs to a session that
|
|
83
|
+
* has been replaced — the ONE failure a reopen repairs.
|
|
84
|
+
*
|
|
85
|
+
* The leaf names it (`leaf/src/node.rs`): "stale stream handle:
|
|
86
|
+
* opened on incarnation N; reopen the stream". It is matched on the
|
|
87
|
+
* message because that is what crosses the wasm boundary; everything
|
|
88
|
+
* else — an oversized payload, a fenced id, a closed node — is
|
|
89
|
+
* permanent, and reopening for it burns a stream handle per frame.
|
|
90
|
+
*/
|
|
91
|
+
export function isStaleStream(error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
return message.includes('stale stream handle');
|
|
94
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice G — `hostStore` and `joinStore` over a real session.
|
|
3
|
+
*
|
|
4
|
+
* This is the layer where the authenticated-origin gate is either
|
|
5
|
+
* satisfied or not, and it is three lines of it: a frame arrives as a
|
|
6
|
+
* `stream_data` event, the event carries `peerNode` — the peer whose
|
|
7
|
+
* installed session opened the packet — and that string is what goes
|
|
8
|
+
* into `owner.receive(frame, peer)`. Nothing in a store frame is
|
|
9
|
+
* consulted for identity, because no store frame has an originator
|
|
10
|
+
* field to consult (§1.3).
|
|
11
|
+
*
|
|
12
|
+
* ## What this module is typed against
|
|
13
|
+
*
|
|
14
|
+
* Not a port, not an adapter: the structural subset of the real
|
|
15
|
+
* `BrowserNode`/`MeshSession` surface that the store actually uses.
|
|
16
|
+
* Both satisfy {@link StoreTransport} as they are, so there is no
|
|
17
|
+
* second implementation of anything, and a test can drive the same
|
|
18
|
+
* type without a mesh.
|
|
19
|
+
*
|
|
20
|
+
* ## The gate this slice does NOT clear
|
|
21
|
+
*
|
|
22
|
+
* §4 gates G on leader-proxy lifecycle: follower tabs, last-consumer
|
|
23
|
+
* cleanup, the peer/stream lifecycle on a proxied handle. A
|
|
24
|
+
* `ProxyStream` now carries its peer (6256f970b) which is the
|
|
25
|
+
* precondition, but the *store's* use of it on a follower is not
|
|
26
|
+
* established, and nothing here claims it is.
|
|
27
|
+
*/
|
|
28
|
+
import { StoreOwner, type Dispatched, type OwnerDeps, type Viewer } from './owner.js';
|
|
29
|
+
import type { ActionSpec, Cancel, EntityCollection, EntityOf, InputSpec, StoreDefinition } from './types.js';
|
|
30
|
+
import { type Hex } from './wire.js';
|
|
31
|
+
/** How often a host expires handles whose lease has run out (§2). */
|
|
32
|
+
export declare const HOST_SWEEP_MS = 5000;
|
|
33
|
+
/**
|
|
34
|
+
* How long `close()` will wait for one goodbye to flush.
|
|
35
|
+
*
|
|
36
|
+
* A page tearing down cannot wait forever to be polite, and a
|
|
37
|
+
* transport whose `send` never settles would otherwise make `close()`
|
|
38
|
+
* never return — a review probe held one and measured exactly that.
|
|
39
|
+
* Short, because the alternative to a bounded wait is a hang, and the
|
|
40
|
+
* frame is not retried afterwards: a farewell that missed its window
|
|
41
|
+
* is a counted loss.
|
|
42
|
+
*
|
|
43
|
+
* HALF A SECOND, not two: the first version chose two and a review
|
|
44
|
+
* probe — which allowed 750 ms for `close()` to return — read that as
|
|
45
|
+
* an unbounded close, and it was right to. A page tearing down has an
|
|
46
|
+
* unload budget under a second, and a frame that has not flushed on a
|
|
47
|
+
* live DataChannel in 500 ms is not going to matter to the replica
|
|
48
|
+
* that was going to receive it.
|
|
49
|
+
*/
|
|
50
|
+
export declare const FAREWELL_DEADLINE_MS = 500;
|
|
51
|
+
/** One frame, as the transport carries it. */
|
|
52
|
+
export type Frame = Uint8Array;
|
|
53
|
+
/** The subset of a stream the store writes to and reads from. */
|
|
54
|
+
export interface TransportStream {
|
|
55
|
+
send(payload: Frame): Promise<void> | void;
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
/** A `stream_data` arrival, narrowed to what the store reads. */
|
|
59
|
+
export interface TransportFrame {
|
|
60
|
+
readonly type: string;
|
|
61
|
+
readonly streamId?: string;
|
|
62
|
+
/** The authenticated peer. The store's only source of identity. */
|
|
63
|
+
readonly peerNode?: string | null;
|
|
64
|
+
readonly payload?: Frame;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The structural subset of `BrowserNode` / `MeshSession` the store uses.
|
|
68
|
+
*
|
|
69
|
+
* `openStream` is synchronous on the direct node and a promise on a
|
|
70
|
+
* session, so this accepts either and the store awaits both.
|
|
71
|
+
*/
|
|
72
|
+
export interface StoreTransport {
|
|
73
|
+
/** This node's id — the authority a replica is talking to. */
|
|
74
|
+
nodeIdHex(): string | null;
|
|
75
|
+
/**
|
|
76
|
+
* Open a stream to a peer.
|
|
77
|
+
*
|
|
78
|
+
* **`label`, never `streamId`.** A stream id is a `u64` the leaf
|
|
79
|
+
* DERIVES from the label, and it sets a discriminator bit that makes
|
|
80
|
+
* an unsolicited arrival classify as stream data at the far end
|
|
81
|
+
* rather than as a channel message. Passing a textual id is refused
|
|
82
|
+
* by the wasm option reader outright, and passing an arbitrary
|
|
83
|
+
* number loses the discriminator — so the label is the only correct
|
|
84
|
+
* input and both ends derive the same id from it without exchanging
|
|
85
|
+
* one.
|
|
86
|
+
*/
|
|
87
|
+
openStream(options: {
|
|
88
|
+
reliability: 'reliable' | 'fireAndForget';
|
|
89
|
+
peer?: string;
|
|
90
|
+
label?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Ride the lossy carrier (fire-and-forget only): unordered, no
|
|
93
|
+
* retransmits, never delaying anything else. A transport without one
|
|
94
|
+
* may ignore it and send the packets like any others.
|
|
95
|
+
*/
|
|
96
|
+
lossy?: boolean;
|
|
97
|
+
}): TransportStream | Promise<TransportStream>;
|
|
98
|
+
onEvent(handler: (event: TransportFrame) => void): Cancel;
|
|
99
|
+
/**
|
|
100
|
+
* Make sure there IS a session with this peer, if the transport can.
|
|
101
|
+
*
|
|
102
|
+
* `openStream({peer})` refuses a peer the node has no session with,
|
|
103
|
+
* and a session — even a relayed one — is installed by a peer
|
|
104
|
+
* ATTEMPT (§9 step 2, `ensure_relayed_session`), not by discovery.
|
|
105
|
+
* So a store that only discovered its host still could not send it
|
|
106
|
+
* anything: `session: no session with 0x…`, which is what the real
|
|
107
|
+
* browser harness reports.
|
|
108
|
+
*
|
|
109
|
+
* Optional, because a transport that needs no such step (an
|
|
110
|
+
* in-process one, or a page that already connected the peer itself)
|
|
111
|
+
* should not have to pretend to offer it. When it is absent, or it
|
|
112
|
+
* fails, the store still tries to open — and the typed refusal from
|
|
113
|
+
* `openStream` is the honest answer rather than a second guess.
|
|
114
|
+
*/
|
|
115
|
+
connectPeer?(peerHex: string): Promise<unknown>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A peer id in the spelling `openStream` takes: 16 lowercase hex.
|
|
119
|
+
*
|
|
120
|
+
* Events carry the authenticated peer as an EXACT DECIMAL string, and
|
|
121
|
+
* `openStream({ peer })` requires 16 hex digits. Handing the decimal
|
|
122
|
+
* straight back is rejected for a short id and — worse — names a
|
|
123
|
+
* DIFFERENT peer for a 16-digit decimal one, which is a cross-peer
|
|
124
|
+
* defect wearing the shape of a formatting bug. `LeafStream` already
|
|
125
|
+
* reconciles the two spellings through `BigInt`; this is the same
|
|
126
|
+
* reconciliation for the store's own seam.
|
|
127
|
+
*/
|
|
128
|
+
export declare function peerHexOf(peer: string): string | null;
|
|
129
|
+
/** Whether two peer spellings name the same node. */
|
|
130
|
+
export declare function samePeer(left: string, right: string): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* How a host decides what each replica sees: exactly one of these.
|
|
133
|
+
*
|
|
134
|
+
* - `project(state, audience)` — one view per audience, shared by
|
|
135
|
+
* every player reading it. The default choice.
|
|
136
|
+
* - `projectFor(state, { peer, audience })` — one view per player, for
|
|
137
|
+
* what differs by who is looking (your own hand). Costs one
|
|
138
|
+
* projection per player per change.
|
|
139
|
+
*/
|
|
140
|
+
export type HostProjection<S extends object> = {
|
|
141
|
+
project(state: S, audience: readonly string[]): S;
|
|
142
|
+
readonly projectFor?: never;
|
|
143
|
+
} | {
|
|
144
|
+
projectFor(state: S, viewer: Viewer): S;
|
|
145
|
+
readonly project?: never;
|
|
146
|
+
} | {
|
|
147
|
+
/** Neither: the definition's declared `visibility` alone decides. */
|
|
148
|
+
readonly project?: never;
|
|
149
|
+
readonly projectFor?: never;
|
|
150
|
+
};
|
|
151
|
+
/** What a host needs beyond the owner's own dependencies. */
|
|
152
|
+
export type HostStoreOptions<S extends object, A extends ActionSpec, I extends InputSpec> = HostStoreBaseOptions<S, A, I> & HostProjection<S>;
|
|
153
|
+
/** {@link HostStoreOptions} without the projection. */
|
|
154
|
+
export interface HostStoreBaseOptions<S extends object, A extends ActionSpec, I extends InputSpec> extends Omit<OwnerDeps<S, A, I>, 'maxEventBytes' | 'newHandle' | 'newIncarnation' | 'now' | 'canProject' | 'store' | 'project' | 'projectFor'> {
|
|
155
|
+
readonly transport: StoreTransport;
|
|
156
|
+
/**
|
|
157
|
+
* WHICH store of this definition this is — the address a joiner
|
|
158
|
+
* names. Defaults to the definition id, which is what a node
|
|
159
|
+
* serving a single store of it means.
|
|
160
|
+
*
|
|
161
|
+
* A node hosting SEVERAL stores of one definition MUST name each,
|
|
162
|
+
* and their joiners must ask for them by name: every host store on
|
|
163
|
+
* a node sees every frame, so this is the only thing that decides
|
|
164
|
+
* which one answers.
|
|
165
|
+
*/
|
|
166
|
+
readonly store?: string;
|
|
167
|
+
/** The stream id both sides address. One label per store. */
|
|
168
|
+
readonly streamId?: string;
|
|
169
|
+
readonly maxEventBytes: number;
|
|
170
|
+
readonly initialState: S;
|
|
171
|
+
/**
|
|
172
|
+
* Development checks: `true` warns through `console.warn`, a function
|
|
173
|
+
* receives the messages. It says so when every player would receive
|
|
174
|
+
* the whole state without the definition declaring `visibility:
|
|
175
|
+
* 'open'`, and when a projection fails the state validator (which
|
|
176
|
+
* otherwise sends the player `empty()` silently). Leave it off in
|
|
177
|
+
* production.
|
|
178
|
+
*/
|
|
179
|
+
readonly dev?: boolean | ((message: string) => void);
|
|
180
|
+
now?: () => number;
|
|
181
|
+
newHandle?: () => Hex;
|
|
182
|
+
newIncarnation?: () => Hex;
|
|
183
|
+
canProject?: () => boolean;
|
|
184
|
+
/** Elapsed-time driver, so a test does not need real timers. */
|
|
185
|
+
schedule?: (run: () => void, ms: number) => Cancel;
|
|
186
|
+
}
|
|
187
|
+
/** The authoritative store, served to whoever the transport authenticates. */
|
|
188
|
+
export interface HostedStoreHandle<S extends object, A extends ActionSpec = ActionSpec, I extends InputSpec = InputSpec> {
|
|
189
|
+
/** This node's id: the authority every replica is talking to. */
|
|
190
|
+
readonly authority: string;
|
|
191
|
+
/** The definition this store serves. */
|
|
192
|
+
readonly definition: StoreDefinition<S, A, I>;
|
|
193
|
+
getState(): S;
|
|
194
|
+
subscribe(listener: (state: S, previous: S) => void): Cancel;
|
|
195
|
+
setState(next: S): void;
|
|
196
|
+
/**
|
|
197
|
+
* Write entities of a collection declared in the definition's
|
|
198
|
+
* `entities`: `id → value`, or `id → undefined` to remove one. Only the
|
|
199
|
+
* written entities are validated (by their per-entity parser); the
|
|
200
|
+
* whole-document `state` validator does not run, so a tick that moves a
|
|
201
|
+
* few ships out of thousands costs what it touches. One revision, one
|
|
202
|
+
* delta, however many ids.
|
|
203
|
+
*/
|
|
204
|
+
setEntities<C extends EntityCollection<S>>(collection: C, changes: Readonly<Record<string, EntityOf<S, C> | undefined>>): void;
|
|
205
|
+
/** {@link setEntities} for one id; `undefined` removes it. */
|
|
206
|
+
setEntity<C extends EntityCollection<S>>(collection: C, id: string, value: EntityOf<S, C> | undefined): void;
|
|
207
|
+
/**
|
|
208
|
+
* Handles, ledgers and pending projections, for a bounds report; and
|
|
209
|
+
* `sparseViews`, how many per-view deltas were projected from the changed
|
|
210
|
+
* entities alone (an entity write on a collection declared in both
|
|
211
|
+
* `interest` and `entities`, with only declared visibility rules).
|
|
212
|
+
*/
|
|
213
|
+
counts(): {
|
|
214
|
+
readonly handles: number;
|
|
215
|
+
readonly ledgers: number;
|
|
216
|
+
readonly deferred: number;
|
|
217
|
+
readonly sparseViews: number;
|
|
218
|
+
};
|
|
219
|
+
counters(): Readonly<Record<string, number>>;
|
|
220
|
+
close(): Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* What {@link hostPlayer} needs from a hosted store and nothing else
|
|
224
|
+
* does: its owner, the path that sends what a local call dispatched,
|
|
225
|
+
* and whether the store is still serving. Package-internal — keyed on
|
|
226
|
+
* the handle object, so only a handle `hostStore` returned has one.
|
|
227
|
+
*/
|
|
228
|
+
export interface HostInternals<S extends object, A extends ActionSpec, I extends InputSpec> {
|
|
229
|
+
readonly owner: StoreOwner<S, A, I>;
|
|
230
|
+
readonly peer: string;
|
|
231
|
+
readonly maxEventBytes: number;
|
|
232
|
+
dispatched(result: Dispatched): void;
|
|
233
|
+
isClosed(): boolean;
|
|
234
|
+
onClose(listener: () => void): void;
|
|
235
|
+
}
|
|
236
|
+
/** The internals of a handle `hostStore` returned, or `undefined`. */
|
|
237
|
+
export declare function hostInternals<S extends object, A extends ActionSpec, I extends InputSpec>(handle: HostedStoreHandle<S, A, I>): HostInternals<S, A, I> | undefined;
|
|
238
|
+
/**
|
|
239
|
+
* Serve one store to the mesh.
|
|
240
|
+
*
|
|
241
|
+
* Every inbound frame is dispatched with the peer the TRANSPORT
|
|
242
|
+
* authenticated. A frame whose event carries no peer is dropped and
|
|
243
|
+
* counted rather than dispatched with a guess: on a follower's proxied
|
|
244
|
+
* handle that field was once absent, and admitting those frames is the
|
|
245
|
+
* cross-peer admixture the identity work removed.
|
|
246
|
+
*/
|
|
247
|
+
export declare function hostStore<S extends object, A extends ActionSpec, I extends InputSpec>(options: HostStoreOptions<S, A, I>): HostedStoreHandle<S, A, I>;
|