@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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The networked game store.
|
|
3
|
+
*
|
|
4
|
+
* **The local half:** the type contract, `defineStore`, and the
|
|
5
|
+
* transport-free {@link StoreCore} that both an authoritative host and
|
|
6
|
+
* a joined replica are built on — state, subscriptions, status and the
|
|
7
|
+
* synchronous transaction.
|
|
8
|
+
*
|
|
9
|
+
* **The protocol:** the codec and parser ladder (`wire`), chunking and
|
|
10
|
+
* assembly (`chunker`, `assembly`), the owner's dispatch and the
|
|
11
|
+
* action ledger (`owner`, `ledger`), and the replica's state machine
|
|
12
|
+
* (`replica`).
|
|
13
|
+
*
|
|
14
|
+
* **The transport halves:** {@link hostStore} and {@link joinStore}.
|
|
15
|
+
* A frame arrives as a `stream_data` event, the event carries the peer
|
|
16
|
+
* whose installed session opened the packet, and THAT is the identity
|
|
17
|
+
* the policy is handed — no store frame has an originator field to
|
|
18
|
+
* consult (§1.3).
|
|
19
|
+
*
|
|
20
|
+
* **What these are not yet evidence for.** §4 gates `joinStore` on
|
|
21
|
+
* leader-proxy lifecycle: a follower's `ProxyStream` now carries its
|
|
22
|
+
* peer (6256f970b), which is the precondition, but the store's own use
|
|
23
|
+
* of a proxied handle on a follower — last-consumer cleanup, the
|
|
24
|
+
* peer/stream lifecycle across a leader change — is not established.
|
|
25
|
+
* A host and a joiner in one tab are exercised here; two tabs sharing
|
|
26
|
+
* a leader are not. See
|
|
27
|
+
* `docs/internal/plans/BROWSER_GAME_STORE_API_DESIGN.md` §7.
|
|
28
|
+
*/
|
|
29
|
+
export { defineStore } from './definition.js';
|
|
30
|
+
export { hostStore, HOST_SWEEP_MS } from './host.js';
|
|
31
|
+
export { joinStore, ALIVE_INTERVAL_MS, MAX_OUTSTANDING, REQUEST_DEADLINE_MS } from './join.js';
|
|
32
|
+
export { hostPlayer } from './player.js';
|
|
33
|
+
export type { HostPlayerOptions } from './player.js';
|
|
34
|
+
export type { Frame, HostedStoreHandle, HostProjection, HostStoreBaseOptions, HostStoreOptions, StoreTransport, TransportFrame, TransportStream, } from './host.js';
|
|
35
|
+
export type { JoinedStoreHandle, JoinStoreOptions } from './join.js';
|
|
36
|
+
export type { Viewer } from './owner.js';
|
|
37
|
+
export { StoreCore } from './core.js';
|
|
38
|
+
export { StoreError } from './errors.js';
|
|
39
|
+
export { mergeShallow, reconcile } from './state.js';
|
|
40
|
+
export type { StoreErrorCode } from './errors.js';
|
|
41
|
+
export type { AccessRequest, ActionContext, ActionSpec, Cancel, EntityCollection, EntityOf, HostedStore, InputDisposition, InputSpec, JoinedStore, OperationOptions, Parse, ReadonlyState, SelectorOptions, StateUpdate, StoreDefinition, StoreLimits, StorePhase, StoreReader, StoreStatus, } from './types.js';
|
|
42
|
+
export type { LeaveReason, StoreEvent } from './owner.js';
|
|
43
|
+
export { MAX_EVENT_ROUNDS } from './owner.js';
|
|
44
|
+
export { addItems, countItems, emptyInventory, hasItems, InventoryError, inventoryOf, MAX_ITEM_ID_CHARS, onlyOwn, parseInventory, removeItems, } from './inventory.js';
|
|
45
|
+
export type { Inventory, InventoryErrorCode, InventoryRules } from './inventory.js';
|
|
46
|
+
export { assertHidden, HIDDEN, hiddenOr, isHidden, projectVisible, } from './visibility.js';
|
|
47
|
+
export type { Hidden, Visibility, VisibilityPreset, VisibilityRule, VisibilityRules } from './visibility.js';
|
|
48
|
+
export { cellKey, cellsAround, sameCells, stickyCells } from './interest.js';
|
|
49
|
+
export type { CellOptions } from './interest.js';
|
|
50
|
+
export { MAX_INTEREST_KEYS, MAX_INTEREST_KEY_BYTES } from './wire.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The networked game store.
|
|
3
|
+
*
|
|
4
|
+
* **The local half:** the type contract, `defineStore`, and the
|
|
5
|
+
* transport-free {@link StoreCore} that both an authoritative host and
|
|
6
|
+
* a joined replica are built on — state, subscriptions, status and the
|
|
7
|
+
* synchronous transaction.
|
|
8
|
+
*
|
|
9
|
+
* **The protocol:** the codec and parser ladder (`wire`), chunking and
|
|
10
|
+
* assembly (`chunker`, `assembly`), the owner's dispatch and the
|
|
11
|
+
* action ledger (`owner`, `ledger`), and the replica's state machine
|
|
12
|
+
* (`replica`).
|
|
13
|
+
*
|
|
14
|
+
* **The transport halves:** {@link hostStore} and {@link joinStore}.
|
|
15
|
+
* A frame arrives as a `stream_data` event, the event carries the peer
|
|
16
|
+
* whose installed session opened the packet, and THAT is the identity
|
|
17
|
+
* the policy is handed — no store frame has an originator field to
|
|
18
|
+
* consult (§1.3).
|
|
19
|
+
*
|
|
20
|
+
* **What these are not yet evidence for.** §4 gates `joinStore` on
|
|
21
|
+
* leader-proxy lifecycle: a follower's `ProxyStream` now carries its
|
|
22
|
+
* peer (6256f970b), which is the precondition, but the store's own use
|
|
23
|
+
* of a proxied handle on a follower — last-consumer cleanup, the
|
|
24
|
+
* peer/stream lifecycle across a leader change — is not established.
|
|
25
|
+
* A host and a joiner in one tab are exercised here; two tabs sharing
|
|
26
|
+
* a leader are not. See
|
|
27
|
+
* `docs/internal/plans/BROWSER_GAME_STORE_API_DESIGN.md` §7.
|
|
28
|
+
*/
|
|
29
|
+
export { defineStore } from './definition.js';
|
|
30
|
+
export { hostStore, HOST_SWEEP_MS } from './host.js';
|
|
31
|
+
export { joinStore, ALIVE_INTERVAL_MS, MAX_OUTSTANDING, REQUEST_DEADLINE_MS } from './join.js';
|
|
32
|
+
export { hostPlayer } from './player.js';
|
|
33
|
+
export { StoreCore } from './core.js';
|
|
34
|
+
export { StoreError } from './errors.js';
|
|
35
|
+
export { mergeShallow, reconcile } from './state.js';
|
|
36
|
+
export { MAX_EVENT_ROUNDS } from './owner.js';
|
|
37
|
+
export { addItems, countItems, emptyInventory, hasItems, InventoryError, inventoryOf, MAX_ITEM_ID_CHARS, onlyOwn, parseInventory, removeItems, } from './inventory.js';
|
|
38
|
+
export { assertHidden, HIDDEN, hiddenOr, isHidden, projectVisible, } from './visibility.js';
|
|
39
|
+
export { cellKey, cellsAround, sameCells, stickyCells } from './interest.js';
|
|
40
|
+
export { MAX_INTEREST_KEYS, MAX_INTEREST_KEY_BYTES } from './wire.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid-cell helpers for interest management.
|
|
3
|
+
*
|
|
4
|
+
* Interest keys are any strings; a square grid over the ground plane is
|
|
5
|
+
* the common case, so here it is:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* defineStore({ …, interest: { ships: ship => cellKey(ship.x, ship.z, 32) } });
|
|
9
|
+
*
|
|
10
|
+
* const replica = joinStore({ …, interest: cellsAround(me.x, me.z, { size: 32 }) });
|
|
11
|
+
* // each frame, or when the player moves:
|
|
12
|
+
* const next = stickyCells(current, me.x, me.z, { size: 32 });
|
|
13
|
+
* if (!sameCells(next, current)) { current = next; void replica.setInterest(next); }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `stickyCells` keeps a cell you just left until you are `margin` cells
|
|
17
|
+
* past it, so a player standing on a border does not flip interest on
|
|
18
|
+
* every step.
|
|
19
|
+
*/
|
|
20
|
+
/** Options for the grid helpers. */
|
|
21
|
+
export interface CellOptions {
|
|
22
|
+
/** Cell edge length, in world units. */
|
|
23
|
+
readonly size: number;
|
|
24
|
+
/** Cells around the centre one to include: 1 → a 3×3 block. Default 1. */
|
|
25
|
+
readonly radius?: number;
|
|
26
|
+
/** For `stickyCells`: extra cells a previously held cell may be away before it is dropped. Default 1. */
|
|
27
|
+
readonly margin?: number;
|
|
28
|
+
}
|
|
29
|
+
/** The cell containing `(x, z)`: `"<column>:<row>"`. */
|
|
30
|
+
export declare function cellKey(x: number, z: number, size: number): string;
|
|
31
|
+
/** The cells within `radius` of the one containing `(x, z)`, centre first. */
|
|
32
|
+
export declare function cellsAround(x: number, z: number, options: CellOptions): string[];
|
|
33
|
+
/**
|
|
34
|
+
* {@link cellsAround}, plus any cell of `previous` still within
|
|
35
|
+
* `radius + margin` of the player — the hysteresis that stops a player
|
|
36
|
+
* on a cell border from churning interest.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stickyCells(previous: readonly string[], x: number, z: number, options: CellOptions): string[];
|
|
39
|
+
/** Whether two interest sets hold the same keys, in any order. */
|
|
40
|
+
export declare function sameCells(a: readonly string[], b: readonly string[]): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid-cell helpers for interest management.
|
|
3
|
+
*
|
|
4
|
+
* Interest keys are any strings; a square grid over the ground plane is
|
|
5
|
+
* the common case, so here it is:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* defineStore({ …, interest: { ships: ship => cellKey(ship.x, ship.z, 32) } });
|
|
9
|
+
*
|
|
10
|
+
* const replica = joinStore({ …, interest: cellsAround(me.x, me.z, { size: 32 }) });
|
|
11
|
+
* // each frame, or when the player moves:
|
|
12
|
+
* const next = stickyCells(current, me.x, me.z, { size: 32 });
|
|
13
|
+
* if (!sameCells(next, current)) { current = next; void replica.setInterest(next); }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `stickyCells` keeps a cell you just left until you are `margin` cells
|
|
17
|
+
* past it, so a player standing on a border does not flip interest on
|
|
18
|
+
* every step.
|
|
19
|
+
*/
|
|
20
|
+
function checkSize(size) {
|
|
21
|
+
if (!(size > 0) || !Number.isFinite(size))
|
|
22
|
+
throw new RangeError(`a cell size is a positive number, got ${String(size)}`);
|
|
23
|
+
}
|
|
24
|
+
/** The cell containing `(x, z)`: `"<column>:<row>"`. */
|
|
25
|
+
export function cellKey(x, z, size) {
|
|
26
|
+
checkSize(size);
|
|
27
|
+
return `${String(Math.floor(x / size))}:${String(Math.floor(z / size))}`;
|
|
28
|
+
}
|
|
29
|
+
function parseCell(key) {
|
|
30
|
+
const match = /^(-?\d+):(-?\d+)$/.exec(key);
|
|
31
|
+
return match === null ? null : [Number(match[1]), Number(match[2])];
|
|
32
|
+
}
|
|
33
|
+
/** The cells within `radius` of the one containing `(x, z)`, centre first. */
|
|
34
|
+
export function cellsAround(x, z, options) {
|
|
35
|
+
checkSize(options.size);
|
|
36
|
+
const radius = options.radius ?? 1;
|
|
37
|
+
const column = Math.floor(x / options.size);
|
|
38
|
+
const row = Math.floor(z / options.size);
|
|
39
|
+
const out = [`${String(column)}:${String(row)}`];
|
|
40
|
+
for (let dc = -radius; dc <= radius; dc += 1) {
|
|
41
|
+
for (let dr = -radius; dr <= radius; dr += 1) {
|
|
42
|
+
if (dc !== 0 || dr !== 0)
|
|
43
|
+
out.push(`${String(column + dc)}:${String(row + dr)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* {@link cellsAround}, plus any cell of `previous` still within
|
|
50
|
+
* `radius + margin` of the player — the hysteresis that stops a player
|
|
51
|
+
* on a cell border from churning interest.
|
|
52
|
+
*/
|
|
53
|
+
export function stickyCells(previous, x, z, options) {
|
|
54
|
+
const now = cellsAround(x, z, options);
|
|
55
|
+
const radius = options.radius ?? 1;
|
|
56
|
+
const reach = radius + (options.margin ?? 1);
|
|
57
|
+
const column = Math.floor(x / options.size);
|
|
58
|
+
const row = Math.floor(z / options.size);
|
|
59
|
+
const held = new Set(now);
|
|
60
|
+
for (const key of previous) {
|
|
61
|
+
if (held.has(key))
|
|
62
|
+
continue;
|
|
63
|
+
const cell = parseCell(key);
|
|
64
|
+
if (cell !== null && Math.abs(cell[0] - column) <= reach && Math.abs(cell[1] - row) <= reach) {
|
|
65
|
+
now.push(key);
|
|
66
|
+
held.add(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return now;
|
|
70
|
+
}
|
|
71
|
+
/** Whether two interest sets hold the same keys, in any order. */
|
|
72
|
+
export function sameCells(a, b) {
|
|
73
|
+
if (a.length !== b.length)
|
|
74
|
+
return false;
|
|
75
|
+
const set = new Set(a);
|
|
76
|
+
return b.every(key => set.has(key));
|
|
77
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bones of a player inventory: stacks of items, kept in the store's
|
|
3
|
+
* state, changed only by the host.
|
|
4
|
+
*
|
|
5
|
+
* An inventory is plain JSON — item id → count — so it rides in any
|
|
6
|
+
* store document, diffs like the rest of it, and needs no protocol of
|
|
7
|
+
* its own. These are pure functions: call them inside an action
|
|
8
|
+
* handler or the `onEvent` hook and `setState` the result. A change
|
|
9
|
+
* that breaks the rules throws, which inside a handler refuses the
|
|
10
|
+
* action and discards its writes.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // state: { inventories: Record<peer, Inventory>, … }
|
|
14
|
+
* onEvent: (event, context) => {
|
|
15
|
+
* if (event.type !== 'join') return;
|
|
16
|
+
* const inventories = { ...context.getState().inventories };
|
|
17
|
+
* inventories[event.peer] ??= addItems(emptyInventory(), 'potion', 3, rules);
|
|
18
|
+
* context.setState({ inventories });
|
|
19
|
+
* },
|
|
20
|
+
* actions: {
|
|
21
|
+
* drink: (_input, context) => {
|
|
22
|
+
* const inventories = { ...context.getState().inventories };
|
|
23
|
+
* inventories[context.peer] = removeItems(inventoryOf(inventories, context.peer), 'potion', 1);
|
|
24
|
+
* context.setState({ inventories });
|
|
25
|
+
* return { left: countItems(inventories[context.peer], 'potion') };
|
|
26
|
+
* },
|
|
27
|
+
* },
|
|
28
|
+
* projectFor: (state, { peer }) => ({ ...state, inventories: onlyOwn(state.inventories, peer) }),
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* **Deliberately not here yet:** trading between players (it needs both
|
|
32
|
+
* sides' consent and one atomic transfer), equipment slots, item
|
|
33
|
+
* metadata. Item ids are yours; what an item *is* stays game data.
|
|
34
|
+
*/
|
|
35
|
+
/** Item id → how many. Every count is a whole number ≥ 1; absent means none. */
|
|
36
|
+
export type Inventory = Readonly<Record<string, number>>;
|
|
37
|
+
/** Limits an inventory keeps. Every limit is optional. */
|
|
38
|
+
export interface InventoryRules {
|
|
39
|
+
/** How many different items it may hold at once. */
|
|
40
|
+
readonly maxKinds?: number;
|
|
41
|
+
/** The most of one item: one number for all, or per item id (missing ids unlimited). */
|
|
42
|
+
readonly maxCount?: number | Readonly<Record<string, number>>;
|
|
43
|
+
}
|
|
44
|
+
/** Why an inventory change was refused. */
|
|
45
|
+
export type InventoryErrorCode = 'insufficient' | 'full' | 'invalid';
|
|
46
|
+
export declare class InventoryError extends Error {
|
|
47
|
+
readonly code: InventoryErrorCode;
|
|
48
|
+
constructor(code: InventoryErrorCode, message: string);
|
|
49
|
+
}
|
|
50
|
+
/** Longest item id accepted. Ids are keys in a document every player may receive. */
|
|
51
|
+
export declare const MAX_ITEM_ID_CHARS = 64;
|
|
52
|
+
/** An inventory with nothing in it. */
|
|
53
|
+
export declare function emptyInventory(): Inventory;
|
|
54
|
+
/** How many of `item` it holds. */
|
|
55
|
+
export declare function countItems(inventory: Inventory | undefined, item: string): number;
|
|
56
|
+
/** Whether it holds at least `count` of `item`. */
|
|
57
|
+
export declare function hasItems(inventory: Inventory | undefined, item: string, count?: number): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Add `count` of `item`. Throws `full` when that would break a rule;
|
|
60
|
+
* nothing is partly added.
|
|
61
|
+
*/
|
|
62
|
+
export declare function addItems(inventory: Inventory, item: string, count: number, rules?: InventoryRules): Inventory;
|
|
63
|
+
/**
|
|
64
|
+
* Remove `count` of `item`. Throws `insufficient` when it holds fewer;
|
|
65
|
+
* nothing is partly removed. An item that reaches zero is gone.
|
|
66
|
+
*/
|
|
67
|
+
export declare function removeItems(inventory: Inventory, item: string, count: number): Inventory;
|
|
68
|
+
/**
|
|
69
|
+
* A validator for an inventory inside a store definition's `state`:
|
|
70
|
+
* accepts only whole positive counts under valid item ids, and the
|
|
71
|
+
* rules if given. Throws otherwise, as a definition validator should.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseInventory(value: unknown, rules?: InventoryRules): Inventory;
|
|
74
|
+
/** The inventory kept for `peer` in a map of them, or an empty one. */
|
|
75
|
+
export declare function inventoryOf(inventories: Readonly<Record<string, Inventory>>, peer: string): Inventory;
|
|
76
|
+
/**
|
|
77
|
+
* For `projectFor`: the viewer's own entry of a per-player map, and no
|
|
78
|
+
* one else's. Hidden entries are absent — not zeroed — so another
|
|
79
|
+
* player's inventory is never in the frames sent to you.
|
|
80
|
+
*/
|
|
81
|
+
export declare function onlyOwn<T>(byPeer: Readonly<Record<string, T>>, peer: string): Record<string, T>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bones of a player inventory: stacks of items, kept in the store's
|
|
3
|
+
* state, changed only by the host.
|
|
4
|
+
*
|
|
5
|
+
* An inventory is plain JSON — item id → count — so it rides in any
|
|
6
|
+
* store document, diffs like the rest of it, and needs no protocol of
|
|
7
|
+
* its own. These are pure functions: call them inside an action
|
|
8
|
+
* handler or the `onEvent` hook and `setState` the result. A change
|
|
9
|
+
* that breaks the rules throws, which inside a handler refuses the
|
|
10
|
+
* action and discards its writes.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // state: { inventories: Record<peer, Inventory>, … }
|
|
14
|
+
* onEvent: (event, context) => {
|
|
15
|
+
* if (event.type !== 'join') return;
|
|
16
|
+
* const inventories = { ...context.getState().inventories };
|
|
17
|
+
* inventories[event.peer] ??= addItems(emptyInventory(), 'potion', 3, rules);
|
|
18
|
+
* context.setState({ inventories });
|
|
19
|
+
* },
|
|
20
|
+
* actions: {
|
|
21
|
+
* drink: (_input, context) => {
|
|
22
|
+
* const inventories = { ...context.getState().inventories };
|
|
23
|
+
* inventories[context.peer] = removeItems(inventoryOf(inventories, context.peer), 'potion', 1);
|
|
24
|
+
* context.setState({ inventories });
|
|
25
|
+
* return { left: countItems(inventories[context.peer], 'potion') };
|
|
26
|
+
* },
|
|
27
|
+
* },
|
|
28
|
+
* projectFor: (state, { peer }) => ({ ...state, inventories: onlyOwn(state.inventories, peer) }),
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* **Deliberately not here yet:** trading between players (it needs both
|
|
32
|
+
* sides' consent and one atomic transfer), equipment slots, item
|
|
33
|
+
* metadata. Item ids are yours; what an item *is* stays game data.
|
|
34
|
+
*/
|
|
35
|
+
export class InventoryError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.name = 'InventoryError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Longest item id accepted. Ids are keys in a document every player may receive. */
|
|
44
|
+
export const MAX_ITEM_ID_CHARS = 64;
|
|
45
|
+
const EMPTY = Object.freeze({});
|
|
46
|
+
/** An inventory with nothing in it. */
|
|
47
|
+
export function emptyInventory() {
|
|
48
|
+
return EMPTY;
|
|
49
|
+
}
|
|
50
|
+
function checkItem(item) {
|
|
51
|
+
if (typeof item !== 'string' || item.length === 0 || item.length > MAX_ITEM_ID_CHARS || item === '__proto__') {
|
|
52
|
+
throw new InventoryError('invalid', `an item id is 1-${MAX_ITEM_ID_CHARS} characters, got ${JSON.stringify(item)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function checkCount(count) {
|
|
56
|
+
if (!Number.isSafeInteger(count) || count < 1) {
|
|
57
|
+
throw new InventoryError('invalid', `a count is a whole number of at least 1, got ${String(count)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function limitFor(item, rules) {
|
|
61
|
+
const max = rules?.maxCount;
|
|
62
|
+
if (max === undefined)
|
|
63
|
+
return Number.MAX_SAFE_INTEGER;
|
|
64
|
+
if (typeof max === 'number')
|
|
65
|
+
return max;
|
|
66
|
+
return Object.prototype.hasOwnProperty.call(max, item) ? max[item] : Number.MAX_SAFE_INTEGER;
|
|
67
|
+
}
|
|
68
|
+
/** How many of `item` it holds. */
|
|
69
|
+
export function countItems(inventory, item) {
|
|
70
|
+
if (inventory === undefined || !Object.prototype.hasOwnProperty.call(inventory, item))
|
|
71
|
+
return 0;
|
|
72
|
+
return inventory[item] ?? 0;
|
|
73
|
+
}
|
|
74
|
+
/** Whether it holds at least `count` of `item`. */
|
|
75
|
+
export function hasItems(inventory, item, count = 1) {
|
|
76
|
+
return countItems(inventory, item) >= count;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Add `count` of `item`. Throws `full` when that would break a rule;
|
|
80
|
+
* nothing is partly added.
|
|
81
|
+
*/
|
|
82
|
+
export function addItems(inventory, item, count, rules) {
|
|
83
|
+
checkItem(item);
|
|
84
|
+
checkCount(count);
|
|
85
|
+
const had = countItems(inventory, item);
|
|
86
|
+
const next = had + count;
|
|
87
|
+
if (!Number.isSafeInteger(next) || next > limitFor(item, rules)) {
|
|
88
|
+
throw new InventoryError('full', `no room for ${String(count)} more ${item}`);
|
|
89
|
+
}
|
|
90
|
+
if (had === 0 && rules?.maxKinds !== undefined && Object.keys(inventory).length >= rules.maxKinds) {
|
|
91
|
+
throw new InventoryError('full', `no room for another kind of item (${String(rules.maxKinds)} at most)`);
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze({ ...inventory, [item]: next });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Remove `count` of `item`. Throws `insufficient` when it holds fewer;
|
|
97
|
+
* nothing is partly removed. An item that reaches zero is gone.
|
|
98
|
+
*/
|
|
99
|
+
export function removeItems(inventory, item, count) {
|
|
100
|
+
checkItem(item);
|
|
101
|
+
checkCount(count);
|
|
102
|
+
const had = countItems(inventory, item);
|
|
103
|
+
if (had < count)
|
|
104
|
+
throw new InventoryError('insufficient', `has ${String(had)} ${item}, needs ${String(count)}`);
|
|
105
|
+
const next = { ...inventory };
|
|
106
|
+
if (had === count)
|
|
107
|
+
delete next[item];
|
|
108
|
+
else
|
|
109
|
+
next[item] = had - count;
|
|
110
|
+
return Object.freeze(next);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A validator for an inventory inside a store definition's `state`:
|
|
114
|
+
* accepts only whole positive counts under valid item ids, and the
|
|
115
|
+
* rules if given. Throws otherwise, as a definition validator should.
|
|
116
|
+
*/
|
|
117
|
+
export function parseInventory(value, rules) {
|
|
118
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
119
|
+
throw new InventoryError('invalid', 'an inventory is an object of item id → count');
|
|
120
|
+
}
|
|
121
|
+
let out = EMPTY;
|
|
122
|
+
for (const [item, count] of Object.entries(value)) {
|
|
123
|
+
if (typeof count !== 'number')
|
|
124
|
+
throw new InventoryError('invalid', `the count of ${item} is not a number`);
|
|
125
|
+
out = addItems(out, item, count, rules);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** The inventory kept for `peer` in a map of them, or an empty one. */
|
|
130
|
+
export function inventoryOf(inventories, peer) {
|
|
131
|
+
return Object.prototype.hasOwnProperty.call(inventories, peer) ? inventories[peer] : EMPTY;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* For `projectFor`: the viewer's own entry of a per-player map, and no
|
|
135
|
+
* one else's. Hidden entries are absent — not zeroed — so another
|
|
136
|
+
* player's inventory is never in the frames sent to you.
|
|
137
|
+
*/
|
|
138
|
+
export function onlyOwn(byPeer, peer) {
|
|
139
|
+
return Object.prototype.hasOwnProperty.call(byPeer, peer) ? { [peer]: byPeer[peer] } : {};
|
|
140
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice G, caller half — `joinStore`.
|
|
3
|
+
*
|
|
4
|
+
* The replica (slice D/F) decides what may be published; this decides
|
|
5
|
+
* nothing and carries frames, keeps the lease alive, and turns the
|
|
6
|
+
* protocol's correlation into the promises a page awaits.
|
|
7
|
+
*
|
|
8
|
+
* ## The lease is store machinery, never game code
|
|
9
|
+
*
|
|
10
|
+
* `alive` goes out every 20 s while the handle is live, because a page
|
|
11
|
+
* rendering a spectator view sends nothing and expiring it would be a
|
|
12
|
+
* defect (§1.6). The honest statement about the pair: a 20 s interval
|
|
13
|
+
* against a 60 s lease tolerates **one** lost renewal with margin, and
|
|
14
|
+
* the second is a race — the third renewal lands *at* the boundary
|
|
15
|
+
* before any scheduling delay is counted.
|
|
16
|
+
*
|
|
17
|
+
* ## Correlation, and what a timeout does not mean
|
|
18
|
+
*
|
|
19
|
+
* An action that was never submitted is known not to have executed. An
|
|
20
|
+
* action that was submitted and then timed out is `indeterminate`, and
|
|
21
|
+
* the store says exactly that rather than retrying: a silent resend is
|
|
22
|
+
* how one action becomes two.
|
|
23
|
+
*/
|
|
24
|
+
import { type StoreTransport } from './host.js';
|
|
25
|
+
import type { ActionSpec, Cancel, InputDisposition, InputSpec, ReadonlyState, StoreDefinition, StoreStatus } from './types.js';
|
|
26
|
+
import { type Hex } from './wire.js';
|
|
27
|
+
/** How often a live handle renews its lease (§1.6). */
|
|
28
|
+
export declare const ALIVE_INTERVAL_MS = 20000;
|
|
29
|
+
/** How long a request waits before it is a typed timeout (§2). */
|
|
30
|
+
export declare const REQUEST_DEADLINE_MS = 10000;
|
|
31
|
+
/**
|
|
32
|
+
* How long one TRANSITION call waits for its installation before it
|
|
33
|
+
* is a typed timeout (§2).
|
|
34
|
+
*
|
|
35
|
+
* A transition's answer is its INSTALLATION, never the request
|
|
36
|
+
* clock's — a snapshot still transferring at 10 s is a success in
|
|
37
|
+
* progress — but that exemption needs a fence of its own: an owner
|
|
38
|
+
* that answers every `resync`/`join` with a fresh `man` whose
|
|
39
|
+
* assembly never completes restarts the 10 s assembly deadline each
|
|
40
|
+
* time, and the `#abandon → #resync → man` loop would hold the call
|
|
41
|
+
* (and its `MAX_OUTSTANDING` slot) for ever. So every call carries
|
|
42
|
+
* this named wall-clock bound from its own start. A timed-out call
|
|
43
|
+
* removes only that waiter (`cancelWaiter`), and the slot goes with
|
|
44
|
+
* its last caller.
|
|
45
|
+
*/
|
|
46
|
+
export declare const TRANSITION_DEADLINE_MS = 60000;
|
|
47
|
+
/**
|
|
48
|
+
* How often the replica's own clock is driven.
|
|
49
|
+
*
|
|
50
|
+
* The assembly deadline is 10 s (`assembly.ts`), so a second is fine
|
|
51
|
+
* resolution and cheap: the tick does nothing at all unless an
|
|
52
|
+
* assembly is open and overdue.
|
|
53
|
+
*/
|
|
54
|
+
export declare const TICK_INTERVAL_MS = 1000;
|
|
55
|
+
/** Outstanding correlations per caller (§2). */
|
|
56
|
+
export declare const MAX_OUTSTANDING = 64;
|
|
57
|
+
export interface JoinStoreOptions<S extends object, A extends ActionSpec, I extends InputSpec> {
|
|
58
|
+
readonly definition: StoreDefinition<S, A, I>;
|
|
59
|
+
readonly transport: StoreTransport;
|
|
60
|
+
/** The node hosting the store. */
|
|
61
|
+
readonly host: string;
|
|
62
|
+
/** The audience this caller wants. */
|
|
63
|
+
readonly audience: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* WHICH store of this definition to join, when the host serves
|
|
66
|
+
* more than one. Defaults to the definition id, which is what a
|
|
67
|
+
* host serving a single store of it declares.
|
|
68
|
+
*
|
|
69
|
+
* Not `key`: that is the caller's opaque policy token, and two
|
|
70
|
+
* callers of ONE store carry different ones. This is the store's
|
|
71
|
+
* address, and it is on the wire because every host store on a
|
|
72
|
+
* node sees every frame and a `join` names no handle — without it
|
|
73
|
+
* the store that answers is whichever listener ran first.
|
|
74
|
+
*/
|
|
75
|
+
readonly store?: string;
|
|
76
|
+
/** The opaque key the host's policy reads. */
|
|
77
|
+
readonly key: string;
|
|
78
|
+
/**
|
|
79
|
+
* The interest set to start with: keys of the definition's `interest`
|
|
80
|
+
* collections (grid cells, rooms…). Only those entities are delivered.
|
|
81
|
+
* Omit it to receive every entity.
|
|
82
|
+
*/
|
|
83
|
+
readonly interest?: readonly string[];
|
|
84
|
+
readonly maxEventBytes: number;
|
|
85
|
+
readonly streamId?: string;
|
|
86
|
+
now?: () => number;
|
|
87
|
+
newQ?: () => Hex;
|
|
88
|
+
schedule?: (run: () => void, ms: number) => Cancel;
|
|
89
|
+
}
|
|
90
|
+
/** A joined replica: reads, actions, inputs, audience, teardown. */
|
|
91
|
+
export interface JoinedStoreHandle<S extends object, A extends ActionSpec, I extends InputSpec> {
|
|
92
|
+
getState(): ReadonlyState<S>;
|
|
93
|
+
subscribe(listener: (state: ReadonlyState<S>, previous: ReadonlyState<S>) => void): Cancel;
|
|
94
|
+
getStatus(): StoreStatus;
|
|
95
|
+
subscribeStatus(listener: (status: StoreStatus, previous: StoreStatus) => void): Cancel;
|
|
96
|
+
/** Resolves when a consistent view is installed. */
|
|
97
|
+
ready(): Promise<void>;
|
|
98
|
+
act<K extends keyof A & string>(name: K, input: A[K]['input']): Promise<A[K]['output']>;
|
|
99
|
+
input<K extends keyof I & string>(name: K, value: I[K]): InputDisposition;
|
|
100
|
+
setAudience(names: readonly string[]): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Replace the interest set. Entities entering it arrive and entities
|
|
103
|
+
* leaving it go in one change — the view never blanks. Resolves once
|
|
104
|
+
* the host has applied it.
|
|
105
|
+
*/
|
|
106
|
+
setInterest(keys: readonly string[]): Promise<void>;
|
|
107
|
+
/** The session was replaced: resume on the new one (§1.6). */
|
|
108
|
+
reconnect(): Promise<void>;
|
|
109
|
+
close(): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Join a store hosted by `host`.
|
|
113
|
+
*
|
|
114
|
+
* Returns as soon as the join is sent; `ready()` is what waits for a
|
|
115
|
+
* consistent view, because a caller that wants to render a loading
|
|
116
|
+
* state should not have to await the world first.
|
|
117
|
+
*/
|
|
118
|
+
export declare function joinStore<S extends object, A extends ActionSpec, I extends InputSpec>(options: JoinStoreOptions<S, A, I>): JoinedStoreHandle<S, A, I>;
|