@irtio/runtime 0.5.1 → 0.6.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/dist/{chunk-5ZDQAAFJ.js → chunk-K42HA75G.js} +963 -114
- package/dist/{chunk-HFOMXKSO.js → chunk-VMCE3LRO.js} +1 -1
- package/dist/contract-BjMsoJIV.d.ts +415 -0
- package/dist/index.d.ts +41 -3
- package/dist/index.js +26 -2
- package/dist/{room-9ZQoy9yi.d.ts → room-CfnEjlcg.d.ts} +214 -7
- package/dist/test/index.d.ts +51 -4
- package/dist/test/index.js +70 -1
- package/dist/worker/index.d.ts +156 -3
- package/dist/worker/index.js +96 -10
- package/package.json +8 -4
- package/dist/contract-B8QSO0MH.d.ts +0 -204
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { L as LogLevel,
|
|
2
|
-
import { ErrorCodeName } from '@irtio/protocol';
|
|
3
|
-
import { LeaveReason } from '@irtio/server';
|
|
1
|
+
import { L as LogLevel, T as TimelineDump, k as RoomStats, b as HostCall, c as HostCallResult, R as RoomCoreApi, i as RoomHost } from '../contract-BjMsoJIV.js';
|
|
2
|
+
import { ErrorCodeName, ProfileSnapshot } from '@irtio/protocol';
|
|
3
|
+
import { NpcConfig, LeaveReason } from '@irtio/server';
|
|
4
4
|
import '@irtio/schema';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -35,6 +35,11 @@ type ToWorker = {
|
|
|
35
35
|
* the one `bundleUrl` implements.
|
|
36
36
|
*/
|
|
37
37
|
migrate?: MigrationChainStep[];
|
|
38
|
+
/**
|
|
39
|
+
* D65: run this room with a bandwidth ledger. Absent means off, which is the shape every
|
|
40
|
+
* existing supervisor already sends, and a room without it pays nothing.
|
|
41
|
+
*/
|
|
42
|
+
profile?: boolean;
|
|
38
43
|
} | {
|
|
39
44
|
t: 'join';
|
|
40
45
|
clientId: string;
|
|
@@ -44,6 +49,8 @@ type ToWorker = {
|
|
|
44
49
|
* `ctx.playerId`. Absent for key joins, whose playerId stays the client id. */
|
|
45
50
|
playerId?: string;
|
|
46
51
|
reconnecting?: boolean;
|
|
52
|
+
/** D44: this join is a scripted NPC's loopback session. */
|
|
53
|
+
npc?: boolean;
|
|
47
54
|
} | {
|
|
48
55
|
t: 'leave';
|
|
49
56
|
clientId: string;
|
|
@@ -65,8 +72,29 @@ type ToWorker = {
|
|
|
65
72
|
} | {
|
|
66
73
|
t: 'inspect';
|
|
67
74
|
reqId: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* D41: arm (or re-arm) the authoritative timeline recorder, and read it back. Two messages
|
|
78
|
+
* rather than a flag on `inspect`, because recording is a room-lifetime decision and reading is
|
|
79
|
+
* a per-request one. Both are additive: a supervisor that never sends them costs a room nothing.
|
|
80
|
+
*/
|
|
81
|
+
| {
|
|
82
|
+
t: 'record';
|
|
83
|
+
reqId: number;
|
|
84
|
+
maxTicks?: number;
|
|
85
|
+
maxRecords?: number;
|
|
68
86
|
} | {
|
|
87
|
+
t: 'timeline';
|
|
88
|
+
reqId: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* D36: `reqId` correlates the answer, so a caller can *await* one room's counters instead of
|
|
92
|
+
* waiting for a periodic push. Optional and echoed back when present, so a host that asks
|
|
93
|
+
* without one still gets the uncorrelated message it expects.
|
|
94
|
+
*/
|
|
95
|
+
| {
|
|
69
96
|
t: 'stats';
|
|
97
|
+
reqId?: number;
|
|
70
98
|
}
|
|
71
99
|
/**
|
|
72
100
|
* Week 12: the supervisor answering a `hostCall` (a D24 save, a D25 KV operation). It arrives
|
|
@@ -82,6 +110,28 @@ type ToWorker = {
|
|
|
82
110
|
| {
|
|
83
111
|
t: 'alarm';
|
|
84
112
|
name: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* D59: a published message reaching a channel this room is subscribed to. A separate message
|
|
116
|
+
* from `alarm` because an alarm carries only a name, and the whole point of the bus is the
|
|
117
|
+
* payload. `from` is stamped by the supervisor off the publishing room's own record, so a room
|
|
118
|
+
* cannot forge a source.
|
|
119
|
+
*/
|
|
120
|
+
| {
|
|
121
|
+
t: 'busEvent';
|
|
122
|
+
channel: string;
|
|
123
|
+
from: string;
|
|
124
|
+
payload: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* D59: a directed `room.bus.send` being delivered. At-least-once: the supervisor re-arms an
|
|
128
|
+
* entry it could not deliver, so the same message can arrive twice.
|
|
129
|
+
*/
|
|
130
|
+
| {
|
|
131
|
+
t: 'busMessage';
|
|
132
|
+
name: string;
|
|
133
|
+
from: string;
|
|
134
|
+
payload: string;
|
|
85
135
|
} | {
|
|
86
136
|
t: 'stop';
|
|
87
137
|
};
|
|
@@ -98,6 +148,12 @@ type FromWorker = {
|
|
|
98
148
|
idleMs: number;
|
|
99
149
|
reconnectGraceMs: number;
|
|
100
150
|
maxClients: number;
|
|
151
|
+
/** D63-e: the room type's declared backfill eligibility. The ceiling on
|
|
152
|
+
* `room.backfill.set`, and `false` for every room that declares nothing. */
|
|
153
|
+
backfill?: boolean;
|
|
154
|
+
/** D47: the physics engine this room runs, when it declares one. Reported so room-hours
|
|
155
|
+
* rows carry the engine as a dimension; the supervisor never acts on it. */
|
|
156
|
+
engine?: string;
|
|
101
157
|
};
|
|
102
158
|
/** Present when `init.migrate` ran; the supervisor re-snapshots on the strength of it. */
|
|
103
159
|
migrated?: {
|
|
@@ -158,11 +214,56 @@ type FromWorker = {
|
|
|
158
214
|
}[];
|
|
159
215
|
/** Worker process RSS in bytes. */
|
|
160
216
|
rss: number;
|
|
217
|
+
}
|
|
218
|
+
/** D41: the recorder is armed. `ok: false` when there is no room core to arm. */
|
|
219
|
+
| {
|
|
220
|
+
t: 'recording';
|
|
221
|
+
reqId: number;
|
|
222
|
+
ok: boolean;
|
|
223
|
+
}
|
|
224
|
+
/** D41: the recorded timeline. `dump` absent when nobody armed the recorder. */
|
|
225
|
+
| {
|
|
226
|
+
t: 'timeline';
|
|
227
|
+
reqId: number;
|
|
228
|
+
dump?: TimelineDump;
|
|
161
229
|
} | {
|
|
162
230
|
t: 'stats';
|
|
231
|
+
/** Echoed from the request when it carried one (D36). */
|
|
232
|
+
reqId?: number;
|
|
163
233
|
stats: Omit<RoomStats, 'bytesOutByClient'> & {
|
|
164
234
|
bytesOutByClient: [string, number][];
|
|
165
235
|
};
|
|
236
|
+
/**
|
|
237
|
+
* D55: the worker thread's own V8 heap, sampled at reply time. It rides this existing
|
|
238
|
+
* round trip rather than adding a protocol surface, and it is deliberately not part of
|
|
239
|
+
* `RoomStats` — `RoomStats` is the room's own counters, and this is a fact about the
|
|
240
|
+
* thread the room happens to be running in.
|
|
241
|
+
*
|
|
242
|
+
* `limitBytes` is V8's `heap_size_limit`, which inside a worker reflects the
|
|
243
|
+
* `resourceLimits` the host spawned it with, so the supervisor does not have to remember
|
|
244
|
+
* what cap it handed out to know how close the room is to it. Absent when the worker's
|
|
245
|
+
* V8 could not be sampled, which the supervisor must report as unread rather than as
|
|
246
|
+
* zero, for the same reason `ticks`/`overruns` are optional.
|
|
247
|
+
*/
|
|
248
|
+
heap?: {
|
|
249
|
+
usedBytes: number;
|
|
250
|
+
limitBytes: number;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* D56: `true` once any Rapier world in this worker process has stepped, which is the point
|
|
254
|
+
* V8 has tier-compiled the physics WASM and paid its RSS transient. It rides `stats`
|
|
255
|
+
* because that round trip already exists; the worker also pushes one uncorrelated `stats`
|
|
256
|
+
* the first time it flips, so the supervisor learns it without having to be polling at the
|
|
257
|
+
* right moment. Absent means "not yet" (or an older worker), never "no".
|
|
258
|
+
*/
|
|
259
|
+
physicsStepped?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* D65: the room's bandwidth ledger, when it was started with one. It rides `stats` for the
|
|
262
|
+
* same reason D55's `heap` does — the round trip already exists — and stays off
|
|
263
|
+
* `RoomStats`, which is a fixed shape the harness and the hibernation path both pin.
|
|
264
|
+
* Absent means "this room is not profiling", never "no bytes".
|
|
265
|
+
*/
|
|
266
|
+
profile?: ProfileSnapshot;
|
|
166
267
|
}
|
|
167
268
|
/** Week 12: the room asked for host-side async work; the supervisor answers `hostResult`. */
|
|
168
269
|
| {
|
|
@@ -179,6 +280,58 @@ type FromWorker = {
|
|
|
179
280
|
t: 'setAlarm';
|
|
180
281
|
name: string;
|
|
181
282
|
atMs?: number;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* D63-e: `room.backfill.set(open)`. Fire-and-forget: the room gets no result, because the only
|
|
286
|
+
* thing that could go wrong is that the room never declared `backfill` at all, which is a fact
|
|
287
|
+
* the room already knows about itself.
|
|
288
|
+
*/
|
|
289
|
+
| {
|
|
290
|
+
t: 'setBackfill';
|
|
291
|
+
open: boolean;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* D59: `room.bus.publish(channel, payload)`. Fire-and-forget by design — the room gets no
|
|
295
|
+
* result, because the only honest answer is "delivered to whoever happened to be awake", and a
|
|
296
|
+
* refusal (an oversized payload, a rate limit) comes back as a log line rather than as a value
|
|
297
|
+
* the room would have no useful response to.
|
|
298
|
+
*/
|
|
299
|
+
| {
|
|
300
|
+
t: 'busPublish';
|
|
301
|
+
channel: string;
|
|
302
|
+
payload: string;
|
|
303
|
+
}
|
|
304
|
+
/** D59: `room.bus.subscribe`/`unsubscribe`, and the automatic subscribe of declared channels. */
|
|
305
|
+
| {
|
|
306
|
+
t: 'busSubscribe';
|
|
307
|
+
channel: string;
|
|
308
|
+
subscribed: boolean;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* D59: the room ran (or knowingly discarded) the mailbox entry called `name`.
|
|
312
|
+
*
|
|
313
|
+
* This is what clears the entry. The supervisor re-arms an entry for a visibility timeout at the
|
|
314
|
+
* moment it posts the `busMessage`, so a delivery that is posted but never runs comes due again
|
|
315
|
+
* instead of vanishing. Without this ack, at-least-once would be at-most-once wearing its name.
|
|
316
|
+
*/
|
|
317
|
+
| {
|
|
318
|
+
t: 'busAck';
|
|
319
|
+
name: string;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* D44: `room.spawnNPC(config)` / the handle's `despawn()`. The room minted `clientId`, so the
|
|
323
|
+
* handle is usable synchronously; the supervisor opens the loopback session under it and runs
|
|
324
|
+
* the named entry of the definition's `npcs` map — which it can, because it imported the same
|
|
325
|
+
* bundle to read the config out of. Only the *name* crosses this boundary; the function stays
|
|
326
|
+
* on the side that can call it. Additive, like every other message here.
|
|
327
|
+
*/
|
|
328
|
+
| {
|
|
329
|
+
t: 'spawnNpc';
|
|
330
|
+
clientId: string;
|
|
331
|
+
config: NpcConfig;
|
|
332
|
+
} | {
|
|
333
|
+
t: 'despawnNpc';
|
|
334
|
+
clientId: string;
|
|
182
335
|
} | {
|
|
183
336
|
t: 'stopped';
|
|
184
337
|
};
|
package/dist/worker/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-VMCE3LRO.js";
|
|
4
4
|
import {
|
|
5
5
|
RoomCore,
|
|
6
6
|
RoomFullError,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
initMatter,
|
|
8
|
+
initPhysics,
|
|
9
|
+
onFirstRapierStep,
|
|
10
|
+
rapierHasStepped
|
|
11
|
+
} from "../chunk-K42HA75G.js";
|
|
9
12
|
|
|
10
13
|
// src/worker/index.ts
|
|
14
|
+
import { getHeapStatistics } from "v8";
|
|
11
15
|
import { isMainThread, parentPort } from "worker_threads";
|
|
12
16
|
import { isRoomDefinition } from "@irtio/server";
|
|
13
17
|
|
|
@@ -60,6 +64,21 @@ function createWorkerHost(post) {
|
|
|
60
64
|
hostCall(reqId, call) {
|
|
61
65
|
post({ t: "hostCall", reqId, call });
|
|
62
66
|
},
|
|
67
|
+
spawnNpc(clientId, config) {
|
|
68
|
+
post({ t: "spawnNpc", clientId, config });
|
|
69
|
+
},
|
|
70
|
+
despawnNpc(clientId) {
|
|
71
|
+
post({ t: "despawnNpc", clientId });
|
|
72
|
+
},
|
|
73
|
+
busPublish(channel, payload) {
|
|
74
|
+
post({ t: "busPublish", channel, payload });
|
|
75
|
+
},
|
|
76
|
+
busSubscribe(channel, subscribed) {
|
|
77
|
+
post({ t: "busSubscribe", channel, subscribed });
|
|
78
|
+
},
|
|
79
|
+
setBackfill(open) {
|
|
80
|
+
post({ t: "setBackfill", open });
|
|
81
|
+
},
|
|
63
82
|
setAlarm(name, atMs) {
|
|
64
83
|
if (atMs === void 0) {
|
|
65
84
|
post({ t: "setAlarm", name });
|
|
@@ -91,9 +110,28 @@ async function loadChain(steps) {
|
|
|
91
110
|
}
|
|
92
111
|
return out;
|
|
93
112
|
}
|
|
94
|
-
function
|
|
113
|
+
function heapSample() {
|
|
114
|
+
try {
|
|
115
|
+
const s = getHeapStatistics();
|
|
116
|
+
if (!Number.isFinite(s.used_heap_size) || !Number.isFinite(s.heap_size_limit)) return void 0;
|
|
117
|
+
if (s.heap_size_limit <= 0) return void 0;
|
|
118
|
+
return { usedBytes: s.used_heap_size, limitBytes: s.heap_size_limit };
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function statsPayload(stats, reqId, profile) {
|
|
95
124
|
const { bytesOutByClient, ...rest } = stats;
|
|
96
|
-
|
|
125
|
+
const heap = heapSample();
|
|
126
|
+
return {
|
|
127
|
+
t: "stats",
|
|
128
|
+
...reqId !== void 0 ? { reqId } : {},
|
|
129
|
+
stats: { ...rest, bytesOutByClient: [...bytesOutByClient.entries()] },
|
|
130
|
+
...heap !== void 0 ? { heap } : {},
|
|
131
|
+
...rapierHasStepped() ? { physicsStepped: true } : {},
|
|
132
|
+
// D65: absent unless this room was started with a ledger.
|
|
133
|
+
...profile !== void 0 ? { profile } : {}
|
|
134
|
+
};
|
|
97
135
|
}
|
|
98
136
|
async function handleMessage(state, host, post, msg) {
|
|
99
137
|
try {
|
|
@@ -113,7 +151,14 @@ async function handleMessage(state, host, post, msg) {
|
|
|
113
151
|
}
|
|
114
152
|
if (def.config.physics) {
|
|
115
153
|
try {
|
|
116
|
-
await
|
|
154
|
+
if (def.config.physics.engine === "matter2d") await initMatter();
|
|
155
|
+
else {
|
|
156
|
+
await initPhysics();
|
|
157
|
+
onFirstRapierStep(() => {
|
|
158
|
+
const core = state.core;
|
|
159
|
+
if (core) post(statsPayload(core.stats, void 0, core.profile()));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
117
162
|
} catch (err) {
|
|
118
163
|
post({
|
|
119
164
|
t: "initFailed",
|
|
@@ -144,7 +189,8 @@ async function handleMessage(state, host, post, msg) {
|
|
|
144
189
|
const options = {
|
|
145
190
|
roomId: msg.roomId,
|
|
146
191
|
...msg.seed !== void 0 ? { seed: msg.seed } : {},
|
|
147
|
-
...msg.publicUrl !== void 0 ? { publicUrl: msg.publicUrl } : {}
|
|
192
|
+
...msg.publicUrl !== void 0 ? { publicUrl: msg.publicUrl } : {},
|
|
193
|
+
...msg.profile === true ? { profile: true } : {}
|
|
148
194
|
};
|
|
149
195
|
const core = snapshot ? RoomCore.restore(def, snapshot, host, options) : new RoomCore(def, host, options);
|
|
150
196
|
state.core = core;
|
|
@@ -160,7 +206,16 @@ async function handleMessage(state, host, post, msg) {
|
|
|
160
206
|
tickRate: c.tickRate,
|
|
161
207
|
idleMs: c.idleMs,
|
|
162
208
|
reconnectGraceMs: c.reconnectGraceMs,
|
|
163
|
-
maxClients: c.maxClients
|
|
209
|
+
maxClients: c.maxClients,
|
|
210
|
+
// D63-e: the declared ceiling, carried on the same frame and for the same reason
|
|
211
|
+
// `engine` is: `ready.config` replaces the supervisor's copy, so a flag threaded
|
|
212
|
+
// only through `loadBundle` would be dropped the moment a worker started.
|
|
213
|
+
backfill: c.backfill === true,
|
|
214
|
+
// D47: `ready.config` REPLACES the supervisor's copy of the bundle config, so an
|
|
215
|
+
// engine threaded only through `loadBundle` would be dropped here the moment a
|
|
216
|
+
// worker started. That is exactly how the first known-shape run read `engine: none`
|
|
217
|
+
// against a matter2d room.
|
|
218
|
+
...typeof c.physics?.engine === "string" ? { engine: c.physics.engine } : {}
|
|
164
219
|
},
|
|
165
220
|
...migrated ? {
|
|
166
221
|
migrated: {
|
|
@@ -192,7 +247,8 @@ async function handleMessage(state, host, post, msg) {
|
|
|
192
247
|
...msg.role !== void 0 ? { role: msg.role } : {},
|
|
193
248
|
...msg.name !== void 0 ? { name: msg.name } : {},
|
|
194
249
|
...msg.playerId !== void 0 ? { playerId: msg.playerId } : {},
|
|
195
|
-
...msg.reconnecting !== void 0 ? { reconnecting: msg.reconnecting } : {}
|
|
250
|
+
...msg.reconnecting !== void 0 ? { reconnecting: msg.reconnecting } : {},
|
|
251
|
+
...msg.npc === true ? { npc: true } : {}
|
|
196
252
|
};
|
|
197
253
|
const result = core.join(msg.clientId, joinOptions);
|
|
198
254
|
const snapshot = ownBuffer(result.snapshot);
|
|
@@ -253,10 +309,28 @@ async function handleMessage(state, host, post, msg) {
|
|
|
253
309
|
});
|
|
254
310
|
return;
|
|
255
311
|
}
|
|
312
|
+
case "record": {
|
|
313
|
+
const core = state.core;
|
|
314
|
+
if (!core) {
|
|
315
|
+
post({ t: "recording", reqId: msg.reqId, ok: false });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
core.startRecording({
|
|
319
|
+
...msg.maxTicks !== void 0 ? { maxTicks: msg.maxTicks } : {},
|
|
320
|
+
...msg.maxRecords !== void 0 ? { maxRecords: msg.maxRecords } : {}
|
|
321
|
+
});
|
|
322
|
+
post({ t: "recording", reqId: msg.reqId, ok: true });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
case "timeline": {
|
|
326
|
+
const dump = state.core?.recording();
|
|
327
|
+
post({ t: "timeline", reqId: msg.reqId, ...dump !== void 0 ? { dump } : {} });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
256
330
|
case "stats": {
|
|
257
331
|
const core = state.core;
|
|
258
332
|
if (!core) return;
|
|
259
|
-
post(statsPayload(core.stats));
|
|
333
|
+
post(statsPayload(core.stats, msg.reqId, core.profile()));
|
|
260
334
|
return;
|
|
261
335
|
}
|
|
262
336
|
case "hostResult": {
|
|
@@ -267,6 +341,18 @@ async function handleMessage(state, host, post, msg) {
|
|
|
267
341
|
state.core?.fireAlarm(msg.name);
|
|
268
342
|
return;
|
|
269
343
|
}
|
|
344
|
+
case "busEvent": {
|
|
345
|
+
state.core?.deliverBusEvent(msg.channel, msg.from, msg.payload);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
case "busMessage": {
|
|
349
|
+
try {
|
|
350
|
+
state.core?.deliverBusMessage(msg.from, msg.payload);
|
|
351
|
+
} finally {
|
|
352
|
+
post({ t: "busAck", name: msg.name });
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
270
356
|
case "stop": {
|
|
271
357
|
state.core?.stop();
|
|
272
358
|
post({ t: "stopped" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
32
|
-
"
|
|
33
|
-
"@irtio/
|
|
34
|
-
"@irtio/
|
|
32
|
+
"matter-js": "0.20.0",
|
|
33
|
+
"@irtio/protocol": "0.6.0",
|
|
34
|
+
"@irtio/schema": "0.6.0",
|
|
35
|
+
"@irtio/server": "0.6.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/matter-js": "0.20.2"
|
|
35
39
|
},
|
|
36
40
|
"scripts": {
|
|
37
41
|
"build": "tsup",
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { AnySchema, PlainState, State, Tracked } from '@irtio/schema';
|
|
2
|
-
import { ErrorCodeName } from '@irtio/protocol';
|
|
3
|
-
import { RoomDefinition, Room, LeaveReason } from '@irtio/server';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Live inspection for the dev page (`irtio dev`) and the supervisor admin API: a JSON view of
|
|
7
|
-
* the room's state plus a ring of recent events. Always on; recording is O(1) per event.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
type RoomEventKind = 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'alarm' | 'error';
|
|
11
|
-
interface RoomEvent {
|
|
12
|
-
readonly tick: number;
|
|
13
|
-
readonly kind: RoomEventKind;
|
|
14
|
-
readonly clientId?: string;
|
|
15
|
-
readonly detail?: string;
|
|
16
|
-
}
|
|
17
|
-
declare const EVENT_RING_SIZE = 100;
|
|
18
|
-
/** JSON-able view of a plain state: entities as `{id: {owner, value}}`, singletons as values. */
|
|
19
|
-
declare function inspectState(schema: AnySchema, plain: PlainState): Record<string, unknown>;
|
|
20
|
-
interface RoomInspection {
|
|
21
|
-
readonly tick: number;
|
|
22
|
-
readonly state: Record<string, unknown>;
|
|
23
|
-
readonly recent: readonly RoomEvent[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type LogLevel = 'info' | 'warn' | 'error';
|
|
27
|
-
/**
|
|
28
|
-
* Week 12: host-side async work a room asked for — a save generation (D24) or a player-KV
|
|
29
|
-
* operation (D25). Room handlers are synchronous and there is no way to block one on I/O, so
|
|
30
|
-
* every one of these is started by `RoomHost.hostCall` and *finished* by the host calling
|
|
31
|
-
* `RoomCoreApi.completeHostCall` — the same way an inbound frame arrives. That is what makes
|
|
32
|
-
* "the continuation runs as its own event between ticks" a structural property of the seam
|
|
33
|
-
* rather than a promise the runtime makes and hopes the host keeps.
|
|
34
|
-
*/
|
|
35
|
-
type HostCall = {
|
|
36
|
-
readonly kind: 'save';
|
|
37
|
-
} | {
|
|
38
|
-
readonly kind: 'kvGet';
|
|
39
|
-
readonly playerId: string;
|
|
40
|
-
readonly key: string;
|
|
41
|
-
} | {
|
|
42
|
-
readonly kind: 'kvSet';
|
|
43
|
-
readonly playerId: string;
|
|
44
|
-
readonly key: string;
|
|
45
|
-
readonly value: string;
|
|
46
|
-
} | {
|
|
47
|
-
readonly kind: 'kvDelete';
|
|
48
|
-
readonly playerId: string;
|
|
49
|
-
readonly key: string;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* What a `HostCall` produced. `value` carries the save id for `save` and the stored string for
|
|
53
|
-
* `kvGet` (absent = the key was never written, which resolves `undefined` rather than rejecting).
|
|
54
|
-
* A failure carries a named `E_*` code so the room can tell a size limit from an outage.
|
|
55
|
-
*/
|
|
56
|
-
type HostCallResult = {
|
|
57
|
-
readonly ok: true;
|
|
58
|
-
readonly value?: string;
|
|
59
|
-
} | {
|
|
60
|
-
readonly ok: false;
|
|
61
|
-
readonly code: string;
|
|
62
|
-
readonly message: string;
|
|
63
|
-
};
|
|
64
|
-
/** How long a `HostCall` may stay outstanding before the runtime rejects it itself. */
|
|
65
|
-
declare const HOST_CALL_TIMEOUT_MS = 10000;
|
|
66
|
-
/** Everything the room needs from the outside world. No timers, sockets, clocks, or console inside. */
|
|
67
|
-
interface RoomHost {
|
|
68
|
-
/** Monotonic ms. Fake in the harness. */
|
|
69
|
-
now(): number;
|
|
70
|
-
setTimeout(fn: () => void, ms: number): unknown;
|
|
71
|
-
clearTimeout(handle: unknown): void;
|
|
72
|
-
/** Deliver one fully framed protocol frame (type byte + payload) to a client. */
|
|
73
|
-
send(clientId: string, frame: Uint8Array): void;
|
|
74
|
-
/** The room wants this client gone (`room.kick`, protocol violation). Host closes the socket. */
|
|
75
|
-
kick(clientId: string, code: ErrorCodeName, reason?: string): void;
|
|
76
|
-
/** `room.close()`: host disconnects everyone and tears the room down. */
|
|
77
|
-
close(reason?: string): void;
|
|
78
|
-
/** Event mode: idle or `room.sleep()` — host should `serialize()` then `stop()`. */
|
|
79
|
-
sleep(): void;
|
|
80
|
-
log(level: LogLevel, args: unknown[]): void;
|
|
81
|
-
/** N consecutive tick throws; the runtime does not recover itself. */
|
|
82
|
-
crashed(reason: string): void;
|
|
83
|
-
/**
|
|
84
|
-
* Week 12: start host-side async work (D24 save, D25 KV). The host **must** deliver the
|
|
85
|
-
* outcome by calling `RoomCoreApi.completeHostCall(reqId, result)` from its own event loop
|
|
86
|
-
* turn, never synchronously from inside this call — a handler is on the stack.
|
|
87
|
-
*/
|
|
88
|
-
hostCall(reqId: number, call: HostCall): void;
|
|
89
|
-
/**
|
|
90
|
-
* D26: arm the durable alarm `name` for `atMs`, or cancel it when `atMs` is `undefined`.
|
|
91
|
-
* Arming a name that is already armed replaces its due time. Alarm state is the **host's**,
|
|
92
|
-
* not the room's: nothing about it goes into the hibernation blob, and the host fires one by
|
|
93
|
-
* calling `RoomCoreApi.fireAlarm(name)`.
|
|
94
|
-
*/
|
|
95
|
-
setAlarm(name: string, atMs: number | undefined): void;
|
|
96
|
-
}
|
|
97
|
-
interface RoomCoreOptions {
|
|
98
|
-
readonly roomId: string;
|
|
99
|
-
/** Seed for `room.random()`; default 1. */
|
|
100
|
-
readonly seed?: number;
|
|
101
|
-
/** Base URL for `room.link` (`<publicUrl>?room=<id>`); default `http://localhost/`. */
|
|
102
|
-
readonly publicUrl?: string;
|
|
103
|
-
/** Bytes from a previous `serialize()`; skips `onCreate`, runs `onWake`. */
|
|
104
|
-
readonly restoreFrom?: Uint8Array;
|
|
105
|
-
}
|
|
106
|
-
interface JoinOptions {
|
|
107
|
-
/** Requested role; unknown/missing → `roles[0]` (logged) or `''` when the schema has no roles. */
|
|
108
|
-
readonly role?: string;
|
|
109
|
-
readonly name?: string;
|
|
110
|
-
/** A resumed session: presence record kept, `ctx.reconnecting = true`. */
|
|
111
|
-
readonly reconnecting?: boolean;
|
|
112
|
-
/**
|
|
113
|
-
* D25: the identity `ctx.playerId` reports and `room.kv` is meant to be keyed by. Defaults to
|
|
114
|
-
* `clientId`, which — because a resume token carries the client id across a reconnect — is
|
|
115
|
-
* exactly the resume-token identity, and no more stable than that token. Week 13 (D27) passes
|
|
116
|
-
* a JWT subject here instead without anything else changing.
|
|
117
|
-
*/
|
|
118
|
-
readonly playerId?: string;
|
|
119
|
-
}
|
|
120
|
-
interface JoinResult {
|
|
121
|
-
readonly tick: number;
|
|
122
|
-
readonly role: string;
|
|
123
|
-
/** Snapshot bytes for this client's view (extended schema). The host builds `WELCOME`. */
|
|
124
|
-
readonly snapshot: Uint8Array;
|
|
125
|
-
}
|
|
126
|
-
declare class RoomFullError extends Error {
|
|
127
|
-
readonly maxClients: number;
|
|
128
|
-
readonly name = "RoomFullError";
|
|
129
|
-
constructor(maxClients: number);
|
|
130
|
-
}
|
|
131
|
-
interface RoomStats {
|
|
132
|
-
ticks: number;
|
|
133
|
-
lastTickMs: number;
|
|
134
|
-
maxTickMs: number;
|
|
135
|
-
overruns: number;
|
|
136
|
-
framesIn: number;
|
|
137
|
-
framesOut: number;
|
|
138
|
-
bytesOut: number;
|
|
139
|
-
/** Per connected client. */
|
|
140
|
-
bytesOutByClient: Map<string, number>;
|
|
141
|
-
handlerErrors: number;
|
|
142
|
-
encodesLastFlush: number;
|
|
143
|
-
aoiEncodesLastFlush: number;
|
|
144
|
-
gridBuildMs: number;
|
|
145
|
-
gridQueryMs: number;
|
|
146
|
-
aoiEncodeMs: number;
|
|
147
|
-
visibleIdsTotal: number;
|
|
148
|
-
membershipEnters: number;
|
|
149
|
-
membershipLeaves: number;
|
|
150
|
-
corrections: number;
|
|
151
|
-
}
|
|
152
|
-
/** The surface the worker host and the harness drive. Implemented by `RoomCore`. */
|
|
153
|
-
interface RoomCoreApi<S extends AnySchema = AnySchema> {
|
|
154
|
-
readonly definition: RoomDefinition<S>;
|
|
155
|
-
/** The builder's schema. */
|
|
156
|
-
readonly schema: S;
|
|
157
|
-
/** `withBuiltins(schema)`: what the codec and every frame use. */
|
|
158
|
-
readonly ext: AnySchema;
|
|
159
|
-
readonly roomId: string;
|
|
160
|
-
readonly tick: number;
|
|
161
|
-
/** Tracked authority (proxy tree incl. the built-in `clients` collection). */
|
|
162
|
-
readonly state: State<S>;
|
|
163
|
-
readonly tracked: Tracked<AnySchema>;
|
|
164
|
-
/** The `room` API object handed to handlers. */
|
|
165
|
-
readonly room: Room<S>;
|
|
166
|
-
readonly stats: RoomStats;
|
|
167
|
-
readonly mode: 'tick' | 'event';
|
|
168
|
-
/** Begin ticking (tick mode) / idle timer (event mode). Idempotent. */
|
|
169
|
-
start(): void;
|
|
170
|
-
/** Stop timers; no more frames are sent. */
|
|
171
|
-
stop(): void;
|
|
172
|
-
/** Throws `RoomFullError`. Runs `onJoin`, then snapshots the client's view. */
|
|
173
|
-
join(clientId: string, options?: JoinOptions): JoinResult;
|
|
174
|
-
/** Runs `onLeave`, removes presence, rejects pending RPCs for the client. */
|
|
175
|
-
leave(clientId: string, reason: LeaveReason): void;
|
|
176
|
-
/** Grace window opened by the host: presence `connected = false`; frames are no longer sent. */
|
|
177
|
-
markDisconnected(clientId: string): void;
|
|
178
|
-
/** One inbound protocol frame (`WRITE` | `CALL` | `REPLY` | `MSG`). Anything else → host.kick(E_BAD_FRAME). */
|
|
179
|
-
receive(clientId: string, frame: Uint8Array): void;
|
|
180
|
-
/** Full state + tick + rng for hibernation. Runs `onSleep`. Clears timers. */
|
|
181
|
-
serialize(): Uint8Array;
|
|
182
|
-
/**
|
|
183
|
-
* D24: the same bytes `serialize()` produces, with none of its side effects — the room carries
|
|
184
|
-
* on exactly as it was. This is what a save generation is written from; `serialize()` is what
|
|
185
|
-
* a room going to sleep is written from.
|
|
186
|
-
*/
|
|
187
|
-
snapshot(): Uint8Array;
|
|
188
|
-
/** Live JSON view of the room (dev page / supervisor admin API). */
|
|
189
|
-
inspect(): RoomInspection;
|
|
190
|
-
/**
|
|
191
|
-
* Week 12: settles the promise `hostCall(reqId, …)` returned. The host calls this from its own
|
|
192
|
-
* turn; the runtime runs the continuation as a discrete event and flushes afterwards, so state
|
|
193
|
-
* a continuation mutated goes out exactly like state a handler mutated.
|
|
194
|
-
*/
|
|
195
|
-
completeHostCall(reqId: number, result: HostCallResult): void;
|
|
196
|
-
/**
|
|
197
|
-
* D26: runs the `alarms[name]` handler as its own event between ticks, then flushes. Unknown
|
|
198
|
-
* names are logged and ignored — a room that dropped a handler in a redeploy must not crash on
|
|
199
|
-
* an alarm armed by the version before it.
|
|
200
|
-
*/
|
|
201
|
-
fireAlarm(name: string): void;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export { EVENT_RING_SIZE as E, HOST_CALL_TIMEOUT_MS as H, type JoinOptions as J, type LogLevel as L, type RoomCoreApi as R, type HostCall as a, type HostCallResult as b, type JoinResult as c, type RoomCoreOptions as d, type RoomEvent as e, type RoomEventKind as f, RoomFullError as g, type RoomHost as h, type RoomInspection as i, type RoomStats as j, inspectState as k };
|