@net-mesh/browser 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +666 -0
  5. package/dist/async-queue.d.ts +26 -0
  6. package/dist/async-queue.js +62 -0
  7. package/dist/credential.d.ts +70 -0
  8. package/dist/credential.js +95 -0
  9. package/dist/errors.d.ts +400 -0
  10. package/dist/errors.js +726 -0
  11. package/dist/events.d.ts +232 -0
  12. package/dist/events.js +298 -0
  13. package/dist/identity.d.ts +40 -0
  14. package/dist/identity.js +78 -0
  15. package/dist/index.bundle.js +2 -0
  16. package/dist/index.d.ts +38 -0
  17. package/dist/index.js +46 -0
  18. package/dist/leader/events.d.ts +111 -0
  19. package/dist/leader/events.js +116 -0
  20. package/dist/leader/index.d.ts +14 -0
  21. package/dist/leader/index.js +11 -0
  22. package/dist/leader/session.d.ts +539 -0
  23. package/dist/leader/session.js +770 -0
  24. package/dist/leader/wasm.d.ts +123 -0
  25. package/dist/leader/wasm.js +39 -0
  26. package/dist/lobby.d.ts +183 -0
  27. package/dist/lobby.js +504 -0
  28. package/dist/local.d.ts +80 -0
  29. package/dist/local.js +160 -0
  30. package/dist/net_leaf.d.ts +1494 -0
  31. package/dist/net_leaf.js +3753 -0
  32. package/dist/net_leaf_bg.wasm +0 -0
  33. package/dist/net_leaf_bg.wasm.d.ts +137 -0
  34. package/dist/netcode/client.d.ts +102 -0
  35. package/dist/netcode/client.js +296 -0
  36. package/dist/netcode/clock.d.ts +45 -0
  37. package/dist/netcode/clock.js +54 -0
  38. package/dist/netcode/host.d.ts +118 -0
  39. package/dist/netcode/host.js +276 -0
  40. package/dist/netcode/index.d.ts +19 -0
  41. package/dist/netcode/index.js +14 -0
  42. package/dist/netcode/interpolate.d.ts +50 -0
  43. package/dist/netcode/interpolate.js +100 -0
  44. package/dist/netcode/wire.d.ts +125 -0
  45. package/dist/netcode/wire.js +117 -0
  46. package/dist/node.d.ts +908 -0
  47. package/dist/node.js +1085 -0
  48. package/dist/org.d.ts +412 -0
  49. package/dist/org.js +567 -0
  50. package/dist/peer-driver.d.ts +117 -0
  51. package/dist/peer-driver.js +224 -0
  52. package/dist/store/assembly.d.ts +176 -0
  53. package/dist/store/assembly.js +325 -0
  54. package/dist/store/chunker.d.ts +83 -0
  55. package/dist/store/chunker.js +190 -0
  56. package/dist/store/core.d.ts +136 -0
  57. package/dist/store/core.js +477 -0
  58. package/dist/store/definition.d.ts +23 -0
  59. package/dist/store/definition.js +83 -0
  60. package/dist/store/errors.d.ts +77 -0
  61. package/dist/store/errors.js +94 -0
  62. package/dist/store/host.d.ts +247 -0
  63. package/dist/store/host.js +675 -0
  64. package/dist/store/index.d.ts +50 -0
  65. package/dist/store/index.js +40 -0
  66. package/dist/store/interest.d.ts +40 -0
  67. package/dist/store/interest.js +77 -0
  68. package/dist/store/inventory.d.ts +81 -0
  69. package/dist/store/inventory.js +140 -0
  70. package/dist/store/join.d.ts +118 -0
  71. package/dist/store/join.js +724 -0
  72. package/dist/store/json.d.ts +50 -0
  73. package/dist/store/json.js +283 -0
  74. package/dist/store/ledger.d.ts +187 -0
  75. package/dist/store/ledger.js +292 -0
  76. package/dist/store/owner.d.ts +616 -0
  77. package/dist/store/owner.js +1711 -0
  78. package/dist/store/patch.d.ts +70 -0
  79. package/dist/store/patch.js +166 -0
  80. package/dist/store/player.d.ts +65 -0
  81. package/dist/store/player.js +261 -0
  82. package/dist/store/replica.d.ts +187 -0
  83. package/dist/store/replica.js +740 -0
  84. package/dist/store/state.d.ts +42 -0
  85. package/dist/store/state.js +165 -0
  86. package/dist/store/types.d.ts +200 -0
  87. package/dist/store/types.js +25 -0
  88. package/dist/store/visibility.d.ts +118 -0
  89. package/dist/store/visibility.js +282 -0
  90. package/dist/store/wire.d.ts +309 -0
  91. package/dist/store/wire.js +834 -0
  92. package/dist/stream.d.ts +174 -0
  93. package/dist/stream.js +238 -0
  94. package/dist/three/index.d.ts +109 -0
  95. package/dist/three/index.js +130 -0
  96. package/dist/udp-probe.d.ts +159 -0
  97. package/dist/udp-probe.js +245 -0
  98. package/dist/wasm.d.ts +642 -0
  99. package/dist/wasm.js +98 -0
  100. package/dist/world/border.d.ts +84 -0
  101. package/dist/world/border.js +99 -0
  102. package/dist/world/handoff.d.ts +187 -0
  103. package/dist/world/handoff.js +222 -0
  104. package/dist/world/index.d.ts +13 -0
  105. package/dist/world/index.js +9 -0
  106. package/dist/world/link.d.ts +179 -0
  107. package/dist/world/link.js +476 -0
  108. package/dist/world/regions.d.ts +154 -0
  109. package/dist/world/regions.js +314 -0
  110. package/package.json +72 -0
@@ -0,0 +1,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 {};