@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
package/dist/wasm.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The wasm-bindgen boundary, described in TypeScript.
3
+ *
4
+ * These are the *only* shapes this package assumes about
5
+ * `net-mesh-leaf`. They are snake_case because that is what
6
+ * `wasm-bindgen` emits from Rust method names; every camelCase name a
7
+ * consumer sees is added by {@link BrowserNode} one layer up. Keeping
8
+ * the boundary in one file means a change on the Rust side breaks
9
+ * compilation here, at the seam, instead of somewhere in the middle of
10
+ * the wrapper.
11
+ */
12
+ /**
13
+ * One instantiation per specifier, shared by every `connect()`.
14
+ *
15
+ * `wasm-bindgen`'s initialiser instantiates a fresh module every call;
16
+ * two `connect()`s in one page would otherwise ship two copies of the
17
+ * leaf into memory.
18
+ */
19
+ const instantiated = new Map();
20
+ /** Resolve and instantiate the leaf wasm described by `source`. */
21
+ export async function loadLeafWasm(source = {}) {
22
+ if (source.wasm)
23
+ return source.wasm;
24
+ const specifier = source.wasmModule ?? new URL('./net_leaf.js', import.meta.url).href;
25
+ const key = `${specifier}|${String(source.wasmUrl ?? '')}`;
26
+ const pending = instantiated.get(key);
27
+ if (pending)
28
+ return pending;
29
+ const loading = importAndInit(specifier, source.wasmUrl);
30
+ instantiated.set(key, loading);
31
+ try {
32
+ return await loading;
33
+ }
34
+ catch (error) {
35
+ // A failed instantiation must not poison every later attempt: a
36
+ // page may retry with a corrected URL.
37
+ instantiated.delete(key);
38
+ throw error;
39
+ }
40
+ }
41
+ async function importAndInit(specifier, wasmUrl) {
42
+ // Genuinely runtime-selected, and a static import would be wrong
43
+ // twice over: the glue is a build artifact of the Rust crate that
44
+ // does not exist when this package is type-checked or published,
45
+ // and a page may point at any URL it serves it from.
46
+ const loaded = await import(/* @vite-ignore */ specifier);
47
+ const module = asLeafWasmModule(loaded, specifier);
48
+ if (module.default) {
49
+ await module.default(wasmUrl === undefined ? undefined : { module_or_path: wasmUrl });
50
+ }
51
+ return module;
52
+ }
53
+ function asLeafWasmModule(loaded, specifier) {
54
+ if (loaded === null || typeof loaded !== 'object' || !('LeafNode' in loaded)) {
55
+ throw new Error(`${specifier} is not a net-mesh-leaf module: no LeafNode export`);
56
+ }
57
+ // `wasm-bindgen` emits `LeafNode` as an ES class, so it is a
58
+ // *function* and `connect` is a static on it — not a plain object.
59
+ const leafNode = loaded.LeafNode;
60
+ if (typeof leafNode !== 'function' || !('connect' in leafNode)) {
61
+ throw new Error(`${specifier} exports a LeafNode without a static connect()`);
62
+ }
63
+ if (typeof leafNode.connect !== 'function') {
64
+ throw new Error(`${specifier} exports a LeafNode whose connect is not callable`);
65
+ }
66
+ // Checked as far as a runtime check can reach: the export exists and
67
+ // `connect` is callable. Its argument and return types are
68
+ // `wasm-bindgen`'s contract, not something JS can inspect.
69
+ return loaded;
70
+ }
71
+ /**
72
+ * One id argument of {@link LeafWasmNode.signal} as it crosses the
73
+ * wasm seam: a string passes through VERBATIM, and anything else is
74
+ * refused by the parsers' own names before it can reach the boundary.
75
+ *
76
+ * Refused, never coerced, and either alternative is fatal. A JS
77
+ * number cannot carry a `u64` id losslessly (`as f64 as u64`
78
+ * re-spells 511 of every 512 minted dialogs) — and a non-string must
79
+ * never reach `wasm-bindgen`'s String marshaling at all: it corrupts
80
+ * there, panicking `assert!(old_size > 0)` inside
81
+ * `passStringToWasm0`'s `__wbindgen_realloc`, and the call dies
82
+ * before `parse_peer_id` runs (finding #52's repro, on every peer
83
+ * spelling alike). The refusal carries the parser's name — "is not a
84
+ * peer id" / "is not a dialog id", as `name` picks — so a page that
85
+ * classifies a refusal by its text reads this one as the refusal it
86
+ * is. Peer before dialog at the call sites, the order
87
+ * `LeafNode::signal` parses in.
88
+ */
89
+ export function idArg(value, name) {
90
+ if (typeof value === 'string')
91
+ return value;
92
+ // `String(value)` on a null-prototype object throws, and a refusal
93
+ // that dies is not a refusal.
94
+ const shown = typeof value === 'object' && value !== null ? Object.prototype.toString.call(value) : String(value);
95
+ throw new TypeError(`${shown} is not a ${name} id: a ${name} id crosses this boundary as 16 hex digits as a ` +
96
+ `STRING — the spelling this boundary hands out; a ${typeof value} is refused rather than ` +
97
+ `coerced, because a number cannot carry a u64 id`);
98
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Cross-border actions (browser plan §9 item 4): region A asks region B to
3
+ * act on an entity B owns — a hit across the border, a door on the other
4
+ * side — and **B's authority decides**. At-most-once, like handoff:
5
+ *
6
+ * - A sends `{ id, name, input }` and repeats it until B answers.
7
+ * - B runs the action ONCE per id, records the outcome in `acts` in the same
8
+ * commit as its effect, makes both durable, and answers. Every repeat of
9
+ * that id gets the recorded answer and changes nothing.
10
+ * - A that hears nothing within `giveUpMs` reports `unresolved`: the action
11
+ * may or may not have happened, but it never happened twice. The caller
12
+ * decides whether to ask again under a NEW id.
13
+ *
14
+ * A's pending actions are not persisted: an action A loses in a crash either
15
+ * reached B (and happened once) or did not (and did not happen).
16
+ */
17
+ import type { RegionState } from './handoff.js';
18
+ /** A to B: act on something B owns. */
19
+ export interface ForwardedAction {
20
+ readonly k: 'act';
21
+ readonly id: string;
22
+ /** The asking region. */
23
+ readonly from: string;
24
+ readonly name: string;
25
+ readonly input: unknown;
26
+ }
27
+ /** B to A: the recorded outcome of one action id, the same every time. */
28
+ export type ActionResult = {
29
+ readonly k: 'result';
30
+ readonly id: string;
31
+ readonly from: string;
32
+ readonly ok: true;
33
+ readonly output: unknown;
34
+ } | {
35
+ readonly k: 'result';
36
+ readonly id: string;
37
+ readonly from: string;
38
+ readonly ok: false;
39
+ readonly reason: string;
40
+ };
41
+ /** An outcome the target recorded for an action id. */
42
+ export interface ActRecord {
43
+ readonly at: number;
44
+ readonly ok: boolean;
45
+ readonly output?: unknown;
46
+ readonly reason?: string;
47
+ }
48
+ /**
49
+ * How a region applies one kind of forwarded action: the entities after it
50
+ * and the answer, or a refusal reason (string). Pure: the driver commits the
51
+ * returned entities, the record and nothing else.
52
+ */
53
+ export type BorderAction<E> = (entities: Readonly<Record<string, E>>, input: unknown, from: string) => {
54
+ readonly entities: Readonly<Record<string, E>>;
55
+ readonly output: unknown;
56
+ } | string;
57
+ /**
58
+ * At the target: apply a forwarded action once per id and answer every copy
59
+ * of it the same way. An unknown action name is refused (and recorded, so
60
+ * the answer stays the same).
61
+ */
62
+ export declare function onForwardedAction<E>(state: RegionState<E>, act: ForwardedAction, now: number, actions: Readonly<Record<string, BorderAction<E>>>): {
63
+ readonly state: RegionState<E>;
64
+ readonly reply: ActionResult;
65
+ };
66
+ /**
67
+ * Region A to neighbour B: A's entities near their shared border, read-only
68
+ * (plan §9 item 5, ghosting). Newer `seq` replaces older; nothing here is
69
+ * authoritative in B.
70
+ */
71
+ export interface GhostFrame<E> {
72
+ readonly k: 'ghost';
73
+ readonly from: string;
74
+ readonly seq: number;
75
+ readonly entities: Readonly<Record<string, E>>;
76
+ }
77
+ /**
78
+ * Which neighbours of region `r:<rx>:<rz>` (squares of `size`) should see an
79
+ * entity at (x, z) within `margin` of their edge — up to three (two sides
80
+ * and the corner between them).
81
+ */
82
+ export declare function ghostTargets(region: string, x: number, z: number, size: number, margin: number): string[];
83
+ /** Forget action records older than `retentionMs` (which must outlast any source's retries). */
84
+ export declare function pruneActs<E>(state: RegionState<E>, now: number, retentionMs: number): RegionState<E>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Cross-border actions (browser plan §9 item 4): region A asks region B to
3
+ * act on an entity B owns — a hit across the border, a door on the other
4
+ * side — and **B's authority decides**. At-most-once, like handoff:
5
+ *
6
+ * - A sends `{ id, name, input }` and repeats it until B answers.
7
+ * - B runs the action ONCE per id, records the outcome in `acts` in the same
8
+ * commit as its effect, makes both durable, and answers. Every repeat of
9
+ * that id gets the recorded answer and changes nothing.
10
+ * - A that hears nothing within `giveUpMs` reports `unresolved`: the action
11
+ * may or may not have happened, but it never happened twice. The caller
12
+ * decides whether to ask again under a NEW id.
13
+ *
14
+ * A's pending actions are not persisted: an action A loses in a crash either
15
+ * reached B (and happened once) or did not (and did not happen).
16
+ */
17
+ const has = (record, key) => Object.prototype.hasOwnProperty.call(record, key);
18
+ function resultOf(region, id, record) {
19
+ return record.ok
20
+ ? { k: 'result', id, from: region, ok: true, output: record.output ?? null }
21
+ : { k: 'result', id, from: region, ok: false, reason: record.reason ?? 'refused' };
22
+ }
23
+ /**
24
+ * At the target: apply a forwarded action once per id and answer every copy
25
+ * of it the same way. An unknown action name is refused (and recorded, so
26
+ * the answer stays the same).
27
+ */
28
+ export function onForwardedAction(state, act, now, actions) {
29
+ const acts = state.acts ?? {};
30
+ if (has(acts, act.id))
31
+ return { state, reply: resultOf(state.region, act.id, acts[act.id]) };
32
+ let record;
33
+ let entities = state.entities;
34
+ const handler = has(actions, act.name) ? actions[act.name] : undefined;
35
+ if (handler === undefined) {
36
+ record = { at: now, ok: false, reason: `no action '${act.name}' here` };
37
+ }
38
+ else {
39
+ let outcome;
40
+ try {
41
+ outcome = handler(state.entities, act.input, act.from);
42
+ }
43
+ catch (error) {
44
+ outcome = `action threw: ${String(error)}`;
45
+ }
46
+ if (typeof outcome === 'string') {
47
+ record = { at: now, ok: false, reason: outcome };
48
+ }
49
+ else {
50
+ entities = outcome.entities;
51
+ record = { at: now, ok: true, output: outcome.output };
52
+ }
53
+ }
54
+ const nextActs = { ...acts };
55
+ Object.defineProperty(nextActs, act.id, { value: record, enumerable: true, writable: true, configurable: true });
56
+ return { state: { ...state, entities, acts: nextActs }, reply: resultOf(state.region, act.id, record) };
57
+ }
58
+ /**
59
+ * Which neighbours of region `r:<rx>:<rz>` (squares of `size`) should see an
60
+ * entity at (x, z) within `margin` of their edge — up to three (two sides
61
+ * and the corner between them).
62
+ */
63
+ export function ghostTargets(region, x, z, size, margin) {
64
+ const match = /^r:(-?\d+):(-?\d+)$/.exec(region);
65
+ if (match === null)
66
+ return [];
67
+ const rx = Number(match[1]);
68
+ const rz = Number(match[2]);
69
+ const near = (low, high, at) => {
70
+ const out = [0];
71
+ if (at - low < margin)
72
+ out.push(-1);
73
+ if (high - at <= margin)
74
+ out.push(1);
75
+ return out;
76
+ };
77
+ const out = [];
78
+ for (const dx of near(rx * size, (rx + 1) * size, x)) {
79
+ for (const dz of near(rz * size, (rz + 1) * size, z)) {
80
+ if (dx !== 0 || dz !== 0)
81
+ out.push(`r:${rx + dx}:${rz + dz}`);
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ /** Forget action records older than `retentionMs` (which must outlast any source's retries). */
87
+ export function pruneActs(state, now, retentionMs) {
88
+ const acts = state.acts;
89
+ if (acts === undefined)
90
+ return state;
91
+ let next = null;
92
+ for (const [id, record] of Object.entries(acts)) {
93
+ if (now - record.at > retentionMs) {
94
+ next ??= { ...acts };
95
+ delete next[id];
96
+ }
97
+ }
98
+ return next === null ? state : { ...state, acts: next };
99
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Entity handoff between region hosts — at-most-once, with a typed failure
3
+ * (browser plan §9 item 3, decision Q6).
4
+ *
5
+ * An entity crossing from region A to region B must never be live in both,
6
+ * and must never be applied twice. Exactly-once is not attempted; the
7
+ * failure a crash can cause is an entity that is **nowhere live for a
8
+ * while**, reported typed, never a duplicate.
9
+ *
10
+ * The protocol, over any lossy, duplicating, reordering carrier:
11
+ *
12
+ * 1. **A freezes.** The entity leaves A's live entities and waits in A's
13
+ * `outgoing` under a fresh handoff id, with its state. A answers inputs for
14
+ * it with `in-transit` (see {@link locate}).
15
+ * 2. **A offers** `{ id, entity, state }` to B, and repeats the offer until it
16
+ * hears back. The id is the idempotence key.
17
+ * 3. **B decides once per id.** On the first offer it either admits the
18
+ * entity (it is now live in B) or refuses it, and records the decision in
19
+ * `handled`. Every later copy of that offer gets the SAME answer and
20
+ * changes nothing.
21
+ * 4. **A settles.** `accept`: the frozen copy is dropped, and the entity is
22
+ * B's. `refuse`: the entity returns to A's live entities. Only an explicit
23
+ * refusal can bring it back to A, because silence proves nothing: B may
24
+ * have admitted it and the answer been lost.
25
+ * 5. **No answer.** A keeps it frozen and keeps offering. After `giveUpMs`,
26
+ * {@link retryHandoffs} reports it `unresolved` and stops offering. The
27
+ * entity stays frozen, still nowhere live twice. Recovery (the destination
28
+ * returning, an operator) re-offers with the same id through
29
+ * {@link reofferHandoff}, which B answers from its record.
30
+ *
31
+ * **Durability is the caller's, and it is the whole correctness argument.**
32
+ * Every step here is a pure function from one {@link RegionState} to the
33
+ * next. The host MUST make the returned state durable (e.g.
34
+ * `persistStore(…).flush()`) BEFORE it sends the messages the step returns. A
35
+ * crash then either loses the step with its messages, which is the same as a
36
+ * lost message, or keeps both. B must also remember a decision for longer
37
+ * than any A keeps offering: `handledRetentionMs` > `giveUpMs`.
38
+ */
39
+ import type { ActRecord, ActionResult, ForwardedAction, GhostFrame } from './border.js';
40
+ /** A handoff's idempotence key: unique per crossing. */
41
+ export type HandoffId = string;
42
+ /** A to B: take this entity. */
43
+ export interface HandoffOffer<E> {
44
+ readonly k: 'offer';
45
+ readonly id: HandoffId;
46
+ /** The source region. */
47
+ readonly from: string;
48
+ readonly entity: string;
49
+ readonly state: E;
50
+ }
51
+ /** B to A: the decision for one handoff id, the same every time it is asked. */
52
+ export type HandoffReply = {
53
+ readonly k: 'accept';
54
+ readonly id: HandoffId;
55
+ readonly from: string;
56
+ } | {
57
+ readonly k: 'refuse';
58
+ readonly id: HandoffId;
59
+ readonly from: string;
60
+ readonly reason: string;
61
+ };
62
+ /** A message between region hosts, addressed to region `to`. */
63
+ export type HandoffMessage<E> = {
64
+ readonly to: string;
65
+ readonly body: HandoffOffer<E> | HandoffReply | ForwardedAction | ActionResult | GhostFrame<E>;
66
+ };
67
+ /** An entity frozen at the source, on its way out. */
68
+ export interface Outgoing<E> {
69
+ readonly entity: string;
70
+ readonly state: E;
71
+ readonly to: string;
72
+ /** When the handoff began, ms. */
73
+ readonly since: number;
74
+ /** When the offer was last sent, ms. */
75
+ readonly sentAt: number;
76
+ /** Stopped offering after `giveUpMs` (typed `unresolved`); still frozen. */
77
+ readonly unresolved: boolean;
78
+ }
79
+ /** A decision the target recorded for an id. */
80
+ export interface Handled {
81
+ readonly decision: 'accept' | 'refuse';
82
+ readonly reason?: string;
83
+ readonly at: number;
84
+ }
85
+ /**
86
+ * A region host's handoff-relevant state. Keep it IN the region's persisted
87
+ * document, and make it durable as one unit with the entities: a frozen
88
+ * entity must not come back live after a restart, and an admitted one must
89
+ * not be admitted again.
90
+ */
91
+ export interface RegionState<E> {
92
+ readonly region: string;
93
+ readonly entities: Readonly<Record<string, E>>;
94
+ readonly outgoing: Readonly<Record<HandoffId, Outgoing<E>>>;
95
+ readonly handled: Readonly<Record<HandoffId, Handled>>;
96
+ /** Cross-border actions applied here, by id (`border.ts`). */
97
+ readonly acts?: Readonly<Record<string, ActRecord>>;
98
+ }
99
+ /** Timing for {@link retryHandoffs} and {@link pruneHandled}. */
100
+ export interface HandoffTiming {
101
+ /** Re-offer an unanswered handoff this often, ms. Default 250. */
102
+ readonly retryMs?: number;
103
+ /** Stop offering after this long and report `unresolved`, ms. Default 60 000. */
104
+ readonly giveUpMs?: number;
105
+ /** How long a target remembers a decision, ms. Default 600 000; must exceed `giveUpMs`. */
106
+ readonly handledRetentionMs?: number;
107
+ }
108
+ /** What one step did, for the host to act on AFTER making the state durable. */
109
+ export interface HandoffStep<E> {
110
+ readonly state: RegionState<E>;
111
+ /** Send these, after the state is durable. */
112
+ readonly send: readonly HandoffMessage<E>[];
113
+ /** Settled handoffs, for the game (e.g. tell the player where to go). */
114
+ readonly events: readonly HandoffEvent[];
115
+ }
116
+ /** A handoff outcome at the source, or an admission at the target. */
117
+ export type HandoffEvent = {
118
+ readonly type: 'moved';
119
+ readonly id: HandoffId;
120
+ readonly entity: string;
121
+ readonly to: string;
122
+ } | {
123
+ readonly type: 'refused';
124
+ readonly id: HandoffId;
125
+ readonly entity: string;
126
+ readonly to: string;
127
+ readonly reason: string;
128
+ } | {
129
+ readonly type: 'unresolved';
130
+ readonly id: HandoffId;
131
+ readonly entity: string;
132
+ readonly to: string;
133
+ } | {
134
+ readonly type: 'admitted';
135
+ readonly id: HandoffId;
136
+ readonly entity: string;
137
+ readonly from: string;
138
+ };
139
+ /** An empty region. */
140
+ export declare function regionState<E>(region: string, entities?: Readonly<Record<string, E>>): RegionState<E>;
141
+ /**
142
+ * Step 1–2 at the source: freeze `entity` and offer it to region `to`.
143
+ * Throws if the entity is not live here (frozen already, or unknown).
144
+ */
145
+ export declare function beginHandoff<E>(state: RegionState<E>, entity: string, to: string, id: HandoffId, now: number): HandoffStep<E>;
146
+ /**
147
+ * Step 3 at the target: decide an offer once, and answer every copy of it
148
+ * the same way. `admit` is asked only the first time; it returns `true` or a
149
+ * refusal reason. An admitted entity whose id is already live here is
150
+ * refused (`occupied`), never overwritten.
151
+ */
152
+ export declare function onHandoffOffer<E>(state: RegionState<E>, offer: HandoffOffer<E>, now: number, admit?: (entity: string, value: E) => true | string): HandoffStep<E>;
153
+ /**
154
+ * Step 4 at the source: settle on the target's answer. A reply for a handoff
155
+ * this region is not holding (already settled, or never begun) changes
156
+ * nothing: replies are duplicated and reordered like everything else.
157
+ */
158
+ export declare function onHandoffReply<E>(state: RegionState<E>, reply: HandoffReply): HandoffStep<E>;
159
+ /**
160
+ * Step 2 and 5 at the source, on a timer: re-offer what is unanswered, and
161
+ * mark what has waited past `giveUpMs` as `unresolved` (reported once, still
162
+ * frozen, no longer offered).
163
+ */
164
+ export declare function retryHandoffs<E>(state: RegionState<E>, now: number, options?: HandoffTiming): HandoffStep<E>;
165
+ /**
166
+ * Recovery: offer an `unresolved` handoff again, under the SAME id (the
167
+ * destination answers it from its record, so this cannot admit twice), and
168
+ * restart its give-up clock. Use it when the destination is known to be
169
+ * back. A no-op for a handoff that is not unresolved.
170
+ */
171
+ export declare function reofferHandoff<E>(state: RegionState<E>, id: HandoffId, now: number): HandoffStep<E>;
172
+ /** Forget target decisions older than `handledRetentionMs` (which must outlast any source's offers). */
173
+ export declare function pruneHandled<E>(state: RegionState<E>, now: number, options?: HandoffTiming): RegionState<E>;
174
+ /**
175
+ * Where an entity is, from this region's point of view — what to answer an
176
+ * input for it: `here` (apply it), `in-transit` (frozen, on its way to
177
+ * `to`; refuse, never apply), or `unknown`.
178
+ */
179
+ export declare function locate<E>(state: RegionState<E>, entity: string): {
180
+ readonly at: 'here';
181
+ } | {
182
+ readonly at: 'in-transit';
183
+ readonly to: string;
184
+ readonly id: HandoffId;
185
+ } | {
186
+ readonly at: 'unknown';
187
+ };