@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,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-preserving state application.
|
|
3
|
+
*
|
|
4
|
+
* `getState()` promises that "unchanged subtrees retain identity"
|
|
5
|
+
* (design §4). That is not documentation — it is what lets a Three.js
|
|
6
|
+
* loop diff by reference instead of walking the scene every frame, and
|
|
7
|
+
* a naive apply (`JSON.parse` → replace the root) breaks it silently:
|
|
8
|
+
* every render sees a brand-new root, every selector fires, and the
|
|
9
|
+
* only symptom is that the demo feels heavy.
|
|
10
|
+
*
|
|
11
|
+
* So both write paths funnel through {@link reconcile}:
|
|
12
|
+
*
|
|
13
|
+
* - the owner's shallow merge ({@link mergeShallow}), and
|
|
14
|
+
* - a replica applying a full validated snapshot.
|
|
15
|
+
*
|
|
16
|
+
* A validator necessarily returns fresh objects, so validation always
|
|
17
|
+
* happens *first* and reconciliation restores identity afterwards.
|
|
18
|
+
* That composition is also what makes an equivalent resynchronization
|
|
19
|
+
* snapshot a no-op rather than a whole-world change.
|
|
20
|
+
*
|
|
21
|
+
* Everything reconcile emits is frozen. New nodes are frozen as they
|
|
22
|
+
* are built and unchanged nodes were frozen when they were first
|
|
23
|
+
* built, so the reachable graph is deeply immutable without a separate
|
|
24
|
+
* deep-freeze pass over unchanged data.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The reconciled form of `next`, reusing every part of `previous` that
|
|
28
|
+
* is structurally equal.
|
|
29
|
+
*
|
|
30
|
+
* Returns `previous` itself when the two are equivalent, so
|
|
31
|
+
* `reconcile(state, validate(raw)) === state` is the test for "this
|
|
32
|
+
* snapshot told us nothing new".
|
|
33
|
+
*/
|
|
34
|
+
export declare function reconcile<T>(previous: unknown, next: T): T;
|
|
35
|
+
/**
|
|
36
|
+
* The owner's transaction: a shallow merge at the root, reconciled.
|
|
37
|
+
*
|
|
38
|
+
* Returns `current` unchanged when the patch moves nothing, which is
|
|
39
|
+
* what makes "empty/unchanged updates do not create notifications or
|
|
40
|
+
* revisions" true at the source instead of at every notify site.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeShallow<S extends object>(current: S, patch: Partial<S>): S;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-preserving state application.
|
|
3
|
+
*
|
|
4
|
+
* `getState()` promises that "unchanged subtrees retain identity"
|
|
5
|
+
* (design §4). That is not documentation — it is what lets a Three.js
|
|
6
|
+
* loop diff by reference instead of walking the scene every frame, and
|
|
7
|
+
* a naive apply (`JSON.parse` → replace the root) breaks it silently:
|
|
8
|
+
* every render sees a brand-new root, every selector fires, and the
|
|
9
|
+
* only symptom is that the demo feels heavy.
|
|
10
|
+
*
|
|
11
|
+
* So both write paths funnel through {@link reconcile}:
|
|
12
|
+
*
|
|
13
|
+
* - the owner's shallow merge ({@link mergeShallow}), and
|
|
14
|
+
* - a replica applying a full validated snapshot.
|
|
15
|
+
*
|
|
16
|
+
* A validator necessarily returns fresh objects, so validation always
|
|
17
|
+
* happens *first* and reconciliation restores identity afterwards.
|
|
18
|
+
* That composition is also what makes an equivalent resynchronization
|
|
19
|
+
* snapshot a no-op rather than a whole-world change.
|
|
20
|
+
*
|
|
21
|
+
* Everything reconcile emits is frozen. New nodes are frozen as they
|
|
22
|
+
* are built and unchanged nodes were frozen when they were first
|
|
23
|
+
* built, so the reachable graph is deeply immutable without a separate
|
|
24
|
+
* deep-freeze pass over unchanged data.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The reconciled form of `next`, reusing every part of `previous` that
|
|
28
|
+
* is structurally equal.
|
|
29
|
+
*
|
|
30
|
+
* Returns `previous` itself when the two are equivalent, so
|
|
31
|
+
* `reconcile(state, validate(raw)) === state` is the test for "this
|
|
32
|
+
* snapshot told us nothing new".
|
|
33
|
+
*/
|
|
34
|
+
export function reconcile(previous, next) {
|
|
35
|
+
if (Object.is(previous, next))
|
|
36
|
+
return next;
|
|
37
|
+
if (Array.isArray(next)) {
|
|
38
|
+
const source = next;
|
|
39
|
+
if (!Array.isArray(previous))
|
|
40
|
+
return freezeArray(source, undefined);
|
|
41
|
+
const merged = freezeArray(source, previous);
|
|
42
|
+
// Same length and every element reused ⇒ the old array still
|
|
43
|
+
// describes the world, so keep its identity.
|
|
44
|
+
if (previous.length === source.length && sameElements(previous, merged)) {
|
|
45
|
+
return previous;
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
if (isPlainObject(next)) {
|
|
50
|
+
if (!isPlainObject(previous))
|
|
51
|
+
return freezeRecord(next, undefined);
|
|
52
|
+
const merged = freezeRecord(next, previous);
|
|
53
|
+
const nextKeys = Object.keys(next);
|
|
54
|
+
// Own keys only, and the same SET of them — not merely the same
|
|
55
|
+
// count. Reading `previous[key]` would answer for an inherited
|
|
56
|
+
// member: `previous['__proto__']` is `Object.prototype` on any
|
|
57
|
+
// ordinary object, so a `next` carrying that as an own data key
|
|
58
|
+
// compared equal to a prototype it never came from, and
|
|
59
|
+
// reconciliation concluded the update changed nothing. Equal
|
|
60
|
+
// cardinality with different keys hid it further: `{a:1}` against
|
|
61
|
+
// an own `{__proto__:{}}` is one key each.
|
|
62
|
+
if (nextKeys.length === Object.keys(previous).length &&
|
|
63
|
+
nextKeys.every(key => hasOwn(previous, key) && Object.is(previous[key], merged[key]))) {
|
|
64
|
+
return previous;
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
// Primitive, function, class instance or null: nothing to share.
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The owner's transaction: a shallow merge at the root, reconciled.
|
|
73
|
+
*
|
|
74
|
+
* Returns `current` unchanged when the patch moves nothing, which is
|
|
75
|
+
* what makes "empty/unchanged updates do not create notifications or
|
|
76
|
+
* revisions" true at the source instead of at every notify site.
|
|
77
|
+
*/
|
|
78
|
+
export function mergeShallow(current, patch) {
|
|
79
|
+
const keys = Object.keys(patch);
|
|
80
|
+
if (keys.length === 0)
|
|
81
|
+
return current;
|
|
82
|
+
let changed = false;
|
|
83
|
+
const draft = { ...current };
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
// The prior subtree is an OWN property or it is absent. Reading
|
|
86
|
+
// through the prototype made `__proto__` look like an existing
|
|
87
|
+
// subtree equal to the incoming one, so the merge reported no
|
|
88
|
+
// change and the owner's update vanished: no new root, no
|
|
89
|
+
// revision, no notification.
|
|
90
|
+
const before = ownValue(current, key);
|
|
91
|
+
const incoming = reconcile(before, patch[key]);
|
|
92
|
+
if (!Object.is(before, incoming)) {
|
|
93
|
+
// `defineProperty` for the reason `freezeRecord` uses it: a
|
|
94
|
+
// plain assignment of `__proto__` replaces this object's
|
|
95
|
+
// prototype rather than storing the caller's data. The spread
|
|
96
|
+
// above is already safe (it defines rather than assigns); this
|
|
97
|
+
// was the one that was not.
|
|
98
|
+
Object.defineProperty(draft, key, {
|
|
99
|
+
value: incoming,
|
|
100
|
+
enumerable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
configurable: true,
|
|
103
|
+
});
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!changed)
|
|
108
|
+
return current;
|
|
109
|
+
return Object.freeze(draft);
|
|
110
|
+
}
|
|
111
|
+
function isPlainObject(value) {
|
|
112
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
113
|
+
return false;
|
|
114
|
+
const proto = Object.getPrototypeOf(value);
|
|
115
|
+
return proto === Object.prototype || proto === null;
|
|
116
|
+
}
|
|
117
|
+
function sameElements(previous, merged) {
|
|
118
|
+
for (let i = 0; i < merged.length; i += 1) {
|
|
119
|
+
if (!Object.is(previous[i], merged[i]))
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
function freezeArray(next, previous) {
|
|
125
|
+
const out = new Array(next.length);
|
|
126
|
+
for (let i = 0; i < next.length; i += 1) {
|
|
127
|
+
out[i] = reconcile(previous?.[i], next[i]);
|
|
128
|
+
}
|
|
129
|
+
return Object.freeze(out);
|
|
130
|
+
}
|
|
131
|
+
/** Whether a record carries this key itself. */
|
|
132
|
+
function hasOwn(record, key) {
|
|
133
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
134
|
+
}
|
|
135
|
+
/** A record's own value for a key, or `undefined` when it has none. */
|
|
136
|
+
function ownValue(record, key) {
|
|
137
|
+
if (record === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
return hasOwn(record, key) ? record[key] : undefined;
|
|
140
|
+
}
|
|
141
|
+
function freezeRecord(next, previous) {
|
|
142
|
+
const out = {};
|
|
143
|
+
for (const key of Object.keys(next)) {
|
|
144
|
+
// `defineProperty`, never `out[key] = …`.
|
|
145
|
+
//
|
|
146
|
+
// A plain assignment of the key `__proto__` goes through
|
|
147
|
+
// `Object.prototype`'s legacy setter: it REPLACES this object's
|
|
148
|
+
// prototype instead of storing a property. A patch whose
|
|
149
|
+
// replacement *value* is the ordinary JSON data
|
|
150
|
+
// `{"__proto__":{…}}` — no forbidden path segment anywhere — then
|
|
151
|
+
// came out of a successful, validated reconciliation as `{}` whose
|
|
152
|
+
// prototype carried the data, so the published state both lost the
|
|
153
|
+
// key and answered `true` for a field it does not own. The
|
|
154
|
+
// null-prototype parser and the null-prototype patch draft do not
|
|
155
|
+
// help here: this is the last step, after validation, and it built
|
|
156
|
+
// an ordinary object.
|
|
157
|
+
Object.defineProperty(out, key, {
|
|
158
|
+
value: reconcile(ownValue(previous, key), next[key]),
|
|
159
|
+
enumerable: true,
|
|
160
|
+
writable: false,
|
|
161
|
+
configurable: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return Object.freeze(out);
|
|
165
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store's public type contract.
|
|
3
|
+
*
|
|
4
|
+
* These are the declarations from
|
|
5
|
+
* `docs/internal/plans/BROWSER_GAME_STORE_API_DESIGN.md` §2, moved into
|
|
6
|
+
* the package so they are compiled rather than merely written down — but
|
|
7
|
+
* compiled is all that binds them: the shipped handles are
|
|
8
|
+
* `HostedStoreHandle` (`host.ts`) and `JoinedStoreHandle` (`join.ts`), and
|
|
9
|
+
* `OperationOptions`, `StoreLimits`, `HostedStore` and `JoinedStore` below
|
|
10
|
+
* have no implementor (`HostedStoreHandle.setState(next)` full-replaces;
|
|
11
|
+
* `joinStore` returns `JoinedStoreHandle` synchronously with
|
|
12
|
+
* `act(name, input)` / `input(name, value)`, no options).
|
|
13
|
+
*
|
|
14
|
+
* Two of them encode a review disposition and must not be "simplified":
|
|
15
|
+
*
|
|
16
|
+
* - {@link HostedStore} has `setState`; {@link JoinedStore} does **not**.
|
|
17
|
+
* A replica cannot write, and that is a type-level fact, not a runtime
|
|
18
|
+
* check a caller can be talked out of.
|
|
19
|
+
* - The projection callback on the host returns a validated `S`, never a
|
|
20
|
+
* `Partial<S>`. Visibility is expressed **in the schema** — collections
|
|
21
|
+
* omit invisible entities, individually hidden fields are explicit
|
|
22
|
+
* `null` or a tagged value, and `empty()` means absence. A zero must
|
|
23
|
+
* never be readable as "you cannot see this".
|
|
24
|
+
*/
|
|
25
|
+
import type { StoreError } from './errors.js';
|
|
26
|
+
import type { Visibility } from './visibility.js';
|
|
27
|
+
/** Deeply-readonly view of validated plain state. */
|
|
28
|
+
export type ReadonlyState<T> = T extends readonly (infer V)[] ? readonly ReadonlyState<V>[] : T extends object ? {
|
|
29
|
+
readonly [K in keyof T]: ReadonlyState<T[K]>;
|
|
30
|
+
} : T;
|
|
31
|
+
/**
|
|
32
|
+
* A validator: total on accepted input, throwing otherwise.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately the shape of `schema.parse` so Zod/Valibot/hand-written
|
|
35
|
+
* checks are all usable without the store depending on any of them.
|
|
36
|
+
*/
|
|
37
|
+
export type Parse<T> = (value: unknown) => T;
|
|
38
|
+
/** A subscription disposer. Calling it more than once is harmless. */
|
|
39
|
+
export type Cancel = () => void;
|
|
40
|
+
/** Declared actions: acknowledged, validated request/response pairs. */
|
|
41
|
+
export type ActionSpec = Record<string, {
|
|
42
|
+
input: unknown;
|
|
43
|
+
output: unknown;
|
|
44
|
+
}>;
|
|
45
|
+
/** Declared latest-value inputs: coalesced intent, no per-call promise. */
|
|
46
|
+
export type InputSpec = Record<string, unknown>;
|
|
47
|
+
/** A reusable typed description of a store. Carries no state or connection. */
|
|
48
|
+
export interface StoreDefinition<S extends object, A extends ActionSpec, I extends InputSpec> {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly version: number;
|
|
51
|
+
readonly state: Parse<S>;
|
|
52
|
+
/**
|
|
53
|
+
* The value that represents **absence** — no visible entities, no
|
|
54
|
+
* visible fields. Not a plausible set of game values: a reader that
|
|
55
|
+
* cannot see the ship must not receive a ship pointing north.
|
|
56
|
+
*/
|
|
57
|
+
readonly empty: () => S;
|
|
58
|
+
readonly actions: {
|
|
59
|
+
readonly [K in keyof A]: {
|
|
60
|
+
readonly input: Parse<A[K]['input']>;
|
|
61
|
+
readonly output: Parse<A[K]['output']>;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
readonly inputs: {
|
|
65
|
+
readonly [K in keyof I]: Parse<I[K]>;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* What each player may see, declared: `'open'`, a preset, or path →
|
|
69
|
+
* rule. Enforced by the host after any hand-written projection. See
|
|
70
|
+
* `visibility.ts`.
|
|
71
|
+
*/
|
|
72
|
+
readonly visibility?: Visibility;
|
|
73
|
+
/**
|
|
74
|
+
* Interest management: for each top-level entity map, the key an
|
|
75
|
+
* entity is found under — a grid cell, a room, any string (`null`:
|
|
76
|
+
* always delivered). A replica that declares an interest set receives
|
|
77
|
+
* only the entities whose key is in it. Used by the host; a joiner may
|
|
78
|
+
* omit it.
|
|
79
|
+
*/
|
|
80
|
+
readonly interest?: {
|
|
81
|
+
readonly [collection: string]: (entity: never, id: string) => string | null;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Per-entity validators, for the top-level entity maps whose validity is
|
|
85
|
+
* **per entity**: collection name → a parser for ONE entity.
|
|
86
|
+
*
|
|
87
|
+
* What makes {@link HostedStoreHandle.setEntities} cheap. An entity
|
|
88
|
+
* write validates only the entities it changes, and it commits without
|
|
89
|
+
* running {@link StoreDefinition.state} over the untouched world. That
|
|
90
|
+
* is the whole-document validator, which costs O(world) on every commit.
|
|
91
|
+
*
|
|
92
|
+
* **A contract, not a hint.** Declaring a collection here says that
|
|
93
|
+
* `state`'s rule for that key is exactly "a record of entities, each
|
|
94
|
+
* passing this parser", with no cross-entity or cross-collection
|
|
95
|
+
* invariant. `setEntities` trusts that, so an invariant that `state`
|
|
96
|
+
* enforces across entities is not enforced on this path. Keep such
|
|
97
|
+
* invariants in actions, or write through `setState`.
|
|
98
|
+
*/
|
|
99
|
+
readonly entities?: {
|
|
100
|
+
readonly [collection: string]: Parse<unknown>;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** The top-level keys of `S` whose values are entity maps. */
|
|
104
|
+
export type EntityCollection<S extends object> = {
|
|
105
|
+
[K in keyof S & string]: S[K] extends Readonly<Record<string, unknown>> ? K : never;
|
|
106
|
+
}[keyof S & string];
|
|
107
|
+
/** One entity of collection `C` of `S`. */
|
|
108
|
+
export type EntityOf<S extends object, C extends keyof S> = S[C] extends Readonly<Record<string, infer E>> ? E : never;
|
|
109
|
+
/** Where a handle is in its lifecycle. Kept out of game state. */
|
|
110
|
+
export type StorePhase = 'connecting' | 'syncing' | 'ready' | 'reconnecting' | 'failed' | 'closed';
|
|
111
|
+
/** Readiness, separate from the state itself. */
|
|
112
|
+
export interface StoreStatus {
|
|
113
|
+
readonly phase: StorePhase;
|
|
114
|
+
/** Retained state is no longer known-current. Not "empty world". */
|
|
115
|
+
readonly stale: boolean;
|
|
116
|
+
readonly error: StoreError | null;
|
|
117
|
+
}
|
|
118
|
+
/** Equality and delivery options for a selector subscription. */
|
|
119
|
+
export interface SelectorOptions<T> {
|
|
120
|
+
readonly equality?: (a: T, b: T) => boolean;
|
|
121
|
+
readonly fireImmediately?: boolean;
|
|
122
|
+
}
|
|
123
|
+
/** The read surface both the owner and a replica expose. */
|
|
124
|
+
export interface StoreReader<S extends object> {
|
|
125
|
+
getState(): ReadonlyState<S>;
|
|
126
|
+
subscribe(listener: (state: ReadonlyState<S>, previous: ReadonlyState<S>) => void): Cancel;
|
|
127
|
+
subscribe<T>(selector: (state: ReadonlyState<S>) => T, listener: (selected: T, previous: T) => void, options?: SelectorOptions<T>): Cancel;
|
|
128
|
+
getStatus(): StoreStatus;
|
|
129
|
+
subscribeStatus(listener: (status: StoreStatus, previous: StoreStatus) => void): Cancel;
|
|
130
|
+
}
|
|
131
|
+
/** Per-call deadline and cancellation. */
|
|
132
|
+
export interface OperationOptions {
|
|
133
|
+
readonly timeoutMs?: number;
|
|
134
|
+
readonly signal?: AbortSignal;
|
|
135
|
+
}
|
|
136
|
+
/** What happened to a latest-value input locally. Not remote acceptance. */
|
|
137
|
+
export type InputDisposition = {
|
|
138
|
+
readonly type: 'queued' | 'replaced';
|
|
139
|
+
} | {
|
|
140
|
+
readonly type: 'dropped';
|
|
141
|
+
readonly reason: 'not-ready' | 'capacity';
|
|
142
|
+
};
|
|
143
|
+
/** An owner state update: a shallow patch, or a function producing one. */
|
|
144
|
+
export type StateUpdate<S extends object> = Partial<S> | ((state: ReadonlyState<S>) => Partial<S>);
|
|
145
|
+
/**
|
|
146
|
+
* The handle a handler gets for the duration of **one synchronous
|
|
147
|
+
* transaction**. Valid only during that transaction: returning,
|
|
148
|
+
* throwing or handing back a thenable invalidates it, so a retained
|
|
149
|
+
* context cannot write from a later microtask.
|
|
150
|
+
*/
|
|
151
|
+
export interface ActionContext<S extends object> {
|
|
152
|
+
/** The authenticated caller's 16-lowercase-hex mesh node id. */
|
|
153
|
+
readonly peer: string;
|
|
154
|
+
getState(): ReadonlyState<S>;
|
|
155
|
+
setState(update: StateUpdate<S>): void;
|
|
156
|
+
}
|
|
157
|
+
/** What the owner's `authorize` is asked to rule on. */
|
|
158
|
+
export type AccessRequest<A extends ActionSpec, I extends InputSpec> = {
|
|
159
|
+
readonly type: 'read';
|
|
160
|
+
readonly peer: string;
|
|
161
|
+
readonly audience: readonly string[];
|
|
162
|
+
} | {
|
|
163
|
+
[K in keyof A]: {
|
|
164
|
+
readonly type: 'action';
|
|
165
|
+
readonly peer: string;
|
|
166
|
+
readonly name: K;
|
|
167
|
+
readonly input: A[K]['input'];
|
|
168
|
+
};
|
|
169
|
+
}[keyof A] | {
|
|
170
|
+
[K in keyof I]: {
|
|
171
|
+
readonly type: 'input';
|
|
172
|
+
readonly peer: string;
|
|
173
|
+
readonly name: K;
|
|
174
|
+
readonly input: I[K];
|
|
175
|
+
};
|
|
176
|
+
}[keyof I];
|
|
177
|
+
/** Declared bounds. The host enforces its own; callers cannot raise them. */
|
|
178
|
+
export interface StoreLimits {
|
|
179
|
+
readonly maxSnapshotBytes?: number;
|
|
180
|
+
readonly maxMessageBytes?: number;
|
|
181
|
+
readonly maxPendingActions?: number;
|
|
182
|
+
readonly maxAudiences?: number;
|
|
183
|
+
}
|
|
184
|
+
/** The authoritative instance. The only handle with `setState`. */
|
|
185
|
+
export interface HostedStore<S extends object> extends StoreReader<S> {
|
|
186
|
+
readonly authority: string;
|
|
187
|
+
setState(update: StateUpdate<S>): void;
|
|
188
|
+
close(): Promise<void>;
|
|
189
|
+
}
|
|
190
|
+
/** A subscribed replica. Writes go through `actions` / `inputs`, never `setState`. */
|
|
191
|
+
export interface JoinedStore<S extends object, A extends ActionSpec, I extends InputSpec> extends StoreReader<S> {
|
|
192
|
+
readonly actions: {
|
|
193
|
+
readonly [K in keyof A]: (input: A[K]['input'], options?: OperationOptions) => Promise<A[K]['output']>;
|
|
194
|
+
};
|
|
195
|
+
readonly inputs: {
|
|
196
|
+
readonly [K in keyof I]: (input: I[K]) => InputDisposition;
|
|
197
|
+
};
|
|
198
|
+
setAudience(names: readonly string[], options?: OperationOptions): Promise<void>;
|
|
199
|
+
close(): Promise<void>;
|
|
200
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store's public type contract.
|
|
3
|
+
*
|
|
4
|
+
* These are the declarations from
|
|
5
|
+
* `docs/internal/plans/BROWSER_GAME_STORE_API_DESIGN.md` §2, moved into
|
|
6
|
+
* the package so they are compiled rather than merely written down — but
|
|
7
|
+
* compiled is all that binds them: the shipped handles are
|
|
8
|
+
* `HostedStoreHandle` (`host.ts`) and `JoinedStoreHandle` (`join.ts`), and
|
|
9
|
+
* `OperationOptions`, `StoreLimits`, `HostedStore` and `JoinedStore` below
|
|
10
|
+
* have no implementor (`HostedStoreHandle.setState(next)` full-replaces;
|
|
11
|
+
* `joinStore` returns `JoinedStoreHandle` synchronously with
|
|
12
|
+
* `act(name, input)` / `input(name, value)`, no options).
|
|
13
|
+
*
|
|
14
|
+
* Two of them encode a review disposition and must not be "simplified":
|
|
15
|
+
*
|
|
16
|
+
* - {@link HostedStore} has `setState`; {@link JoinedStore} does **not**.
|
|
17
|
+
* A replica cannot write, and that is a type-level fact, not a runtime
|
|
18
|
+
* check a caller can be talked out of.
|
|
19
|
+
* - The projection callback on the host returns a validated `S`, never a
|
|
20
|
+
* `Partial<S>`. Visibility is expressed **in the schema** — collections
|
|
21
|
+
* omit invisible entities, individually hidden fields are explicit
|
|
22
|
+
* `null` or a tagged value, and `empty()` means absence. A zero must
|
|
23
|
+
* never be readable as "you cannot see this".
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative visibility: say which parts of the state are secret, and
|
|
3
|
+
* the host removes them from every player's view before anything is
|
|
4
|
+
* sent.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* defineStore({
|
|
8
|
+
* …,
|
|
9
|
+
* visibility: {
|
|
10
|
+
* 'players.*.hand': 'owner', // only the player whose id is that key
|
|
11
|
+
* 'deck': 'nobody', // the host only
|
|
12
|
+
* 'deck.length': 'everyone', // …but everyone may count it
|
|
13
|
+
* 'waypoint': ['command'], // players reading the `command` audience
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Why this exists: a hand-written `project` fails SILENTLY — the game
|
|
19
|
+
* works and the secret is readable in the browser. Rules declared here
|
|
20
|
+
* are enforced by the SDK, after any hand-written projection, so code
|
|
21
|
+
* can narrow what a rule allows but never widen it.
|
|
22
|
+
*
|
|
23
|
+
* **The rules.**
|
|
24
|
+
*
|
|
25
|
+
* - A path is dot-separated keys; `*` matches any key of an object (or
|
|
26
|
+
* index of an array). `deck.length` — a path ending in `.length` —
|
|
27
|
+
* reveals only the length of an array its parent rule hides.
|
|
28
|
+
* - `'everyone'`, `'nobody'` (the host only), `'owner'` (the player
|
|
29
|
+
* whose peer id is the key matched by the path's FIRST `*` — the
|
|
30
|
+
* `ships[context.peer]` pattern needs no setup), or a list of
|
|
31
|
+
* audiences (any one of them suffices).
|
|
32
|
+
* - Unlisted paths are visible, as without rules. A rule covers
|
|
33
|
+
* everything beneath its path, and a deeper rule cannot re-open what
|
|
34
|
+
* a shallower one hid (except `.length`).
|
|
35
|
+
*
|
|
36
|
+
* **What hidden looks like.** An entry of a collection (a path ending
|
|
37
|
+
* in `*`) is removed. A field becomes the {@link HIDDEN} marker — never
|
|
38
|
+
* `0`, `""` or `null`, which a game would read as real values — so the
|
|
39
|
+
* state validator must accept it there: wrap that field's parser with
|
|
40
|
+
* {@link hiddenOr}. A counted array becomes an array of markers of the
|
|
41
|
+
* same length.
|
|
42
|
+
*
|
|
43
|
+
* **Presets**, for the common shapes: `'open'` (everyone sees
|
|
44
|
+
* everything — an explicit decision rather than an accident) and
|
|
45
|
+
* `'card-game'` (`players.*.hand` owner, `deck` nobody, `deck.length`
|
|
46
|
+
* everyone). A preset takes overrides: `{ preset: 'card-game',
|
|
47
|
+
* 'players.*.score': 'nobody' }`.
|
|
48
|
+
*
|
|
49
|
+
* **What it cannot do.** The host itself holds everything: a player who
|
|
50
|
+
* hosts can read every secret in their own tab. Games with real stakes
|
|
51
|
+
* need a host that is not a player.
|
|
52
|
+
*/
|
|
53
|
+
import type { Viewer } from './owner.js';
|
|
54
|
+
/** Who may see a path. */
|
|
55
|
+
export type VisibilityRule = 'everyone' | 'nobody' | 'owner' | readonly string[];
|
|
56
|
+
/** The presets. */
|
|
57
|
+
export type VisibilityPreset = 'card-game';
|
|
58
|
+
/** Path → rule. */
|
|
59
|
+
export type VisibilityRules = {
|
|
60
|
+
readonly [path: string]: VisibilityRule;
|
|
61
|
+
};
|
|
62
|
+
/** What `defineStore({ visibility })` accepts. */
|
|
63
|
+
export type Visibility = 'open' | VisibilityPreset | VisibilityRules | ({
|
|
64
|
+
readonly preset: VisibilityPreset;
|
|
65
|
+
} & {
|
|
66
|
+
readonly [path: string]: VisibilityRule | VisibilityPreset;
|
|
67
|
+
});
|
|
68
|
+
/** The marker a hidden field carries in a player's view. */
|
|
69
|
+
export interface Hidden {
|
|
70
|
+
readonly $hidden: true;
|
|
71
|
+
}
|
|
72
|
+
/** The one hidden value. Plain JSON, so it survives the wire. */
|
|
73
|
+
export declare const HIDDEN: Hidden;
|
|
74
|
+
/** Whether a value is the hidden marker (the original, or one parsed off the wire). */
|
|
75
|
+
export declare function isHidden(value: unknown): value is Hidden;
|
|
76
|
+
/** Wrap a field's parser so the field may also be {@link HIDDEN}. */
|
|
77
|
+
export declare function hiddenOr<T>(parse: (value: unknown) => T): (value: unknown) => T | Hidden;
|
|
78
|
+
interface CompiledRule {
|
|
79
|
+
readonly path: string;
|
|
80
|
+
readonly segments: readonly string[];
|
|
81
|
+
readonly rule: VisibilityRule;
|
|
82
|
+
/** A `.length` rule: reveals the count of what its parent hides. */
|
|
83
|
+
readonly counts: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** Rules ready to apply. `'open'` compiles to no rules. */
|
|
86
|
+
export interface CompiledVisibility {
|
|
87
|
+
readonly rules: readonly CompiledRule[];
|
|
88
|
+
/** Whether any rule depends on who is looking — projections are then per player. */
|
|
89
|
+
readonly perPlayer: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check and compile a visibility declaration. Throws `invalid-data` for
|
|
93
|
+
* anything that could not be enforced as written — at definition time,
|
|
94
|
+
* not at the first projection.
|
|
95
|
+
*/
|
|
96
|
+
export declare function compileVisibility(visibility: Visibility): CompiledVisibility;
|
|
97
|
+
/**
|
|
98
|
+
* Apply compiled rules for one viewer (`null`: nobody in particular —
|
|
99
|
+
* sees only what `'everyone'` may).
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyVisibility<S>(compiled: CompiledVisibility, state: S, viewer: Viewer | null): S;
|
|
102
|
+
/**
|
|
103
|
+
* The view a viewer would receive under a definition's declared rules,
|
|
104
|
+
* for tests and tools. (A host also applies its hand-written `project`
|
|
105
|
+
* / `projectFor` first; this is the rules alone.)
|
|
106
|
+
*/
|
|
107
|
+
export declare function projectVisible<S>(definition: {
|
|
108
|
+
readonly visibility?: Visibility;
|
|
109
|
+
}, state: S, viewer: Viewer | null): S;
|
|
110
|
+
/**
|
|
111
|
+
* Throw if `viewer` could read any of `paths` (concrete dot paths, e.g.
|
|
112
|
+
* `players.bob.hand`) under the definition's rules. Absent, or the
|
|
113
|
+
* hidden marker, is hidden; anything else is a leak.
|
|
114
|
+
*/
|
|
115
|
+
export declare function assertHidden<S>(definition: {
|
|
116
|
+
readonly visibility?: Visibility;
|
|
117
|
+
}, state: S, viewer: Viewer | null, paths: readonly string[]): void;
|
|
118
|
+
export {};
|