@irtio/client 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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +556 -0
- package/dist/index.js +1364 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 irtio contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { ReadonlyCollection, RoleOf, VisibleKeys, SchemaDefs, EntityDef, DeepReadonly, InferFields, Owned, SingletonDef, ClientImplementations, ClientRpcs, SchemaRpc, ClientCallProxy, AnySchema, PlainState, CollectionDesc, Delta } from '@irtio/schema';
|
|
2
|
+
import { PresenceRecord } from '@irtio/protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@irtio/client`'s public contract: the `Room` surface `joinRoom` returns, its options, the
|
|
6
|
+
* client-side state types, and the two injectable seams (`Transport`, `Scheduler`).
|
|
7
|
+
*
|
|
8
|
+
* Nothing here imports a runtime dependency: the package speaks the wire with `@irtio/protocol`
|
|
9
|
+
* and `@irtio/schema` only, and reaches for the global `WebSocket` that Node 22 and every
|
|
10
|
+
* browser already have.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A client-side entity collection: the read API of `@irtio/schema`'s `ReadonlyCollection`, plus
|
|
15
|
+
* index sugar so `state.players[room.me]` reads the same as `state.players.get(room.me)`.
|
|
16
|
+
*/
|
|
17
|
+
type ClientCollection<T> = ReadonlyCollection<T> & {
|
|
18
|
+
readonly [id: string]: T | undefined;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The client's view of the room state for `Role`.
|
|
22
|
+
*
|
|
23
|
+
* Instances of an instance-owned collection are typed **writable** (`Owned<T>`): whether *this*
|
|
24
|
+
* client owns a given instance is a runtime fact, so the compiler cannot decide it. Writes to an
|
|
25
|
+
* instance the client does not own are ignored at runtime with a one-time warning naming the
|
|
26
|
+
* owner. `serverOwned` collections and every singleton are `DeepReadonly`, so writing to them is
|
|
27
|
+
* a compile error as well.
|
|
28
|
+
*/
|
|
29
|
+
type ClientState<S, Role extends string = RoleOf<S> & string> = {
|
|
30
|
+
[K in VisibleKeys<S, Role>]: SchemaDefs<S>[K] extends EntityDef<infer F, infer O> ? O extends {
|
|
31
|
+
serverOwned: true;
|
|
32
|
+
} ? ClientCollection<DeepReadonly<InferFields<F>>> : ClientCollection<Owned<InferFields<F>>> : SchemaDefs<S>[K] extends SingletonDef<infer F, any> ? DeepReadonly<InferFields<F>> : never;
|
|
33
|
+
};
|
|
34
|
+
/** `connecting` → `starting`? → `connected` ⇄ `reconnecting` → `closed`. */
|
|
35
|
+
type Status = 'connecting' | 'starting' | 'connected' | 'reconnecting' | 'closed';
|
|
36
|
+
/**
|
|
37
|
+
* Client-local error code for a transport failure (socket close or error) before `WELCOME`
|
|
38
|
+
* arrives — there is no protocol `E_*` code for this because the server never got to say
|
|
39
|
+
* anything. `joinRoom` rejects with `Error(`${E_CONNECT_FAILED}: ...`)`, matching the shape the
|
|
40
|
+
* fatal-`ERROR` reject path already uses (`${code}: ${message}`).
|
|
41
|
+
*/
|
|
42
|
+
declare const E_CONNECT_FAILED = "E_CONNECT_FAILED";
|
|
43
|
+
/** A server `ERROR` frame, or a local decode/transport failure surfaced the same way. */
|
|
44
|
+
interface RoomError {
|
|
45
|
+
/** Catalogue name (`E_ROOM_FULL`, …) or `'E_INTERNAL'` for a local failure. */
|
|
46
|
+
readonly code: string;
|
|
47
|
+
readonly message: string;
|
|
48
|
+
/** Fatal errors close the room; non-fatal ones are informational (`E_STARTING`). */
|
|
49
|
+
readonly fatal: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** One `CORRECT` op: the server overrode fields of an instance this client owns. */
|
|
52
|
+
interface Correction {
|
|
53
|
+
readonly collection: string;
|
|
54
|
+
readonly id: string;
|
|
55
|
+
/** Top-level field names the correction carried. */
|
|
56
|
+
readonly fields: readonly string[];
|
|
57
|
+
/** The server's values for those fields (already applied to `room.state`). */
|
|
58
|
+
readonly patch: Readonly<Record<string, unknown>>;
|
|
59
|
+
readonly tick: number;
|
|
60
|
+
/**
|
|
61
|
+
* The client write tick the server judged (D19, week 8): every local write through it is
|
|
62
|
+
* reflected in `patch`; newer local writes were re-applied on top. Absent against a
|
|
63
|
+
* pre-week-8 server (the correction then wins in full).
|
|
64
|
+
*/
|
|
65
|
+
readonly clientTick?: number;
|
|
66
|
+
/** How many pending local writes were re-applied over this correction (0 = pure snap). */
|
|
67
|
+
readonly replayed: number;
|
|
68
|
+
/**
|
|
69
|
+
* `true` when the correction was older than the client's resimulation window (20 flushed
|
|
70
|
+
* writes) — everything it named snapped to the server's values, nothing was replayed.
|
|
71
|
+
*/
|
|
72
|
+
readonly snapped: boolean;
|
|
73
|
+
}
|
|
74
|
+
interface RoomEvents {
|
|
75
|
+
status: Status;
|
|
76
|
+
error: RoomError;
|
|
77
|
+
correct: Correction;
|
|
78
|
+
}
|
|
79
|
+
type Unsubscribe = () => void;
|
|
80
|
+
/**
|
|
81
|
+
* @internal One connection. The default implementation wraps the global `WebSocket`;
|
|
82
|
+
* `@irtio/testing` injects an in-process pair driven by the harness clock.
|
|
83
|
+
*/
|
|
84
|
+
interface TransportSocket {
|
|
85
|
+
send(bytes: Uint8Array): void;
|
|
86
|
+
close(): void;
|
|
87
|
+
onopen: (() => void) | null;
|
|
88
|
+
onmessage: ((bytes: Uint8Array) => void) | null;
|
|
89
|
+
onclose: ((info?: {
|
|
90
|
+
code?: number;
|
|
91
|
+
reason?: string;
|
|
92
|
+
}) => void) | null;
|
|
93
|
+
onerror: ((error: unknown) => void) | null;
|
|
94
|
+
}
|
|
95
|
+
/** @internal */
|
|
96
|
+
interface Transport {
|
|
97
|
+
connect(url: string): TransportSocket;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* @internal Everything time-shaped the client does. `@irtio/testing` supplies a fake clock so a
|
|
101
|
+
* whole room runs deterministically with no real timers.
|
|
102
|
+
*/
|
|
103
|
+
interface Scheduler {
|
|
104
|
+
now(): number;
|
|
105
|
+
/** Returns a cancel function. */
|
|
106
|
+
setTimeout(fn: () => void, ms: number): () => void;
|
|
107
|
+
/**
|
|
108
|
+
* Optional animation-frame callback (browsers). When present the write batcher aligns to it,
|
|
109
|
+
* with `writeIntervalMs` as a hard cap; when absent it uses `setTimeout` alone.
|
|
110
|
+
*/
|
|
111
|
+
frame?(fn: () => void): () => void;
|
|
112
|
+
}
|
|
113
|
+
/** @internal One frame crossing the socket, for the bot trace recorder. */
|
|
114
|
+
type FrameHook = (dir: 'in' | 'out', type: number, bytes: Uint8Array) => void;
|
|
115
|
+
interface JoinOptions<S, Role extends string = string> {
|
|
116
|
+
/**
|
|
117
|
+
* Room code (`ABCD`) or a full share link. Omitted in a browser ⇒ `?room=` from the current
|
|
118
|
+
* URL; absent there too ⇒ a room is created and `?room=` appended.
|
|
119
|
+
*/
|
|
120
|
+
readonly room?: string;
|
|
121
|
+
/**
|
|
122
|
+
* The role to join as. Passing a literal (`role: 'host'`) narrows `room.state` to that role's
|
|
123
|
+
* view; omitting it keeps every role's guaranteed-visible collections and drops the
|
|
124
|
+
* role-scoped ones.
|
|
125
|
+
*/
|
|
126
|
+
readonly role?: Role;
|
|
127
|
+
readonly name?: string;
|
|
128
|
+
/** Implementations of every server → client RPC. Exhaustive by type when the schema has any. */
|
|
129
|
+
readonly rpc?: ClientImplementations<ClientRpcs<SchemaRpc<S>>>;
|
|
130
|
+
/** Endpoint override. Otherwise resolved as described in `resolveUrl`. */
|
|
131
|
+
readonly url?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Public project key. Defaults to `schema.project`, or `'dev'` against a localhost endpoint
|
|
134
|
+
* (which is what `irtio dev` uses when there is no `irtio.json`).
|
|
135
|
+
*/
|
|
136
|
+
readonly key?: string;
|
|
137
|
+
readonly onStatus?: (status: Status) => void;
|
|
138
|
+
/** Hard cap on the owned-write flush window, in ms. Default 50. */
|
|
139
|
+
readonly writeIntervalMs?: number;
|
|
140
|
+
/** @internal */
|
|
141
|
+
readonly transport?: Transport;
|
|
142
|
+
/** @internal */
|
|
143
|
+
readonly scheduler?: Scheduler;
|
|
144
|
+
/** @internal */
|
|
145
|
+
readonly onFrame?: FrameHook;
|
|
146
|
+
}
|
|
147
|
+
/** `joinRelay` options: a relay room has no schema, so there is no state and no RPC. */
|
|
148
|
+
interface JoinRelayOptions {
|
|
149
|
+
readonly room?: string;
|
|
150
|
+
readonly role?: string;
|
|
151
|
+
readonly name?: string;
|
|
152
|
+
readonly url?: string;
|
|
153
|
+
/** Public project key; defaults to `'dev'`. */
|
|
154
|
+
readonly key?: string;
|
|
155
|
+
readonly onStatus?: (status: Status) => void;
|
|
156
|
+
/** @internal */
|
|
157
|
+
readonly transport?: Transport;
|
|
158
|
+
/** @internal */
|
|
159
|
+
readonly scheduler?: Scheduler;
|
|
160
|
+
/** @internal */
|
|
161
|
+
readonly onFrame?: FrameHook;
|
|
162
|
+
}
|
|
163
|
+
type MessageTarget = 'all' | string | {
|
|
164
|
+
readonly role: string;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* `room.call.<rpc>(params)` — every server RPC the builder declared, plus the one built-in.
|
|
168
|
+
* `requestOwnership` takes the entity/id pair positionally and unwraps `{ granted }`.
|
|
169
|
+
*/
|
|
170
|
+
type RoomCallProxy<S> = ClientCallProxy<SchemaRpc<S>> & {
|
|
171
|
+
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
172
|
+
};
|
|
173
|
+
/** What `joinRoom` returns. */
|
|
174
|
+
interface Room<S, Role extends string = RoleOf<S> & string> {
|
|
175
|
+
/** This client's id — `ctx.clientId` on the server. */
|
|
176
|
+
readonly me: string;
|
|
177
|
+
/** The room code (from `WELCOME`). */
|
|
178
|
+
readonly id: string;
|
|
179
|
+
/** Shareable URL carrying `?room=<id>`. */
|
|
180
|
+
readonly link: string;
|
|
181
|
+
/** The last server tick this client saw. */
|
|
182
|
+
readonly tick: number;
|
|
183
|
+
readonly status: Status;
|
|
184
|
+
/** Round-trip time in ms from the last `PING`/`PONG`, or 0 before the first one. */
|
|
185
|
+
readonly rtt: number;
|
|
186
|
+
readonly state: ClientState<S, Role>;
|
|
187
|
+
/** Built-in presence, ordered by join. */
|
|
188
|
+
readonly clients: readonly PresenceRecord[];
|
|
189
|
+
readonly call: RoomCallProxy<S>;
|
|
190
|
+
/** Convenience alias for `room.call.requestOwnership`. */
|
|
191
|
+
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
192
|
+
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
193
|
+
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
194
|
+
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
195
|
+
/** Sends any pending owned writes immediately instead of at the next flush window. */
|
|
196
|
+
flush(): void;
|
|
197
|
+
leave(): void;
|
|
198
|
+
}
|
|
199
|
+
/** What `joinRelay` returns: presence + the raw message channel, nothing else. */
|
|
200
|
+
interface RelayRoom {
|
|
201
|
+
readonly me: string;
|
|
202
|
+
readonly id: string;
|
|
203
|
+
readonly link: string;
|
|
204
|
+
readonly status: Status;
|
|
205
|
+
/** Round-trip time in ms from the last `PING`/`PONG`, or 0 before the first one. */
|
|
206
|
+
readonly rtt: number;
|
|
207
|
+
readonly clients: readonly PresenceRecord[];
|
|
208
|
+
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
209
|
+
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
210
|
+
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
211
|
+
leave(): void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Endpoint resolution and `?room=` handling.
|
|
216
|
+
*
|
|
217
|
+
* The builder writes no URL and no env var in the common case: the schema carries the project id,
|
|
218
|
+
* `location.hostname` decides localhost vs the region, and the room code lives in the URL.
|
|
219
|
+
*/
|
|
220
|
+
/** The region every project resolves to (single-region hosting). */
|
|
221
|
+
declare const DEFAULT_REGION = "eu";
|
|
222
|
+
/** `irtio dev`'s default port. */
|
|
223
|
+
declare const DEV_PORT = 7070;
|
|
224
|
+
/**
|
|
225
|
+
* Resolution order: explicit `url` > `IRT_URL` / `window.IRT_URL` > `ws://localhost:7070` when the
|
|
226
|
+
* page is on localhost (or there is no page at all — Node, bots, tests) > `wss://eu.irt.io`.
|
|
227
|
+
*/
|
|
228
|
+
declare function resolveUrl(explicit?: string): string;
|
|
229
|
+
/**
|
|
230
|
+
* A room option may be a bare code (`ABCD`) or a whole share link. Returns the code, or `''`
|
|
231
|
+
* when the argument names no room (create-by-join).
|
|
232
|
+
*/
|
|
233
|
+
declare function roomIdFrom(roomOrLink: string): string;
|
|
234
|
+
/**
|
|
235
|
+
* The share link when there is no page to read: the websocket endpoint with an http(s) scheme
|
|
236
|
+
* and `?room=<id>`. `WELCOME` carries no public URL, so this is the best the client can do
|
|
237
|
+
* — it is right for `irtio dev` (the dev server serves HTTP on the same port) and for a
|
|
238
|
+
* single-origin deployment.
|
|
239
|
+
*/
|
|
240
|
+
declare function linkForUrl(wsUrl: string, roomId: string): string;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The default `Scheduler`. In a browser the write batcher aligns to `requestAnimationFrame` (one
|
|
244
|
+
* `WRITE` per rendered frame, which is what a game loop produces); everywhere else — and as a
|
|
245
|
+
* hard cap in the browser too — it falls back to a `setTimeout` of `writeIntervalMs`.
|
|
246
|
+
*
|
|
247
|
+
* `@irtio/testing` replaces this whole object with a fake clock, which is why the client never
|
|
248
|
+
* calls `setTimeout` / `requestAnimationFrame` / `Date.now()` directly.
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/** The scheduler used when `joinRoom` is not given one. */
|
|
252
|
+
declare function defaultScheduler(): Scheduler;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The default `Transport`: the global `WebSocket`. Node 22 and every browser we target ship one,
|
|
256
|
+
* which is why `@irtio/client` has no runtime dependency at all (`ws` would pull one in and would
|
|
257
|
+
* not work in a browser).
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
declare const webSocketTransport: Transport;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The client's state layer.
|
|
264
|
+
*
|
|
265
|
+
* One plain `PlainState` (decoded from the join snapshot, advanced by every `DELTA`) with two
|
|
266
|
+
* views over it:
|
|
267
|
+
*
|
|
268
|
+
* - **owned instances** → the `track()` proxy for that record, so a write normalizes exactly like
|
|
269
|
+
* the server would (f32 quantized, ranges and string sizes enforced by a throw) and marks a
|
|
270
|
+
* dirty set the flush window turns into one `WRITE`;
|
|
271
|
+
* - **everything else** → `frozenProxy`, which ignores writes and warns once with the ownership
|
|
272
|
+
* hint.
|
|
273
|
+
*
|
|
274
|
+
* Which one you get is decided per `get()`, so an ownership change needs no bookkeeping — but the
|
|
275
|
+
* object identity is not stable across it, which is why `state.x.get(id)` must be re-read after a
|
|
276
|
+
* grab (documented in the README).
|
|
277
|
+
*
|
|
278
|
+
* The server-wins rule lives here too: a `DELTA`'s update ops are dropped field-wise for
|
|
279
|
+
* instances this client owns (adds, removes and owner changes still apply), while a `CORRECT`
|
|
280
|
+
* applies in full and clears the matching local dirty marks.
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
type AnyRecord$1 = Record<string, unknown>;
|
|
284
|
+
/**
|
|
285
|
+
* Resimulation depth cap (D19): how many flushed-but-unjudged `WRITE`s the client retains for
|
|
286
|
+
* replay after a `CORRECT`. A correction older than the retained window snaps everything it
|
|
287
|
+
* corrects and counts it (`snapped`); buffer memory is bounded by the same window.
|
|
288
|
+
*/
|
|
289
|
+
declare const RESIM_DEPTH = 20;
|
|
290
|
+
/** One correction op, as the `'correct'` event sees it. */
|
|
291
|
+
interface CorrectionOp {
|
|
292
|
+
readonly collection: string;
|
|
293
|
+
readonly id: string;
|
|
294
|
+
readonly fields: readonly string[];
|
|
295
|
+
readonly patch: AnyRecord$1;
|
|
296
|
+
readonly tick: number;
|
|
297
|
+
/**
|
|
298
|
+
* The client write tick the server judged (D19), absent against a pre-week-8 server. Local
|
|
299
|
+
* writes newer than it were re-applied on top of the correction.
|
|
300
|
+
*/
|
|
301
|
+
readonly clientTick?: number;
|
|
302
|
+
/** How many pending local writes were re-applied over this correction's values. */
|
|
303
|
+
readonly replayed: number;
|
|
304
|
+
/** `true` when the correction outran the resim window and everything it named snapped. */
|
|
305
|
+
readonly snapped: boolean;
|
|
306
|
+
}
|
|
307
|
+
declare class ClientStore {
|
|
308
|
+
readonly ext: AnySchema;
|
|
309
|
+
/** Reads the current client id — it is not known until the first `WELCOME`. */
|
|
310
|
+
private readonly meOf;
|
|
311
|
+
plain: PlainState;
|
|
312
|
+
private tracked;
|
|
313
|
+
private readonly descs;
|
|
314
|
+
private readonly frozen;
|
|
315
|
+
/** The object handed out as `room.state`; identity survives a resync. */
|
|
316
|
+
readonly view: AnyRecord$1;
|
|
317
|
+
/** Flushed-but-unjudged writes, oldest first, at most `RESIM_DEPTH` entries (D19). */
|
|
318
|
+
private readonly pendingWrites;
|
|
319
|
+
/** The newest write tick evicted from `pendingWrites` — corrections older than it must snap. */
|
|
320
|
+
private evictedThroughTick;
|
|
321
|
+
constructor(ext: AnySchema,
|
|
322
|
+
/** Reads the current client id — it is not known until the first `WELCOME`. */
|
|
323
|
+
meOf: () => string);
|
|
324
|
+
loadSnapshot(bytes: Uint8Array): void;
|
|
325
|
+
/**
|
|
326
|
+
* Captures the field values of every pending owned write, keyed by collection then id then
|
|
327
|
+
* field name — call this **before** `loadSnapshot` on a resync, or the edit is gone once the
|
|
328
|
+
* old `plain` it lives in is replaced. Paired with `applyPendingWrites`.
|
|
329
|
+
*/
|
|
330
|
+
capturePendingWrites(): Map<string, Map<string, AnyRecord$1>>;
|
|
331
|
+
/**
|
|
332
|
+
* Writes a `capturePendingWrites` snapshot into the freshly loaded `plain` (still the raw
|
|
333
|
+
* decoded values — no validation, they were already validated at the time of the original
|
|
334
|
+
* local write) and marks every owned field dirty so the next flush resends the real edit, not
|
|
335
|
+
* whatever the resync snapshot happened to carry for that field.
|
|
336
|
+
*/
|
|
337
|
+
applyPendingWrites(pending: Map<string, Map<string, AnyRecord$1>>): void;
|
|
338
|
+
private buildView;
|
|
339
|
+
/** The writable tracked proxy when this client owns `id`, otherwise a frozen one. */
|
|
340
|
+
instance(desc: CollectionDesc, id: string): unknown;
|
|
341
|
+
private freeze;
|
|
342
|
+
private entityHint;
|
|
343
|
+
private singletonHint;
|
|
344
|
+
/**
|
|
345
|
+
* Applies a server `DELTA`. Update ops for instances this client owns are dropped field-wise
|
|
346
|
+
* (server wins only through `CORRECT`); adds, removes and owner changes always apply.
|
|
347
|
+
*/
|
|
348
|
+
applyServerDelta(delta: Delta): void;
|
|
349
|
+
/**
|
|
350
|
+
* The flushed-write half of the §7.2 in-flight own-write fix: an `add` op for an instance this
|
|
351
|
+
* client owns replaces the whole record, so any write already *flushed* (dirty set consumed —
|
|
352
|
+
* `preserveLocalWrites` cannot see it) but not yet judged would be reverted by the echo.
|
|
353
|
+
* Re-apply the retained pending writes in flush order; the server's eventual `CORRECT` (if the
|
|
354
|
+
* write is clamped) still wins through the snap+replay path.
|
|
355
|
+
*/
|
|
356
|
+
private replayOverAddEchoes;
|
|
357
|
+
private withoutOwnedUpdates;
|
|
358
|
+
/**
|
|
359
|
+
* An `add` for an instance this client will own, with its unflushed local field values folded
|
|
360
|
+
* back in. Returns `op` unchanged when there is nothing to preserve.
|
|
361
|
+
*/
|
|
362
|
+
private preserveLocalWrites;
|
|
363
|
+
/**
|
|
364
|
+
* Applies a `CORRECT`: snap the named fields to the server's values, then re-apply every local
|
|
365
|
+
* write newer than the judged `clientTick` in order (D19 snap + replay) — flushed writes from
|
|
366
|
+
* the retained buffer first, the still-unflushed in-window edit last (it is the newest, and its
|
|
367
|
+
* dirty mark survives so the next flush re-sends the *local* value, not the server's).
|
|
368
|
+
*
|
|
369
|
+
* Without a `clientTick` (a pre-week-8 server) — or when the correction outran the
|
|
370
|
+
* `RESIM_DEPTH` window (`snapped`) — the pre-D19 semantics apply: the correction wins in full
|
|
371
|
+
* and the local dirty marks it supersedes are cleared.
|
|
372
|
+
*/
|
|
373
|
+
applyCorrection(delta: Delta, clientTick?: number): CorrectionOp[];
|
|
374
|
+
private desc;
|
|
375
|
+
/** Cheap check for the flush loop: has anything been written locally since the last flush? */
|
|
376
|
+
get hasLocalWrites(): boolean;
|
|
377
|
+
/**
|
|
378
|
+
* The `WRITE` payload for everything dirty, or `undefined` when nothing qualifies. Consumes the
|
|
379
|
+
* dirty set either way (a write to an instance that has since been removed or handed away is
|
|
380
|
+
* dropped, not retried forever).
|
|
381
|
+
*/
|
|
382
|
+
takeWrite(tick: number): Uint8Array | undefined;
|
|
383
|
+
/**
|
|
384
|
+
* Captures the field values a flushed `WRITE` carried, keyed by its write tick, so a later
|
|
385
|
+
* `CORRECT` can re-apply exactly the writes the server had not judged yet (D19 snap + replay).
|
|
386
|
+
* Bounded to `RESIM_DEPTH` entries; eviction is remembered so an outrun correction snaps.
|
|
387
|
+
*/
|
|
388
|
+
private retainPendingWrite;
|
|
389
|
+
/** Re-applies one retained write's fields for `collection[id]` onto the plain state. */
|
|
390
|
+
private replayWrite;
|
|
391
|
+
/**
|
|
392
|
+
* Re-marks every field of every instance this client owns, so a resync `WELCOME` (wake or
|
|
393
|
+
* worker restart) does not silently drop writes that were in flight.
|
|
394
|
+
*/
|
|
395
|
+
remarkOwned(): void;
|
|
396
|
+
/** Only update ops, only instances that still exist and are still mine, never the owner bit. */
|
|
397
|
+
private ownedDirty;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The connection state machine: one socket, one room, everything the protocol says.
|
|
402
|
+
*
|
|
403
|
+
* `Session` is deliberately schema-agnostic — `joinRoom` gives it the builder's schema, `joinRelay`
|
|
404
|
+
* gives it none — so the wire handling, reconnection, ping/rtt, RPC plumbing and write batching
|
|
405
|
+
* exist exactly once.
|
|
406
|
+
*/
|
|
407
|
+
|
|
408
|
+
type AnyRecord = Record<string, unknown>;
|
|
409
|
+
type ClientImpl = (params: AnyRecord) => unknown;
|
|
410
|
+
/** Default hard cap on the owned-write flush window. */
|
|
411
|
+
declare const DEFAULT_WRITE_INTERVAL_MS = 50;
|
|
412
|
+
/** How often the client pings for `room.rtt`. */
|
|
413
|
+
declare const PING_INTERVAL_MS = 10000;
|
|
414
|
+
/** How long an outbound RPC waits for its `REPLY` before rejecting. */
|
|
415
|
+
declare const CALL_TIMEOUT_MS = 10000;
|
|
416
|
+
interface SessionOptions {
|
|
417
|
+
/** The builder's schema, or `undefined` for a schema-less relay room. */
|
|
418
|
+
readonly schema: AnySchema | undefined;
|
|
419
|
+
readonly url: string;
|
|
420
|
+
readonly roomId: string;
|
|
421
|
+
readonly key: string;
|
|
422
|
+
readonly role?: string | undefined;
|
|
423
|
+
readonly name?: string | undefined;
|
|
424
|
+
readonly rpc?: Readonly<Record<string, ClientImpl>> | undefined;
|
|
425
|
+
readonly writeIntervalMs?: number | undefined;
|
|
426
|
+
readonly transport?: Transport | undefined;
|
|
427
|
+
readonly scheduler?: Scheduler | undefined;
|
|
428
|
+
readonly onFrame?: FrameHook | undefined;
|
|
429
|
+
readonly onStatus?: ((status: Status) => void) | undefined;
|
|
430
|
+
/** `true` in a browser with no explicit `room` option: `?room=` gets written back. */
|
|
431
|
+
readonly publishLocation: boolean;
|
|
432
|
+
}
|
|
433
|
+
declare class Session {
|
|
434
|
+
private readonly options;
|
|
435
|
+
readonly ext: AnySchema;
|
|
436
|
+
readonly store: ClientStore;
|
|
437
|
+
/** The schema the `CALL`/`REPLY` rpc id space indexes into. */
|
|
438
|
+
private readonly rpcSchema;
|
|
439
|
+
private readonly transport;
|
|
440
|
+
private readonly scheduler;
|
|
441
|
+
private readonly writeIntervalMs;
|
|
442
|
+
private readonly impls;
|
|
443
|
+
me: string;
|
|
444
|
+
role: string;
|
|
445
|
+
roomId: string;
|
|
446
|
+
tick: number;
|
|
447
|
+
/**
|
|
448
|
+
* The client-local write counter (D19): +1 per flushed `WRITE`, stamped in its delta header.
|
|
449
|
+
* Monotonic for the life of the session, across reconnects — the server's `lastClientTick`
|
|
450
|
+
* for this client survives the grace window too.
|
|
451
|
+
*/
|
|
452
|
+
writeTick: number;
|
|
453
|
+
/** The room's tick interval from `WELCOME`, or 0 when unknown (relay / pre-week-8 server). */
|
|
454
|
+
tickIntervalMs: number;
|
|
455
|
+
rtt: number;
|
|
456
|
+
status: Status;
|
|
457
|
+
private socket;
|
|
458
|
+
private resumeToken;
|
|
459
|
+
private joined;
|
|
460
|
+
private left;
|
|
461
|
+
private attempt;
|
|
462
|
+
private reqId;
|
|
463
|
+
private readonly pending;
|
|
464
|
+
private readonly listeners;
|
|
465
|
+
private readonly messageListeners;
|
|
466
|
+
private cancelFlush;
|
|
467
|
+
private cancelPing;
|
|
468
|
+
private cancelRetry;
|
|
469
|
+
private linkOverride;
|
|
470
|
+
private settleJoin;
|
|
471
|
+
private failJoin;
|
|
472
|
+
constructor(options: SessionOptions);
|
|
473
|
+
/** Connects and resolves on the first `WELCOME`; a fatal `ERROR` before it rejects. */
|
|
474
|
+
start(): Promise<Session>;
|
|
475
|
+
private connect;
|
|
476
|
+
private transportFailed;
|
|
477
|
+
private sendHello;
|
|
478
|
+
/** Leaves for good: no reconnect, pending calls reject, the socket closes. */
|
|
479
|
+
leave(): void;
|
|
480
|
+
private fatal;
|
|
481
|
+
private onSocketClosed;
|
|
482
|
+
private stopTimers;
|
|
483
|
+
private send;
|
|
484
|
+
private onFrame;
|
|
485
|
+
/** A frame we could not decode or apply. Reported, never fatal: the stream may recover. */
|
|
486
|
+
private localError;
|
|
487
|
+
private onWelcome;
|
|
488
|
+
private onDelta;
|
|
489
|
+
private onCorrect;
|
|
490
|
+
private onError;
|
|
491
|
+
private onPong;
|
|
492
|
+
private onMsg;
|
|
493
|
+
private startTimers;
|
|
494
|
+
/**
|
|
495
|
+
* The write batcher: one window per animation frame in a browser, or per `writeIntervalMs`
|
|
496
|
+
* everywhere else, with `writeIntervalMs` as the hard cap in both. Every owned field written
|
|
497
|
+
* inside a window leaves as a single `WRITE`.
|
|
498
|
+
*/
|
|
499
|
+
private armFlush;
|
|
500
|
+
private armPing;
|
|
501
|
+
/** Sends every pending owned write now. */
|
|
502
|
+
flush(): void;
|
|
503
|
+
private descOf;
|
|
504
|
+
call(name: string, params?: AnyRecord): Promise<unknown>;
|
|
505
|
+
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
506
|
+
private onReply;
|
|
507
|
+
private onCall;
|
|
508
|
+
private replyOk;
|
|
509
|
+
private replyError;
|
|
510
|
+
private rejectPending;
|
|
511
|
+
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
512
|
+
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
513
|
+
get clients(): readonly PresenceRecord[];
|
|
514
|
+
get link(): string;
|
|
515
|
+
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
516
|
+
private emit;
|
|
517
|
+
private setStatus;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* `@irtio/client` — the browser/Node SDK.
|
|
522
|
+
*
|
|
523
|
+
* ```ts
|
|
524
|
+
* import { joinRoom } from '@irtio/client';
|
|
525
|
+
* import { schema } from './irtio/schema';
|
|
526
|
+
*
|
|
527
|
+
* const room = await joinRoom(schema); // reads ?room=, or creates one and adds it
|
|
528
|
+
* const me = room.state.players.get(room.me)!; // owned: write it like local state
|
|
529
|
+
* me.x = 10;
|
|
530
|
+
* ```
|
|
531
|
+
*
|
|
532
|
+
* Three rules the rest of the package exists to keep:
|
|
533
|
+
*
|
|
534
|
+
* 1. **Owned = local.** An instance this client owns is a writable proxy; writing it marks a
|
|
535
|
+
* dirty field, and one `WRITE` per flush window carries every field written in that window.
|
|
536
|
+
* 2. **Server wins.** Incoming `DELTA`s never overwrite fields of instances we own — only a
|
|
537
|
+
* `CORRECT` does, and it clears the local dirty marks it supersedes and fires `'correct'`.
|
|
538
|
+
* 3. **Everything else is frozen.** Non-owned instances, `serverOwned` collections and singletons
|
|
539
|
+
* are `DeepReadonly` at compile time and warn-once no-ops at runtime.
|
|
540
|
+
*/
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Joins (or creates) a room and resolves once the first `WELCOME` arrives. A fatal `ERROR`
|
|
544
|
+
* before that — bad key, wrong origin, room full, schema skew — rejects with its code and message.
|
|
545
|
+
*/
|
|
546
|
+
declare function joinRoom<S extends AnySchema, Role extends string = RoleOf<S> & string>(schema: S, options?: JoinOptions<S, Role> & {
|
|
547
|
+
role?: RoleOf<S> & string;
|
|
548
|
+
}): Promise<Room<S, Role>>;
|
|
549
|
+
/**
|
|
550
|
+
* Joins a **schema-less** relay room: no schema, no state, no RPCs — presence and the raw message
|
|
551
|
+
* channel. A separate entry rather than a schema-less `joinRoom` overload, because everything a
|
|
552
|
+
* `Room` promises about state would be a lie here.
|
|
553
|
+
*/
|
|
554
|
+
declare function joinRelay(options?: JoinRelayOptions): Promise<RelayRoom>;
|
|
555
|
+
|
|
556
|
+
export { CALL_TIMEOUT_MS, type ClientCollection, type ClientState, ClientStore, type Correction, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, type FrameHook, type JoinOptions, type JoinRelayOptions, type MessageTarget, PING_INTERVAL_MS, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, defaultScheduler, joinRelay, joinRoom, linkForUrl, resolveUrl, roomIdFrom, webSocketTransport };
|