@irtio/server 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 +129 -0
- package/dist/index.js +99 -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,129 @@
|
|
|
1
|
+
import { AnySchema, RoleOf, ServerCallProxy, SchemaRpc, BroadcastProxy, State, OwnableKeys, DeepReadonly, SchemaDefs, InstanceOf, Implementations, ServerRpcs } from '@irtio/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@irtio/server` — what `irtio/room.ts` imports. `defineRoom(schema, config)` validates the
|
|
5
|
+
* config, fills defaults, and returns a plain `RoomDefinition` the runtime loads from the bundle.
|
|
6
|
+
* No behaviour lives here; everything is types plus definition-time checks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** `'all'` (everyone but the sender for relayed messages), a client id, or a role. */
|
|
10
|
+
type MessageTarget = 'all' | string | {
|
|
11
|
+
readonly role: string;
|
|
12
|
+
};
|
|
13
|
+
interface ClientInfo {
|
|
14
|
+
readonly clientId: string;
|
|
15
|
+
readonly role: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly connected: boolean;
|
|
18
|
+
}
|
|
19
|
+
type TimerHandle = number;
|
|
20
|
+
interface Room<S extends AnySchema = AnySchema> {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** Shareable URL (`?room=<id>`). */
|
|
23
|
+
readonly link: string;
|
|
24
|
+
readonly tick: number;
|
|
25
|
+
/** Server ms; prefer over `Date.now()` (replayable). */
|
|
26
|
+
readonly now: number;
|
|
27
|
+
/** Built-in presence, ordered by join. */
|
|
28
|
+
readonly clients: readonly ClientInfo[];
|
|
29
|
+
/** Seeded, recorded for replay. */
|
|
30
|
+
random(): number;
|
|
31
|
+
send(target: MessageTarget, bytes: Uint8Array): void;
|
|
32
|
+
setRole(clientId: string, role: RoleOf<S> & string): void;
|
|
33
|
+
kick(clientId: string, reason?: string): void;
|
|
34
|
+
close(reason?: string): void;
|
|
35
|
+
/** Event mode: ask to hibernate now. */
|
|
36
|
+
sleep(): void;
|
|
37
|
+
log(...args: unknown[]): void;
|
|
38
|
+
/** Live-only timers (cancelled on hibernation). */
|
|
39
|
+
setTimeout(ms: number, fn: () => void): TimerHandle;
|
|
40
|
+
setInterval(ms: number, fn: () => void): TimerHandle;
|
|
41
|
+
clearTimeout(handle: TimerHandle): void;
|
|
42
|
+
clearInterval(handle: TimerHandle): void;
|
|
43
|
+
/** Server → client RPC (Promise; 5 s default timeout; rejects on disconnect). */
|
|
44
|
+
call(clientId: string): ServerCallProxy<SchemaRpc<S>>;
|
|
45
|
+
/** Server → client void RPCs to every connected client. */
|
|
46
|
+
readonly broadcast: BroadcastProxy<SchemaRpc<S>>;
|
|
47
|
+
}
|
|
48
|
+
interface Ctx<S extends AnySchema = AnySchema> {
|
|
49
|
+
readonly clientId: string;
|
|
50
|
+
readonly role: RoleOf<S> & string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** Server tick this join/call/write is applied at. */
|
|
53
|
+
readonly tick: number;
|
|
54
|
+
/** Joins only: a resumed session. */
|
|
55
|
+
readonly reconnecting: boolean;
|
|
56
|
+
readonly room: Room<S>;
|
|
57
|
+
}
|
|
58
|
+
type LeaveReason = 'left' | 'timeout' | 'kicked' | 'closed';
|
|
59
|
+
type RoomMode = 'tick' | 'event';
|
|
60
|
+
type Instance<S extends AnySchema, K extends keyof SchemaDefs<S>> = InstanceOf<SchemaDefs<S>[K]>;
|
|
61
|
+
/** Per-entity owner-write validators: return `next` (accept), `prev` (reject), or a clamped object. */
|
|
62
|
+
type Validators<S extends AnySchema> = {
|
|
63
|
+
readonly [K in OwnableKeys<S>]?: (prev: DeepReadonly<Instance<S, K>>, next: DeepReadonly<Instance<S, K>>, ctx: Ctx<S>) => Instance<S, K> | DeepReadonly<Instance<S, K>>;
|
|
64
|
+
};
|
|
65
|
+
type RpcImplementations<S extends AnySchema> = Implementations<ServerRpcs<SchemaRpc<S>>, State<S>, Ctx<S>>;
|
|
66
|
+
type RpcConfig<S extends AnySchema> = keyof ServerRpcs<SchemaRpc<S>> extends never ? {
|
|
67
|
+
readonly rpc?: RpcImplementations<S>;
|
|
68
|
+
} : {
|
|
69
|
+
readonly rpc: RpcImplementations<S>;
|
|
70
|
+
};
|
|
71
|
+
interface RoomConfigBase<S extends AnySchema> {
|
|
72
|
+
/** `'tick'` (fixed-rate loop; default) or `'event'` (apply on arrival, hibernates). */
|
|
73
|
+
readonly mode?: RoomMode;
|
|
74
|
+
/** Tick mode only; default 20. */
|
|
75
|
+
readonly tickRate?: number;
|
|
76
|
+
/** Event mode: hibernate after this long with no frames; default 30 000. */
|
|
77
|
+
readonly idleMs?: number;
|
|
78
|
+
/** Reconnection grace window; default 30 000. */
|
|
79
|
+
readonly reconnectGraceMs?: number;
|
|
80
|
+
/** Default 64. */
|
|
81
|
+
readonly maxClients?: number;
|
|
82
|
+
onCreate?(state: State<S>, room: Room<S>): void;
|
|
83
|
+
onJoin?(state: State<S>, ctx: Ctx<S>): void;
|
|
84
|
+
onLeave?(state: State<S>, ctx: Ctx<S>, reason: LeaveReason): void;
|
|
85
|
+
onSleep?(state: State<S>, room: Room<S>): void;
|
|
86
|
+
onWake?(state: State<S>, room: Room<S>): void;
|
|
87
|
+
/** Tick mode: fixed `dt` in seconds; queued writes/calls already applied. */
|
|
88
|
+
tick?(state: State<S>, dt: number, room: Room<S>): void;
|
|
89
|
+
readonly validate?: Validators<S>;
|
|
90
|
+
/** Implements the built-in `requestOwnership` RPC. Default: grant if unowned. */
|
|
91
|
+
onOwnershipRequest?(state: State<S>, entity: OwnableKeys<S> & string, id: string, ctx: Ctx<S>): boolean;
|
|
92
|
+
/** Raw relay messages; return `false` to drop. */
|
|
93
|
+
onMessage?(state: State<S>, from: string, target: MessageTarget, bytes: Uint8Array, ctx: Ctx<S>): boolean | undefined | void;
|
|
94
|
+
}
|
|
95
|
+
type RoomConfig<S extends AnySchema> = RoomConfigBase<S> & RpcConfig<S>;
|
|
96
|
+
/** Config with defaults filled and `rpc` always present. */
|
|
97
|
+
interface ResolvedRoomConfig<S extends AnySchema> extends RoomConfigBase<S> {
|
|
98
|
+
readonly mode: RoomMode;
|
|
99
|
+
readonly tickRate: number;
|
|
100
|
+
readonly idleMs: number;
|
|
101
|
+
readonly reconnectGraceMs: number;
|
|
102
|
+
readonly maxClients: number;
|
|
103
|
+
readonly rpc: RpcImplementations<S>;
|
|
104
|
+
}
|
|
105
|
+
interface RoomDefinition<S extends AnySchema = AnySchema> {
|
|
106
|
+
readonly kind: 'irtio-room';
|
|
107
|
+
readonly version: 1;
|
|
108
|
+
readonly schema: S;
|
|
109
|
+
readonly config: ResolvedRoomConfig<S>;
|
|
110
|
+
}
|
|
111
|
+
declare const ROOM_DEFINITION_VERSION: 1;
|
|
112
|
+
declare const DEFAULTS: {
|
|
113
|
+
readonly mode: "tick";
|
|
114
|
+
readonly tickRate: 20;
|
|
115
|
+
readonly idleMs: 30000;
|
|
116
|
+
readonly reconnectGraceMs: 30000;
|
|
117
|
+
readonly maxClients: 64;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Validates a room config and returns the definition the runtime loads. Throws on: unknown
|
|
121
|
+
* mode, bad tickRate, `tick` missing in tick mode or present in event mode, `async`/generator
|
|
122
|
+
* handlers (handlers are synchronous), and `rpc` keys that don't match the schema's
|
|
123
|
+
* server RPCs exactly.
|
|
124
|
+
*/
|
|
125
|
+
declare function defineRoom<S extends AnySchema>(schema: S, config: RoomConfig<S>): RoomDefinition<S>;
|
|
126
|
+
/** Type guard for what a bundle's default export should be. */
|
|
127
|
+
declare function isRoomDefinition(v: unknown): v is RoomDefinition;
|
|
128
|
+
|
|
129
|
+
export { type ClientInfo, type Ctx, DEFAULTS, type LeaveReason, type MessageTarget, ROOM_DEFINITION_VERSION, type ResolvedRoomConfig, type Room, type RoomConfig, type RoomConfigBase, type RoomDefinition, type RoomMode, type RpcImplementations, type TimerHandle, type Validators, defineRoom, isRoomDefinition };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var ROOM_DEFINITION_VERSION = 1;
|
|
3
|
+
var DEFAULTS = {
|
|
4
|
+
mode: "tick",
|
|
5
|
+
tickRate: 20,
|
|
6
|
+
idleMs: 3e4,
|
|
7
|
+
reconnectGraceMs: 3e4,
|
|
8
|
+
maxClients: 64
|
|
9
|
+
};
|
|
10
|
+
var HANDLER_KEYS = [
|
|
11
|
+
"onCreate",
|
|
12
|
+
"onJoin",
|
|
13
|
+
"onLeave",
|
|
14
|
+
"onSleep",
|
|
15
|
+
"onWake",
|
|
16
|
+
"tick",
|
|
17
|
+
"onOwnershipRequest",
|
|
18
|
+
"onMessage"
|
|
19
|
+
];
|
|
20
|
+
function defineRoom(schema, config) {
|
|
21
|
+
if (!schema || typeof schema !== "object" || !Array.isArray(schema.collections)) {
|
|
22
|
+
throw new Error("defineRoom: first argument must be a schema from defineSchema()");
|
|
23
|
+
}
|
|
24
|
+
const mode = config.mode ?? DEFAULTS.mode;
|
|
25
|
+
if (mode !== "tick" && mode !== "event") {
|
|
26
|
+
throw new Error(`defineRoom: mode must be 'tick' or 'event', got ${JSON.stringify(mode)}`);
|
|
27
|
+
}
|
|
28
|
+
const tickRate = config.tickRate ?? DEFAULTS.tickRate;
|
|
29
|
+
if (!Number.isInteger(tickRate) || tickRate < 1 || tickRate > 240) {
|
|
30
|
+
throw new Error(`defineRoom: tickRate must be an integer in 1..240, got ${String(tickRate)}`);
|
|
31
|
+
}
|
|
32
|
+
for (const k of ["idleMs", "reconnectGraceMs", "maxClients"]) {
|
|
33
|
+
const v = config[k];
|
|
34
|
+
if (v !== void 0 && (!Number.isFinite(v) || v < 0)) {
|
|
35
|
+
throw new Error(`defineRoom: ${k} must be a non-negative number, got ${String(v)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (mode === "tick" && typeof config.tick !== "function") {
|
|
39
|
+
throw new Error("defineRoom: tick(state, dt, room) is required in 'tick' mode");
|
|
40
|
+
}
|
|
41
|
+
if (mode === "event" && config.tick !== void 0) {
|
|
42
|
+
throw new Error("defineRoom: tick() is not allowed in 'event' mode (use RPCs and timers)");
|
|
43
|
+
}
|
|
44
|
+
for (const k of HANDLER_KEYS) assertSyncHandler(config[k], k);
|
|
45
|
+
const rpc = config.rpc ?? {};
|
|
46
|
+
const validate = config.validate ?? {};
|
|
47
|
+
for (const [k, fn] of Object.entries(rpc)) assertSyncHandler(fn, `rpc.${k}`);
|
|
48
|
+
for (const [k, fn] of Object.entries(validate)) assertSyncHandler(fn, `validate.${k}`);
|
|
49
|
+
const expected = schema.rpcs.filter((r) => r.direction === "server").map((r) => r.name).sort();
|
|
50
|
+
const given = Object.keys(rpc).sort();
|
|
51
|
+
const missing = expected.filter((n) => !given.includes(n));
|
|
52
|
+
const extra = given.filter((n) => !expected.includes(n));
|
|
53
|
+
if (missing.length || extra.length) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (missing.length) parts.push(`missing implementations: ${missing.join(", ")}`);
|
|
56
|
+
if (extra.length) parts.push(`unknown rpc keys: ${extra.join(", ")}`);
|
|
57
|
+
throw new Error(`defineRoom: rpc ${parts.join("; ")}`);
|
|
58
|
+
}
|
|
59
|
+
for (const [k] of Object.entries(validate)) {
|
|
60
|
+
const c = schema.collections.find((x) => x.name === k);
|
|
61
|
+
if (!c || c.kind !== "entity" || c.serverOwned) {
|
|
62
|
+
throw new Error(`defineRoom: validate.${k} must name an instance-owned entity collection`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const resolved = {
|
|
66
|
+
...config,
|
|
67
|
+
mode,
|
|
68
|
+
tickRate,
|
|
69
|
+
idleMs: config.idleMs ?? DEFAULTS.idleMs,
|
|
70
|
+
reconnectGraceMs: config.reconnectGraceMs ?? DEFAULTS.reconnectGraceMs,
|
|
71
|
+
maxClients: config.maxClients ?? DEFAULTS.maxClients,
|
|
72
|
+
rpc
|
|
73
|
+
};
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
kind: "irtio-room",
|
|
76
|
+
version: ROOM_DEFINITION_VERSION,
|
|
77
|
+
schema,
|
|
78
|
+
config: Object.freeze(resolved)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function assertSyncHandler(fn, name) {
|
|
82
|
+
if (fn === void 0) return;
|
|
83
|
+
if (typeof fn !== "function") throw new Error(`defineRoom: ${name} must be a function`);
|
|
84
|
+
const ctor = fn.constructor?.name;
|
|
85
|
+
if (ctor === "AsyncFunction" || ctor === "AsyncGeneratorFunction" || ctor === "GeneratorFunction") {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`defineRoom: ${name} is ${ctor}; handlers are synchronous (no await inside a tick) \u2014 use RPCs, timers, or room.call() continuations instead`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isRoomDefinition(v) {
|
|
92
|
+
return typeof v === "object" && v !== null && v.kind === "irtio-room" && v.version === ROOM_DEFINITION_VERSION;
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
DEFAULTS,
|
|
96
|
+
ROOM_DEFINITION_VERSION,
|
|
97
|
+
defineRoom,
|
|
98
|
+
isRoomDefinition
|
|
99
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@irtio/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "irtio room-file API: defineRoom, handler and ctx types",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@irtio/schema": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"test": "vitest run"
|
|
28
|
+
}
|
|
29
|
+
}
|