@irtio/schema 0.1.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.
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Type builders and runtime type descriptors.
3
+ *
4
+ * The type menu is exactly: bool u8 u16 u32 i32 f32 f64 str(max) enumOf(...) ref(entity)
5
+ * list(T, max) struct({...}) plus `.opt` and `.default(v)` chainable on any of them.
6
+ * Nothing else — the menu is deliberately closed.
7
+ */
8
+ type ScalarKind = 'bool' | 'u8' | 'u16' | 'u32' | 'i32' | 'f32' | 'f64';
9
+ /** Common flags on every descriptor. */
10
+ interface DescBase {
11
+ /** `.opt` — value may be `undefined`; encoded with a presence bit. */
12
+ readonly opt: boolean;
13
+ /** `.default(v)` was called. */
14
+ readonly hasDefault: boolean;
15
+ readonly default?: unknown;
16
+ }
17
+ type TypeDesc = (DescBase & {
18
+ readonly kind: ScalarKind;
19
+ }) | (DescBase & {
20
+ readonly kind: 'str';
21
+ readonly max: number;
22
+ }) | (DescBase & {
23
+ readonly kind: 'enum';
24
+ readonly values: readonly string[];
25
+ }) | (DescBase & {
26
+ readonly kind: 'ref';
27
+ readonly entity: string;
28
+ }) | (DescBase & {
29
+ readonly kind: 'list';
30
+ readonly item: TypeDesc;
31
+ readonly max: number;
32
+ }) | (DescBase & {
33
+ readonly kind: 'struct';
34
+ readonly fields: StructFieldDescs;
35
+ });
36
+ /** Struct fields keep declaration order (field index on the wire). */
37
+ type StructFieldDescs = Readonly<Record<string, TypeDesc>>;
38
+ type Kind = TypeDesc['kind'];
39
+ /**
40
+ * A schema type. `T` is the inferred plain TS type; `D` is `true` once `.default()` was called
41
+ * (it makes the field optional at `add()`/init time).
42
+ */
43
+ interface Type<T, D extends boolean = false> {
44
+ readonly desc: TypeDesc;
45
+ /** Makes the value optional (`T | undefined`). */
46
+ readonly opt: Type<T | undefined, D>;
47
+ /** Declares a default; used by migrations (additive new field) and by `add()`/init. */
48
+ default(value: T): Type<T, true>;
49
+ /** Phantom; never set at runtime. */
50
+ readonly __type?: T;
51
+ readonly __hasDefault?: D;
52
+ }
53
+ type AnyType = Type<any, boolean>;
54
+ type Fields = Readonly<Record<string, AnyType>>;
55
+ declare const bool: Type<boolean>;
56
+ declare const u8: Type<number>;
57
+ declare const u16: Type<number>;
58
+ declare const u32: Type<number>;
59
+ declare const i32: Type<number>;
60
+ declare const f32: Type<number>;
61
+ declare const f64: Type<number>;
62
+ /** String with a maximum length in UTF-8 bytes. Oversize values throw on encode. */
63
+ declare function str(max: number): Type<string>;
64
+ /** Enum encoded as a u8 index in declaration order (appending is additive; reordering is breaking). */
65
+ declare function enumOf<const V extends readonly [string, ...string[]]>(...values: V): Type<V[number]>;
66
+ /** Reference to an entity instance by id; encoded as the id string. */
67
+ declare function ref<E extends string>(entity: E): Type<string>;
68
+ /** Bounded list, whole-replace on the wire. */
69
+ declare function list<T, D extends boolean>(item: Type<T, D>, max: number): Type<T[]>;
70
+ /** Nested object; fields are tracked and encoded individually. */
71
+ declare function struct<F extends Fields>(fields: F): Type<InferFields<F>>;
72
+ type Simplify<T> = {
73
+ [K in keyof T]: T[K];
74
+ } & {};
75
+ /** Plain TS type for a builder type or a record of fields. */
76
+ type Infer<X> = X extends Type<infer T, any> ? T : X extends Fields ? InferFields<X> : never;
77
+ type OptKeys<F extends Fields> = {
78
+ [K in keyof F]: undefined extends Infer<F[K]> ? K : never;
79
+ }[keyof F];
80
+ /** Plain object type for a record of fields: `.opt` fields become `k?: T | undefined`. */
81
+ type InferFields<F extends Fields> = Simplify<{
82
+ [K in Exclude<keyof F, OptKeys<F>>]: Infer<F[K]>;
83
+ } & {
84
+ [K in OptKeys<F>]?: Infer<F[K]> | undefined;
85
+ }>;
86
+ type InitOptKeys<F extends Fields> = {
87
+ [K in keyof F]: undefined extends Infer<F[K]> ? K : F[K] extends Type<any, true> ? K : never;
88
+ }[keyof F];
89
+ /** Init shape for `add()` / singleton init: `.opt` and `.default()` fields may be omitted. */
90
+ type InitFields<F extends Fields> = Simplify<{
91
+ [K in Exclude<keyof F, InitOptKeys<F>>]: Infer<F[K]>;
92
+ } & {
93
+ [K in InitOptKeys<F>]?: Infer<F[K]> | undefined;
94
+ }>;
95
+ declare function describeType(d: TypeDesc): string;
96
+ /** Entity ids are strings of at most this many UTF-8 bytes (`str(32)` on the wire). */
97
+ declare const ID_MAX_BYTES = 32;
98
+ /**
99
+ * Validates a plain value against a descriptor. Returns an error message or `null`.
100
+ * Used by `.default()`, by the codec on encode (throws), and later by RPC param validation.
101
+ */
102
+ declare function validateValue(d: TypeDesc, v: unknown, path?: string): string | null;
103
+ /** Zero/default value for a descriptor (after `.default`, else the type's zero; `.opt` → undefined). */
104
+ declare function defaultValue(d: TypeDesc): unknown;
105
+ /** Deep-clones a plain value (arrays + plain objects + primitives). */
106
+ declare function cloneValue<T>(v: T): T;
107
+ /**
108
+ * Normalizes a plain value to what it will look like after a wire round-trip: fills missing
109
+ * defaulted fields, applies `Math.fround` to f32, validates integers/sizes. Throws on invalid input.
110
+ * Returns a fresh value (never aliases `v` for lists/structs).
111
+ */
112
+ declare function normalizeValue(d: TypeDesc, v: unknown, path?: string): unknown;
113
+
114
+ /**
115
+ * `entity`, `singleton`, `server`, `client`, `defineSchema`, and the compiled schema
116
+ * (ordered collection descriptors, sorted RPC table, canonical form, hash).
117
+ */
118
+
119
+ type Visibility = 'all' | 'role' | 'spatial-grid';
120
+ interface EntityOptions {
121
+ /** `true` → never client-owned; `DeepReadonly` on the client at compile time. */
122
+ readonly serverOwned?: boolean;
123
+ /** `'all'` (default) | `'role'` (per-role views, needs `roles`) | `'spatial-grid'` (accepted by the types, rejected at deploy — not yet supported). */
124
+ readonly visibility?: Visibility;
125
+ /** With `visibility: 'role'`: the roles that see this collection. */
126
+ readonly roles?: readonly string[];
127
+ }
128
+ interface EntityDef<F extends Fields = Fields, O extends EntityOptions = EntityOptions> {
129
+ readonly kind: 'entity';
130
+ readonly fields: F;
131
+ readonly options: O;
132
+ }
133
+ interface SingletonDef<F extends Fields = Fields, O extends EntityOptions = EntityOptions> {
134
+ readonly kind: 'singleton';
135
+ readonly fields: F;
136
+ readonly options: O;
137
+ }
138
+ type AnyDef = EntityDef<any, any> | SingletonDef<any, any>;
139
+ type DefMap = Readonly<Record<string, AnyDef>>;
140
+ /** A keyed collection of instances, each with an owner (client id or the server). */
141
+ declare function entity<F extends Fields>(fields: F): EntityDef<F, {}>;
142
+ declare function entity<F extends Fields, const O extends EntityOptions>(fields: F, options: O): EntityDef<F, O>;
143
+ /** Exactly one instance; same `serverOwned`/`visibility` options. */
144
+ declare function singleton<F extends Fields>(fields: F): SingletonDef<F, {}>;
145
+ declare function singleton<F extends Fields, const O extends EntityOptions>(fields: F, options: O): SingletonDef<F, O>;
146
+ type RpcDirection = 'server' | 'client';
147
+ interface RpcDef<Dir extends RpcDirection = RpcDirection, P extends Fields = Fields, R extends Fields | undefined = Fields | undefined> {
148
+ readonly kind: 'rpc';
149
+ readonly direction: Dir;
150
+ readonly params: P;
151
+ readonly returns: R;
152
+ }
153
+ type AnyRpc = RpcDef<RpcDirection, any, any>;
154
+ type RpcMap = Readonly<Record<string, AnyRpc>>;
155
+ interface RpcSpec<P extends Fields, R extends Fields | undefined> {
156
+ readonly params?: P;
157
+ readonly returns?: R;
158
+ }
159
+ /** Client → server call, implemented in the room file. */
160
+ declare function server(): RpcDef<'server', {}, undefined>;
161
+ declare function server<P extends Fields = {}>(spec: {
162
+ readonly params?: P;
163
+ readonly returns?: undefined;
164
+ }): RpcDef<'server', P, undefined>;
165
+ declare function server<P extends Fields = {}, R extends Fields = Fields>(spec: {
166
+ readonly params?: P;
167
+ readonly returns: R;
168
+ }): RpcDef<'server', P, R>;
169
+ /** Server → client call, implemented in the game client. */
170
+ declare function client(): RpcDef<'client', {}, undefined>;
171
+ declare function client<P extends Fields = {}>(spec: {
172
+ readonly params?: P;
173
+ readonly returns?: undefined;
174
+ }): RpcDef<'client', P, undefined>;
175
+ declare function client<P extends Fields = {}, R extends Fields = Fields>(spec: {
176
+ readonly params?: P;
177
+ readonly returns: R;
178
+ }): RpcDef<'client', P, R>;
179
+ /** RPC names a builder may not declare (built-ins live in `@irtio/protocol`). */
180
+ declare const RESERVED_RPC_NAMES: readonly string[];
181
+ /** Collection names a builder may not declare (the runtime merges built-in state under them). */
182
+ declare const RESERVED_COLLECTION_NAMES: readonly string[];
183
+ interface FieldDesc {
184
+ readonly name: string;
185
+ /** Declaration index = wire index. */
186
+ readonly index: number;
187
+ readonly type: TypeDesc;
188
+ }
189
+ interface CollectionDesc {
190
+ readonly name: string;
191
+ /** Index in `schema.collections` (sorted by name) = wire index. */
192
+ readonly index: number;
193
+ readonly kind: 'entity' | 'singleton';
194
+ readonly fields: readonly FieldDesc[];
195
+ readonly fieldIndex: ReadonlyMap<string, number>;
196
+ readonly serverOwned: boolean;
197
+ readonly visibility: Visibility;
198
+ readonly roles: readonly string[] | undefined;
199
+ }
200
+ interface RpcDesc {
201
+ readonly name: string;
202
+ /** Index in `schema.rpcs` (sorted by name). Built-ins are appended by `@irtio/protocol`. */
203
+ readonly index: number;
204
+ readonly direction: RpcDirection;
205
+ readonly params: readonly FieldDesc[];
206
+ /** `undefined` = void. */
207
+ readonly returns: readonly FieldDesc[] | undefined;
208
+ }
209
+ interface SchemaOptions<Rpc extends RpcMap, Roles extends readonly string[]> {
210
+ readonly rpc?: Rpc;
211
+ readonly roles?: Roles;
212
+ /** The public project id, written by `irtio init`. Identity, not shape: not part of the hash. */
213
+ readonly project?: string;
214
+ /** Internal: lets `@irtio/protocol` build the runtime-extended schema with built-in collections. */
215
+ readonly allowReservedNames?: boolean;
216
+ }
217
+ interface Schema<Defs extends DefMap = DefMap, Rpc extends RpcMap = RpcMap, Roles extends readonly string[] = readonly string[]> {
218
+ readonly defs: Defs;
219
+ readonly rpc: Rpc;
220
+ readonly roles: Roles;
221
+ readonly project: string | undefined;
222
+ /** Collections sorted by name; index = wire index. */
223
+ readonly collections: readonly CollectionDesc[];
224
+ readonly collection: (name: string) => CollectionDesc;
225
+ /** Builder RPCs sorted by name; index = wire `rpcId` (protocol appends built-ins after). */
226
+ readonly rpcs: readonly RpcDesc[];
227
+ /** Canonical JSON (sorted keys, `canonicalVersion: 1`). */
228
+ readonly canonical: string;
229
+ /** SHA-256 of the canonical form, hex. */
230
+ readonly hash: string;
231
+ /** First 8 bytes of the hash (HELLO / snapshot / delta header). */
232
+ readonly hash8: Uint8Array;
233
+ }
234
+ type AnySchema = Schema<any, any, any>;
235
+ declare function defineSchema<const Defs extends DefMap, const Rpc extends RpcMap = {}, const Roles extends readonly string[] = readonly string[]>(defs: Defs, options?: SchemaOptions<Rpc, Roles>): Schema<Defs, Rpc, Roles>;
236
+ declare const CANONICAL_VERSION = 1;
237
+ declare function canonicalType(d: TypeDesc): Record<string, unknown>;
238
+ /**
239
+ * JSON.stringify with object keys sorted at every level (arrays keep order). Keys whose value is
240
+ * `undefined` are dropped, exactly as `JSON.stringify` drops them — without that the canonical
241
+ * form can come out as text that `JSON.parse` rejects.
242
+ */
243
+ declare function stableStringify(v: unknown): string;
244
+ interface SchemaIssue {
245
+ readonly level: 'error' | 'warning';
246
+ readonly message: string;
247
+ }
248
+ /** Checks a schema for things accepted by the types/hash but not currently deployable. */
249
+ declare function validateForDeploy(schema: AnySchema): SchemaIssue[];
250
+
251
+ /**
252
+ * `schemaFromCanonical` — rebuild a runnable schema from its canonical JSON.
253
+ *
254
+ * A deployment stores `schema.canonical`. Waking a room that was hibernated under an older
255
+ * deployment means decoding its snapshot with *that* deployment's schema — and the old
256
+ * TypeScript source is long gone. The canonical form is complete by construction
257
+ * (`canonicalVersion: 1` carries every collection, field descriptor, rpc and role that feeds
258
+ * the hash), so it can be replayed back through the same builders the DSL uses.
259
+ *
260
+ * The invariant that makes this trustworthy: `schemaFromCanonical(s.canonical).canonical === s.canonical`
261
+ * (and therefore the same hash, the same wire indices, and byte-identical codecs). It is checked
262
+ * here on every call — a mismatch is a bug in this file, not a caller error, and failing loudly
263
+ * beats decoding a snapshot with a subtly wrong schema.
264
+ */
265
+
266
+ interface FromCanonicalOptions {
267
+ /**
268
+ * The public project id. Identity, not shape — it is deliberately not part of the canonical
269
+ * form, so a caller that needs `schema.project` populated must pass it back in.
270
+ */
271
+ readonly project?: string;
272
+ }
273
+ /**
274
+ * Reconstructs a runnable schema from canonical JSON. Throws with a pointed message on anything
275
+ * malformed; the caller (deploy, the tenant wake path) treats that as a corrupt deployment.
276
+ */
277
+ declare function schemaFromCanonical(json: string, options?: FromCanonicalOptions): AnySchema;
278
+
279
+ /**
280
+ * Plain (untracked) entity collections and state objects. `track()` wraps these; the codec
281
+ * reads/writes them. Owners live beside values.
282
+ */
283
+
284
+ /** Owner sentinel for "the server". */
285
+ declare const SERVER_OWNER = "";
286
+ /** The implicit id used for singletons in dirty sets and on the wire. */
287
+ declare const SINGLETON_ID = "";
288
+ interface AddOptions {
289
+ readonly owner?: string;
290
+ }
291
+ interface ReadonlyCollection<T> {
292
+ get(id: string): T | undefined;
293
+ has(id: string): boolean;
294
+ ownerOf(id: string): string | undefined;
295
+ readonly size: number;
296
+ ids(): IterableIterator<string>;
297
+ [Symbol.iterator](): IterableIterator<readonly [string, T]>;
298
+ }
299
+ interface Collection<T, Init = T> extends ReadonlyCollection<T> {
300
+ /** Adds (or replaces) an instance. Missing `.opt`/`.default()` fields are filled. */
301
+ add(id: string, values: Init, options?: AddOptions): T;
302
+ remove(id: string): boolean;
303
+ setOwner(id: string, owner: string): void;
304
+ [Symbol.iterator](): IterableIterator<readonly [string, T]>;
305
+ }
306
+ interface Record_<T> {
307
+ owner: string;
308
+ value: T;
309
+ }
310
+ /** Plain collection backed by a Map. Values are normalized (defaults filled, f32 frounded) on add. */
311
+ declare class EntityCollection<T extends object = any, Init = T> implements Collection<T, Init> {
312
+ readonly desc: CollectionDesc;
313
+ readonly records: Map<string, Record_<T>>;
314
+ constructor(desc: CollectionDesc);
315
+ add(id: string, values: Init, options?: AddOptions): T;
316
+ remove(id: string): boolean;
317
+ get(id: string): T | undefined;
318
+ has(id: string): boolean;
319
+ setOwner(id: string, owner: string): void;
320
+ ownerOf(id: string): string | undefined;
321
+ get size(): number;
322
+ ids(): IterableIterator<string>;
323
+ [Symbol.iterator](): IterableIterator<readonly [string, T]>;
324
+ }
325
+ /** Builds a full record from init values: every field present, defaults applied, f32 frounded. */
326
+ declare function normalizeRecord(desc: CollectionDesc, values: Record<string, unknown>): Record<string, unknown>;
327
+ /** A record with every field at its default. */
328
+ declare function defaultRecord(desc: CollectionDesc): Record<string, unknown>;
329
+ /** Plain state: `{ [collectionName]: EntityCollection | singletonObject }`. */
330
+ type PlainState = Record<string, EntityCollection | Record<string, unknown>>;
331
+ /**
332
+ * Creates an empty plain state for a schema: empty collections, singletons at their defaults.
333
+ * The result is what `track()`, the codec, and tests operate on.
334
+ */
335
+ declare function createState<S extends AnySchema>(schema: S, init?: Partial<Record<string, Record<string, unknown>>>): PlainState;
336
+
337
+ /**
338
+ * Type-level inference over a schema: `State`, `ClientState`, per-role views, `Owned`,
339
+ * `ServerRpcs` / `ClientRpcs` / `Implementations`, and call-proxy signatures.
340
+ *
341
+ * Kept to simple mapped types so a 30-entity schema type-checks fast (see test/large.test-d.ts).
342
+ */
343
+
344
+ type DeepReadonly<T> = T extends (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? {
345
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
346
+ } : T;
347
+ type DeepWritable<T> = T extends readonly (infer U)[] ? DeepWritable<U>[] : T extends object ? {
348
+ -readonly [K in keyof T]: DeepWritable<T[K]>;
349
+ } : T;
350
+ /** The writable view of an instance the client owns (the client SDK hands these out). */
351
+ type Owned<T> = DeepWritable<T>;
352
+ type SchemaDefs<S> = S extends Schema<infer D, any, any> ? D : never;
353
+ type SchemaRpc<S> = S extends Schema<any, infer R, any> ? R : never;
354
+ type SchemaRoles<S> = S extends Schema<any, any, infer R> ? R : never;
355
+ type RoleOf<S> = SchemaRoles<S>[number];
356
+ /** Plain instance type of an entity/singleton definition. */
357
+ type InstanceOf<D> = D extends EntityDef<infer F, any> ? InferFields<F> : D extends SingletonDef<infer F, any> ? InferFields<F> : never;
358
+ /** Init type (`add()` values) of an entity definition. */
359
+ type InitOf<D> = D extends EntityDef<infer F, any> ? InitFields<F> : D extends SingletonDef<infer F, any> ? InitFields<F> : never;
360
+ type DefOptions<D> = D extends EntityDef<any, infer O> ? O : D extends SingletonDef<any, infer O> ? O : never;
361
+ type State<S> = {
362
+ [K in keyof SchemaDefs<S>]: SchemaDefs<S>[K] extends EntityDef<infer F, any> ? Collection<InferFields<F>, InitFields<F>> : SchemaDefs<S>[K] extends SingletonDef<infer F, any> ? InferFields<F> : never;
363
+ };
364
+ type RolesOf<O extends EntityOptions> = O extends {
365
+ roles: readonly (infer R)[];
366
+ } ? R : never;
367
+ /** Is collection with options `O` visible to `Role`? */
368
+ type VisibleTo<O extends EntityOptions, Role extends string> = O extends {
369
+ visibility: 'role';
370
+ } ? [Role] extends [RolesOf<O>] ? true : false : true;
371
+ type VisibleKeys<S, Role extends string> = {
372
+ [K in keyof SchemaDefs<S>]: VisibleTo<DefOptions<SchemaDefs<S>[K]>, Role> extends true ? K : never;
373
+ }[keyof SchemaDefs<S>];
374
+ /**
375
+ * The client's view of the state for `Role`. `serverOwned` collections and singletons are
376
+ * `DeepReadonly`; instance-owned collections hand out `DeepReadonly` instances by default
377
+ * (the client SDK uses `Owned<T>` for instances this client owns).
378
+ */
379
+ type ClientState<S, Role extends RoleOf<S> & string = RoleOf<S> & string> = Simplify<{
380
+ [K in VisibleKeys<S, Role>]: SchemaDefs<S>[K] extends EntityDef<infer F, any> ? ReadonlyCollection<DeepReadonly<InferFields<F>>> : SchemaDefs<S>[K] extends SingletonDef<infer F, any> ? DeepReadonly<InferFields<F>> : never;
381
+ }>;
382
+ /** Collections the client can never own (compile-time `serverOwned: true`). */
383
+ type ServerOwnedKeys<S> = {
384
+ [K in keyof SchemaDefs<S>]: DefOptions<SchemaDefs<S>[K]> extends {
385
+ serverOwned: true;
386
+ } ? K : never;
387
+ }[keyof SchemaDefs<S>];
388
+ /** Entity collections whose instances a client may own. */
389
+ type OwnableKeys<S> = {
390
+ [K in keyof SchemaDefs<S>]: SchemaDefs<S>[K] extends EntityDef<any, infer O> ? O extends {
391
+ serverOwned: true;
392
+ } ? never : K : never;
393
+ }[keyof SchemaDefs<S>];
394
+ type RpcParams<D> = D extends RpcDef<any, infer P, any> ? InferFields<P> : never;
395
+ type RpcReturns<D> = D extends RpcDef<any, any, infer R> ? R extends Fields ? InferFields<R> : void : never;
396
+ /** Builder RPCs implemented on the server (client → server). Excludes built-ins. */
397
+ type ServerRpcs<R extends RpcMap> = {
398
+ [K in keyof R as R[K] extends RpcDef<'server', any, any> ? K : never]: R[K];
399
+ };
400
+ /** Builder RPCs implemented on the client (server → client). */
401
+ type ClientRpcs<R extends RpcMap> = {
402
+ [K in keyof R as R[K] extends RpcDef<'client', any, any> ? K : never]: R[K];
403
+ };
404
+ /** Exhaustive implementation map: every key required; extra keys rejected (object literals). */
405
+ type Implementations<Rpcs extends Record<string, AnyRpc>, StateT, Ctx> = {
406
+ [K in keyof Rpcs]: (state: StateT, params: RpcParams<Rpcs[K]>, ctx: Ctx) => RpcReturns<Rpcs[K]>;
407
+ };
408
+ /** Client-side implementations (server → client): params only, may return a Promise. */
409
+ type ClientImplementations<Rpcs extends Record<string, AnyRpc>> = {
410
+ [K in keyof Rpcs]: (params: RpcParams<Rpcs[K]>) => RpcReturns<Rpcs[K]> | Promise<RpcReturns<Rpcs[K]>>;
411
+ };
412
+ type IsEmptyParams<D> = D extends RpcDef<any, infer P, any> ? keyof P extends never ? true : false : false;
413
+ type CallFn<D> = IsEmptyParams<D> extends true ? (params?: RpcParams<D>) => Promise<RpcReturns<D>> : (params: RpcParams<D>) => Promise<RpcReturns<D>>;
414
+ /** `room.call.<rpc>(params)` on the client: every server RPC. */
415
+ type ClientCallProxy<R extends RpcMap> = {
416
+ [K in keyof ServerRpcs<R>]: CallFn<R[K]>;
417
+ };
418
+ /** `room.call(clientId).<rpc>(params)` on the server: every client RPC. */
419
+ type ServerCallProxy<R extends RpcMap> = {
420
+ [K in keyof ClientRpcs<R>]: CallFn<R[K]>;
421
+ };
422
+ type VoidKeys<M extends Record<string, AnyRpc>> = {
423
+ [K in keyof M]: RpcReturns<M[K]> extends void ? K : never;
424
+ }[keyof M];
425
+ /** `room.broadcast.<rpc>(params)`: void client RPCs only. */
426
+ type BroadcastProxy<R extends RpcMap> = {
427
+ [K in VoidKeys<ClientRpcs<R>>]: IsEmptyParams<R[K]> extends true ? (params?: RpcParams<R[K]>) => void : (params: RpcParams<R[K]>) => void;
428
+ };
429
+
430
+ /**
431
+ * The dirty-set contract shared by `track()` (producer) and the codec (consumer).
432
+ *
433
+ * - Field indices are the declaration indices on the collection/struct descriptor.
434
+ * - A struct field is dirty when its bit is in `fields`. If `nested` has an entry for it, only the
435
+ * nested-dirty fields are sent; no nested entry ⇒ the whole struct value is re-sent.
436
+ * - `owner: true` ⇒ the owner system field changed (entities only).
437
+ * - `added` records are sent whole (every field + owner); `removed` ids are sent as removes.
438
+ * An id is never in more than one of `added` / `removed` / `updated`.
439
+ * - Singletons use `SINGLETON_ID` (`''`) in `updated` and never add/remove.
440
+ */
441
+ interface FieldMask {
442
+ readonly fields: Set<number>;
443
+ readonly nested: Map<number, FieldMask>;
444
+ }
445
+ interface RecordDirty {
446
+ readonly mask: FieldMask;
447
+ owner: boolean;
448
+ }
449
+ interface CollectionDirty {
450
+ readonly added: Set<string>;
451
+ readonly removed: Set<string>;
452
+ readonly updated: Map<string, RecordDirty>;
453
+ }
454
+ /** Keyed by collection name. */
455
+ type DirtySet = Map<string, CollectionDirty>;
456
+ declare function createDirtySet(): DirtySet;
457
+ declare function createFieldMask(): FieldMask;
458
+ declare function collectionDirty(dirty: DirtySet, collection: string): CollectionDirty;
459
+ /**
460
+ * Marks a field path (declaration indices, outermost first) dirty on a record. The value at the
461
+ * end of the path is sent whole; intermediate structs become partial unless already whole.
462
+ */
463
+ declare function markField(dirty: DirtySet, collection: string, id: string, path: readonly number[]): void;
464
+ declare function markPath(mask: FieldMask, path: readonly number[]): void;
465
+ declare function markOwner(dirty: DirtySet, collection: string, id: string): void;
466
+ declare function markAdd(dirty: DirtySet, collection: string, id: string): void;
467
+ declare function markRemove(dirty: DirtySet, collection: string, id: string): void;
468
+ declare function isDirtyEmpty(dirty: DirtySet): boolean;
469
+ /** Is field `idx` of `mask` dirty and to be sent whole (no partial nested mask)? */
470
+ declare function isWhole(mask: FieldMask, idx: number): boolean;
471
+ /** Merges `src` into `dst` (union of dirtiness). */
472
+ declare function mergeDirty(dst: DirtySet, src: DirtySet): void;
473
+ declare function mergeMask(dst: FieldMask, src: FieldMask): void;
474
+ /** A new dirty set containing only the collections `keep` accepts (entries are shared, not copied). */
475
+ declare function filterDirty(dirty: DirtySet, keep: (collection: string) => boolean): DirtySet;
476
+
477
+ /**
478
+ * Synchronous SHA-256 (FIPS 180-4), zero dependencies, runs in Node and browsers.
479
+ *
480
+ * `schema.hash` must be available synchronously at module-init time (defineSchema is called at
481
+ * top level of `irtio/schema.ts`, and the client needs the hash for HELLO), which rules out
482
+ * `crypto.subtle.digest`. Tests verify this implementation against `crypto.subtle`.
483
+ */
484
+ /** SHA-256 digest of `data` as 32 bytes. */
485
+ declare function sha256(data: Uint8Array): Uint8Array;
486
+ declare function sha256Hex(data: Uint8Array): string;
487
+ declare function toHex(bytes: Uint8Array): string;
488
+
489
+ /**
490
+ * Byte-level primitives shared by the schema codec and `@irtio/protocol` framing.
491
+ * Little-endian numbers; LEB128 unsigned varints for lengths/indices; strings are
492
+ * varint byte-length + UTF-8.
493
+ */
494
+ declare class ByteWriter {
495
+ private buf;
496
+ private view;
497
+ private pos;
498
+ constructor(initialCapacity?: number);
499
+ get length(): number;
500
+ private ensure;
501
+ u8(v: number): void;
502
+ bool(v: boolean): void;
503
+ u16(v: number): void;
504
+ u32(v: number): void;
505
+ i32(v: number): void;
506
+ f32(v: number): void;
507
+ f64(v: number): void;
508
+ /** LEB128 unsigned varint (0 .. 2^32-1). */
509
+ varint(v: number): void;
510
+ /** Raw bytes, no length prefix. */
511
+ bytes(b: Uint8Array): void;
512
+ /** varint length + raw bytes. */
513
+ blob(b: Uint8Array): void;
514
+ /** varint UTF-8 byte length + bytes. Returns the byte length written. */
515
+ str(s: string): number;
516
+ /** Reserves `n` bytes and returns their offset (for masks filled in later). */
517
+ reserve(n: number): number;
518
+ setU8(at: number, v: number): void;
519
+ orU8(at: number, v: number): void;
520
+ /** A copy of the written bytes. */
521
+ finish(): Uint8Array;
522
+ }
523
+ declare class ByteReader {
524
+ readonly buf: Uint8Array;
525
+ private readonly view;
526
+ pos: number;
527
+ constructor(buf: Uint8Array, offset?: number);
528
+ get remaining(): number;
529
+ get eof(): boolean;
530
+ private need;
531
+ u8(): number;
532
+ bool(): boolean;
533
+ u16(): number;
534
+ u32(): number;
535
+ i32(): number;
536
+ f32(): number;
537
+ f64(): number;
538
+ varint(): number;
539
+ /** `n` raw bytes (a view, not a copy). */
540
+ bytes(n: number): Uint8Array;
541
+ /** varint length + raw bytes (a view). */
542
+ blob(): Uint8Array;
543
+ str(): string;
544
+ /** The rest of the buffer (a view). */
545
+ rest(): Uint8Array;
546
+ }
547
+ declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
548
+
549
+ /**
550
+ * The binary codec: snapshots, deltas, field-level diffing, and exact size estimation.
551
+ *
552
+ * Wire format (little-endian numbers, LEB128 varints, `str` = varint UTF-8 length + bytes):
553
+ *
554
+ * - value: `.opt` values are prefixed with a presence byte (0 = undefined, 1 = present).
555
+ * bool/u8 1 byte, u16 2, u32/i32/f32 4, f64 8, str/ref = str, enum = u8 index,
556
+ * list = varint length + items, struct (snapshot form) = every field in declaration order.
557
+ * - snapshot: `tick` u32, `hash8` 8 bytes, then every collection in `schema.collections` order —
558
+ * entity = varint count + (id str, owner str, record) per record; singleton = record.
559
+ * - delta: `tick` u32, `hash8` 8 bytes, varint dirty-collection count, then per collection
560
+ * (ascending index) varint collectionIndex, varint opCount and the ops. Op byte 0 = add
561
+ * (id str, owner str, record), 1 = update, 2 = remove (id str). An update is
562
+ * id str, a bitmask of `ceil((nFields + 1) / 8)` bytes (bit i = field i dirty,
563
+ * bit nFields = owner changed; bit k lives at byte `k >> 3`, bit `k & 7`), the owner str if
564
+ * the owner bit is set, then the dirty fields in index order. A dirty struct field is written
565
+ * as its presence byte (when `.opt`) followed by a nested mask of `ceil(n / 8)` bytes — all
566
+ * bits set when the whole struct is dirty — and its dirty fields under the same rule.
567
+ *
568
+ * Encoding validates: oversize strings/lists, out-of-range or non-integer ints, wrong types,
569
+ * missing required values and unknown enum members all throw with the field path.
570
+ */
571
+
572
+ /**
573
+ * The subset of `ByteWriter` the codec writes through. `ByteWriter` satisfies it; so does the
574
+ * internal counting sink used by `estimateSize` (which never allocates the payload).
575
+ */
576
+ interface ValueSink {
577
+ u8(v: number): void;
578
+ bool(v: boolean): void;
579
+ u16(v: number): void;
580
+ u32(v: number): void;
581
+ i32(v: number): void;
582
+ f32(v: number): void;
583
+ f64(v: number): void;
584
+ varint(v: number): void;
585
+ bytes(b: Uint8Array): void;
586
+ str(s: string): number;
587
+ }
588
+ /** Writes one value in snapshot form (presence byte first when the descriptor is `.opt`). */
589
+ declare function writeValue(w: ValueSink, desc: TypeDesc, v: unknown, path?: string): void;
590
+ /** Reads one value in snapshot form. */
591
+ declare function readValue(r: ByteReader, desc: TypeDesc): unknown;
592
+ /** Snapshot-form record encoding — used for RPC params and returns. */
593
+ declare function encodeFields(fields: readonly FieldDesc[], value: Record<string, unknown>): Uint8Array;
594
+ /** Decodes a snapshot-form record. Accepts raw bytes or a positioned `ByteReader`. */
595
+ declare function decodeFields(fields: readonly FieldDesc[], bytes: Uint8Array | ByteReader): Record<string, unknown>;
596
+ type PlainRecord = Record<string, unknown>;
597
+ interface Header {
598
+ readonly tick: number;
599
+ }
600
+ /** Snapshot options: `collections` limits which collections carry data (others are encoded empty/default). */
601
+ interface SnapshotOptions {
602
+ readonly collections?: (c: CollectionDesc) => boolean;
603
+ }
604
+ declare function encodeSnapshot(schema: AnySchema, state: PlainState, header: Header, options?: SnapshotOptions): Uint8Array;
605
+ interface DecodedSnapshot {
606
+ readonly tick: number;
607
+ readonly hash8: Uint8Array;
608
+ readonly state: PlainState;
609
+ }
610
+ declare function decodeSnapshot(schema: AnySchema, bytes: Uint8Array): DecodedSnapshot;
611
+ type DeltaOp = {
612
+ readonly op: 'add';
613
+ readonly id: string;
614
+ readonly owner: string;
615
+ readonly value: PlainRecord;
616
+ } | {
617
+ readonly op: 'update';
618
+ readonly id: string;
619
+ readonly owner?: string;
620
+ readonly mask: FieldMask;
621
+ readonly patch: PlainRecord;
622
+ } | {
623
+ readonly op: 'remove';
624
+ readonly id: string;
625
+ };
626
+ interface DeltaCollection {
627
+ readonly name: string;
628
+ readonly ops: DeltaOp[];
629
+ }
630
+ interface Delta {
631
+ readonly tick: number;
632
+ readonly hash8: Uint8Array;
633
+ readonly collections: DeltaCollection[];
634
+ }
635
+ declare function encodeDelta(schema: AnySchema, state: PlainState, dirty: DirtySet, header: Header): Uint8Array;
636
+ /** Exact byte length of `encodeDelta` for the same arguments, without building the payload. */
637
+ declare function estimateSize(schema: AnySchema, state: PlainState, dirty: DirtySet): number;
638
+ declare function decodeDelta(schema: AnySchema, bytes: Uint8Array): Delta;
639
+ /**
640
+ * `decodeDelta` against a caller-owned reader, which is left positioned at the first byte after
641
+ * the delta body. Trailing bytes are the caller's: the `CORRECT` frame appends `clientTick`
642
+ * (u32) after the delta body (week 8, D19) — a decoder that does not know about the suffix
643
+ * ignores it, which is what makes the extension additive.
644
+ */
645
+ declare function decodeDeltaFrom(schema: AnySchema, r: ByteReader): Delta;
646
+ /**
647
+ * Applies a decoded delta in place. Only the `Collection` interface and plain property
648
+ * assignment are used, so this also works against a tracked proxy state.
649
+ */
650
+ declare function applyDelta(schema: AnySchema, state: PlainState, delta: Delta): void;
651
+ /** Field-level diff between two plain states: what would have to be sent to turn `from` into `to`. */
652
+ declare function computeDirty(schema: AnySchema, from: PlainState, to: PlainState): DirtySet;
653
+
654
+ /**
655
+ * `track()` — a proxy tree over a plain state that records every write into a `DirtySet`.
656
+ *
657
+ * The proxies are thin: reads of scalars go straight to the plain object, reads of structs/lists
658
+ * hand back cached child proxies, and writes normalize the value (so what you read back is what a
659
+ * wire round-trip would give you: f32 quantized, defaults filled) before marking the dirty set.
660
+ *
661
+ * Paths are declaration indices, outermost first (`[fieldIndex, nestedIndex, ...]`), exactly the
662
+ * shape `markField` wants. Lists are whole-replace on the wire, so anything that changes inside a
663
+ * list marks the list field itself.
664
+ */
665
+
666
+ interface Tracked<S> {
667
+ /** Proxy tree over the plain state that was passed in. */
668
+ state: State<S>;
669
+ /** The live dirty set — replaced by `flush()`. */
670
+ dirty: DirtySet;
671
+ /** Returns the accumulated dirty set and starts a fresh one. */
672
+ flush(): DirtySet;
673
+ }
674
+ /**
675
+ * Wraps a plain state (from `createState` or a decoded snapshot) in tracking proxies.
676
+ * Every write through `tracked.state` marks `tracked.dirty`; `flush()` hands it over.
677
+ */
678
+ declare function track<S extends AnySchema>(schema: S, state: PlainState): Tracked<S>;
679
+ /**
680
+ * A deeply read-only view that *ignores* writes at runtime (rather than throwing), warning once
681
+ * per field path. The client SDK hands these out for objects this client does not own.
682
+ */
683
+ declare function frozenProxy<T>(value: T, ownershipHint: string): DeepReadonly<T>;
684
+
685
+ /**
686
+ * Schema diff classifier: compares two compiled `Schema`s (from `defineSchema`) and classifies
687
+ * every difference as `additive` (safe to deploy without breaking connected/reconnecting
688
+ * clients) or `breaking` (requires a fresh snapshot / client update). Works from the compiled
689
+ * descriptors (`collections`, `rpcs`, `roles`), never from the raw builder defs.
690
+ */
691
+
692
+ interface SchemaChange {
693
+ readonly kind: 'additive' | 'breaking';
694
+ /** Stable machine tag, e.g. `field.removed`, `enum.appended`. */
695
+ readonly code: string;
696
+ /** Dotted path to the changed thing, e.g. `players.color`, `rpc.dealCards.params.count`. */
697
+ readonly path: string;
698
+ /** One-line human message, printed verbatim by the CLI. */
699
+ readonly message: string;
700
+ }
701
+ /** Compares two compiled schemas and returns every classified change, sorted by path then code. */
702
+ declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
703
+
704
+ export { type AddOptions, type AnyDef, type AnyRpc, type AnySchema, type AnyType, type BroadcastProxy, ByteReader, ByteWriter, CANONICAL_VERSION, type ClientCallProxy, type ClientImplementations, type ClientRpcs, type ClientState, type Collection, type CollectionDesc, type CollectionDirty, type DecodedSnapshot, type DeepReadonly, type DeepWritable, type DefMap, type Delta, type DeltaCollection, type DeltaOp, type DirtySet, EntityCollection, type EntityDef, type EntityOptions, type FieldDesc, type FieldMask, type Fields, type FromCanonicalOptions, type Header, ID_MAX_BYTES, type Implementations, type Infer, type InferFields, type InitFields, type InitOf, type InstanceOf, type Kind, type OwnableKeys, type Owned, type PlainState, RESERVED_COLLECTION_NAMES, RESERVED_RPC_NAMES, type ReadonlyCollection, type RecordDirty, type RoleOf, type RpcDef, type RpcDesc, type RpcDirection, type RpcMap, type RpcParams, type RpcReturns, type RpcSpec, SERVER_OWNER, SINGLETON_ID, type Schema, type SchemaChange, type SchemaDefs, type SchemaIssue, type SchemaOptions, type SchemaRoles, type SchemaRpc, type ServerCallProxy, type ServerOwnedKeys, type ServerRpcs, type SingletonDef, type SnapshotOptions, type State, type Tracked, type Type, type TypeDesc, type ValueSink, type Visibility, type VisibleKeys, applyDelta, bool, bytesEqual, canonicalType, client, cloneValue, collectionDirty, computeDirty, createDirtySet, createFieldMask, createState, decodeDelta, decodeDeltaFrom, decodeFields, decodeSnapshot, defaultRecord, defaultValue, defineSchema, describeType, diffSchemas, encodeDelta, encodeFields, encodeSnapshot, entity, enumOf, estimateSize, f32, f64, filterDirty, frozenProxy, i32, isDirtyEmpty, isWhole, list, markAdd, markField, markOwner, markPath, markRemove, mergeDirty, mergeMask, normalizeRecord, normalizeValue, readValue, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, validateForDeploy, validateValue, writeValue };