@vgai/p2p-colyseus 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 +202 -0
- package/README.md +125 -0
- package/package.json +51 -0
- package/src/access-token.ts +74 -0
- package/src/browser.ts +202 -0
- package/src/callbacks.ts +7 -0
- package/src/channels.ts +12 -0
- package/src/client.ts +480 -0
- package/src/cloudflare/coordinator.ts +383 -0
- package/src/cloudflare/durable-room.ts +377 -0
- package/src/cloudflare/limits.ts +22 -0
- package/src/cloudflare/protocol.ts +75 -0
- package/src/cloudflare/worker.ts +110 -0
- package/src/codec.ts +78 -0
- package/src/diagnostics.ts +55 -0
- package/src/engine.ts +680 -0
- package/src/events.ts +58 -0
- package/src/index.ts +196 -0
- package/src/loopback.ts +65 -0
- package/src/node-websocket.ts +77 -0
- package/src/platform.ts +866 -0
- package/src/protocol.ts +56 -0
- package/src/relay.ts +60 -0
- package/src/replication.ts +164 -0
- package/src/room.ts +282 -0
- package/src/runtime.ts +371 -0
- package/src/schema.ts +287 -0
- package/src/webrtc.ts +78 -0
- package/src/websocket.ts +59 -0
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface Snapshot {
|
|
2
|
+
readonly type: 'snapshot';
|
|
3
|
+
readonly state: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface StatePatch {
|
|
7
|
+
readonly type: 'patch';
|
|
8
|
+
readonly operations: readonly StatePatchOperation[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type StatePatchOperation =
|
|
12
|
+
| { readonly op: 'set'; readonly path: readonly string[]; readonly value: unknown }
|
|
13
|
+
| { readonly op: 'delete'; readonly path: readonly string[] };
|
|
14
|
+
|
|
15
|
+
export type Envelope =
|
|
16
|
+
| {
|
|
17
|
+
readonly kind: 'join';
|
|
18
|
+
readonly requestId: string;
|
|
19
|
+
readonly room: string;
|
|
20
|
+
readonly options?: unknown;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
readonly kind: 'join-ok';
|
|
24
|
+
readonly requestId: string;
|
|
25
|
+
readonly sessionId: string;
|
|
26
|
+
readonly snapshot: Snapshot;
|
|
27
|
+
readonly clock: number;
|
|
28
|
+
}
|
|
29
|
+
| { readonly kind: 'join-error'; readonly requestId: string; readonly message: string }
|
|
30
|
+
| { readonly kind: 'message'; readonly type: string; readonly payload?: unknown }
|
|
31
|
+
| { readonly kind: 'state-patch'; readonly patch: StatePatch; readonly clock: number }
|
|
32
|
+
| { readonly kind: 'state-snapshot'; readonly snapshot: Snapshot; readonly clock: number }
|
|
33
|
+
| { readonly kind: 'state-resync'; readonly clock: number }
|
|
34
|
+
| { readonly kind: 'leave'; readonly code?: number; readonly reason?: string }
|
|
35
|
+
| { readonly kind: 'ping'; readonly t: number }
|
|
36
|
+
| { readonly kind: 'pong'; readonly t: number };
|
|
37
|
+
|
|
38
|
+
export const P2P_CLOSE_CODES = {
|
|
39
|
+
badEnvelope: 4400,
|
|
40
|
+
unauthorized: 4401,
|
|
41
|
+
roomNotFound: 4404,
|
|
42
|
+
joinTimeout: 4408,
|
|
43
|
+
envelopeTooLarge: 4413,
|
|
44
|
+
rateLimited: 4429,
|
|
45
|
+
relayQuotaExceeded: 4430,
|
|
46
|
+
roomFull: 4431,
|
|
47
|
+
hostMissing: 4432,
|
|
48
|
+
relayNotAllowed: 4433,
|
|
49
|
+
internalError: 4500,
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
export type P2PCloseCode = (typeof P2P_CLOSE_CODES)[keyof typeof P2P_CLOSE_CODES];
|
|
53
|
+
|
|
54
|
+
export function isEnvelope(value: unknown): value is Envelope {
|
|
55
|
+
return typeof value === 'object' && value !== null && 'kind' in value;
|
|
56
|
+
}
|
package/src/relay.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PacketChannel } from './channels';
|
|
2
|
+
import type { SignalingRelayCoordinator } from './cloudflare/coordinator';
|
|
3
|
+
import type { Envelope } from './protocol';
|
|
4
|
+
|
|
5
|
+
type EnvelopeHandler = (envelope: Envelope) => void;
|
|
6
|
+
type CloseHandler = (reason?: string) => void;
|
|
7
|
+
|
|
8
|
+
export class CloudflareRelayPacketChannel implements PacketChannel {
|
|
9
|
+
readonly mode = 'relay' as const;
|
|
10
|
+
private readonly envelopeHandlers = new Set<EnvelopeHandler>();
|
|
11
|
+
private readonly closeHandlers = new Set<CloseHandler>();
|
|
12
|
+
private opened = false;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
readonly peerId: string,
|
|
16
|
+
private readonly roomId: string,
|
|
17
|
+
private readonly targetPeerId: string,
|
|
18
|
+
private readonly coordinator: SignalingRelayCoordinator,
|
|
19
|
+
) {
|
|
20
|
+
coordinator.onEnvelope(peerId, (signal) => {
|
|
21
|
+
if (signal.kind !== 'relay-data') return;
|
|
22
|
+
for (const handler of this.envelopeHandlers) handler(signal.envelope);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
send(envelope: Envelope): void {
|
|
27
|
+
if (!this.opened) {
|
|
28
|
+
const opened = this.coordinator.forward({
|
|
29
|
+
kind: 'relay-open',
|
|
30
|
+
roomId: this.roomId,
|
|
31
|
+
target: this.targetPeerId,
|
|
32
|
+
from: this.peerId,
|
|
33
|
+
});
|
|
34
|
+
if (opened?.kind === 'error') throw new Error(opened.message);
|
|
35
|
+
this.opened = true;
|
|
36
|
+
}
|
|
37
|
+
const result = this.coordinator.forward({
|
|
38
|
+
kind: 'relay-data',
|
|
39
|
+
roomId: this.roomId,
|
|
40
|
+
target: this.targetPeerId,
|
|
41
|
+
from: this.peerId,
|
|
42
|
+
envelope,
|
|
43
|
+
});
|
|
44
|
+
if (result?.kind === 'error') throw new Error(result.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onEnvelope(handler: EnvelopeHandler): () => void {
|
|
48
|
+
this.envelopeHandlers.add(handler);
|
|
49
|
+
return () => this.envelopeHandlers.delete(handler);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onClose(handler: CloseHandler): () => void {
|
|
53
|
+
this.closeHandlers.add(handler);
|
|
54
|
+
return () => this.closeHandlers.delete(handler);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close(reason?: string): void {
|
|
58
|
+
for (const handler of this.closeHandlers) handler(reason);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
type AddHandler = (item: Record<string, unknown>, key: string) => void;
|
|
2
|
+
type RemoveHandler = (item: Record<string, unknown>, key: string) => void;
|
|
3
|
+
type ChangeHandler = () => void;
|
|
4
|
+
type StateHandler = (state: Record<string, unknown>) => void;
|
|
5
|
+
|
|
6
|
+
export class ClientReplication {
|
|
7
|
+
state: Record<string, unknown> = {};
|
|
8
|
+
private readonly collectionAdd = new Map<string, Set<AddHandler>>();
|
|
9
|
+
private readonly collectionRemove = new Map<string, Set<RemoveHandler>>();
|
|
10
|
+
private readonly itemChange = new WeakMap<object, Set<ChangeHandler>>();
|
|
11
|
+
private readonly stateChange = new Set<StateHandler>();
|
|
12
|
+
|
|
13
|
+
applySnapshot(next: unknown): void {
|
|
14
|
+
const incoming = isRecord(next) ? next : {};
|
|
15
|
+
this.mergeRoot(incoming);
|
|
16
|
+
for (const handler of [...this.stateChange]) handler(this.state);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onAdd(collection: string, handler: AddHandler): () => void {
|
|
20
|
+
const handlers = getSet(this.collectionAdd, collection);
|
|
21
|
+
handlers.add(handler);
|
|
22
|
+
const current = this.state[collection];
|
|
23
|
+
if (isRecord(current)) {
|
|
24
|
+
for (const [key, value] of Object.entries(current)) {
|
|
25
|
+
if (isRecord(value)) handler(value, key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return () => handlers.delete(handler);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onRemove(collection: string, handler: RemoveHandler): () => void {
|
|
32
|
+
const handlers = getSet(this.collectionRemove, collection);
|
|
33
|
+
handlers.add(handler);
|
|
34
|
+
return () => handlers.delete(handler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onChange(item: Record<string, unknown>, handler: ChangeHandler): () => void {
|
|
38
|
+
const handlers = this.itemChange.get(item) ?? new Set<ChangeHandler>();
|
|
39
|
+
handlers.add(handler);
|
|
40
|
+
this.itemChange.set(item, handlers);
|
|
41
|
+
return () => handlers.delete(handler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mirror `properties` of a replicated item onto `target` — immediately (when
|
|
46
|
+
* `immediate`, the default, matching the real SDK) and again on every change.
|
|
47
|
+
* Compat with @colyseus/sdk's `StateCallbacks.bindTo(from, to, props?, immediate?)`;
|
|
48
|
+
* returns the unbind function.
|
|
49
|
+
*/
|
|
50
|
+
bindTo<TTarget extends Record<string, unknown>>(
|
|
51
|
+
from: Record<string, unknown>,
|
|
52
|
+
to: TTarget,
|
|
53
|
+
properties?: readonly string[],
|
|
54
|
+
immediate = true,
|
|
55
|
+
): () => void {
|
|
56
|
+
const copy = () => {
|
|
57
|
+
const keys = properties ?? Object.keys(from).filter((key) => typeof from[key] !== 'function');
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
(to as Record<string, unknown>)[key] = from[key];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
if (immediate) copy();
|
|
63
|
+
return this.onChange(from, copy);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onStateChange(handler: StateHandler): () => void {
|
|
67
|
+
this.stateChange.add(handler);
|
|
68
|
+
return () => this.stateChange.delete(handler);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private mergeRoot(incoming: Record<string, unknown>): void {
|
|
72
|
+
for (const key of Object.keys(this.state)) {
|
|
73
|
+
if (!(key in incoming)) delete this.state[key];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
77
|
+
const current = this.state[key];
|
|
78
|
+
if (isRecord(value) && isCollectionRecord(value)) {
|
|
79
|
+
this.state[key] = this.mergeCollection(key, isRecord(current) ? current : {}, value);
|
|
80
|
+
} else if (isRecord(value)) {
|
|
81
|
+
this.state[key] = mergeRecord(isRecord(current) ? current : {}, value, this.itemChange);
|
|
82
|
+
} else {
|
|
83
|
+
this.state[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private mergeCollection(
|
|
89
|
+
collection: string,
|
|
90
|
+
current: Record<string, unknown>,
|
|
91
|
+
incoming: Record<string, unknown>,
|
|
92
|
+
): Record<string, unknown> {
|
|
93
|
+
const removeHandlers = this.collectionRemove.get(collection);
|
|
94
|
+
for (const key of Object.keys(current)) {
|
|
95
|
+
if (!(key in incoming)) {
|
|
96
|
+
const old = current[key];
|
|
97
|
+
if (isRecord(old)) {
|
|
98
|
+
for (const handler of removeHandlers ?? []) handler(old, key);
|
|
99
|
+
}
|
|
100
|
+
delete current[key];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const addHandlers = this.collectionAdd.get(collection);
|
|
105
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
106
|
+
if (!isRecord(value)) {
|
|
107
|
+
current[key] = value;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const existed = key in current;
|
|
112
|
+
const target = isRecord(current[key]) ? current[key] : {};
|
|
113
|
+
current[key] = mergeRecord(target, value, this.itemChange);
|
|
114
|
+
if (!existed && isRecord(current[key])) {
|
|
115
|
+
for (const handler of addHandlers ?? []) handler(current[key], key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return current;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeRecord(
|
|
124
|
+
target: Record<string, unknown>,
|
|
125
|
+
incoming: Record<string, unknown>,
|
|
126
|
+
itemChange: WeakMap<object, Set<ChangeHandler>>,
|
|
127
|
+
): Record<string, unknown> {
|
|
128
|
+
let changed = false;
|
|
129
|
+
for (const key of Object.keys(target)) {
|
|
130
|
+
if (!(key in incoming)) {
|
|
131
|
+
delete target[key];
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
136
|
+
if (isRecord(value)) {
|
|
137
|
+
const child = isRecord(target[key]) ? target[key] : {};
|
|
138
|
+
target[key] = mergeRecord(child, value, itemChange);
|
|
139
|
+
} else if (target[key] !== value) {
|
|
140
|
+
target[key] = value;
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (changed) {
|
|
145
|
+
for (const handler of itemChange.get(target) ?? []) handler();
|
|
146
|
+
}
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getSet<K, V>(map: Map<K, Set<V>>, key: K): Set<V> {
|
|
151
|
+
const existing = map.get(key);
|
|
152
|
+
if (existing) return existing;
|
|
153
|
+
const created = new Set<V>();
|
|
154
|
+
map.set(key, created);
|
|
155
|
+
return created;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
159
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isCollectionRecord(value: Record<string, unknown>): boolean {
|
|
163
|
+
return Object.values(value).every((item) => isRecord(item));
|
|
164
|
+
}
|
package/src/room.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { encodeSnapshotState } from './schema';
|
|
2
|
+
|
|
3
|
+
export interface CompatClient {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
state: number;
|
|
7
|
+
userData?: unknown;
|
|
8
|
+
auth?: unknown;
|
|
9
|
+
reconnectionToken: string;
|
|
10
|
+
ref: {
|
|
11
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
12
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
13
|
+
emit(event: string, ...args: unknown[]): void;
|
|
14
|
+
};
|
|
15
|
+
raw(data: Uint8Array | Buffer, options?: unknown, cb?: (err?: Error) => void): void;
|
|
16
|
+
enqueueRaw(data: Uint8Array | Buffer, options?: unknown): void;
|
|
17
|
+
send(type: string | number, payload?: unknown): void;
|
|
18
|
+
sendBytes(type: string | number, bytes: Uint8Array | Buffer): void;
|
|
19
|
+
leave(code?: number, data?: string): void;
|
|
20
|
+
close(code?: number, data?: string): void;
|
|
21
|
+
error(code: number, message?: string): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Client = CompatClient;
|
|
25
|
+
|
|
26
|
+
export type MessageHandler = (client: CompatClient, payload: unknown) => void;
|
|
27
|
+
export type WildcardMessageHandler = (
|
|
28
|
+
client: CompatClient,
|
|
29
|
+
type: string | number,
|
|
30
|
+
payload: unknown,
|
|
31
|
+
) => void;
|
|
32
|
+
|
|
33
|
+
export class ClientArray<C extends CompatClient = CompatClient> extends Array<C> {
|
|
34
|
+
getById(sessionId: string): C | undefined {
|
|
35
|
+
return this.find((client) => client.sessionId === sessionId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
delete(client: C): boolean {
|
|
39
|
+
const index = this.indexOf(client);
|
|
40
|
+
if (index < 0) return false;
|
|
41
|
+
this.splice(index, 1);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BroadcastOptions {
|
|
47
|
+
except?: CompatClient | CompatClient[];
|
|
48
|
+
afterNextPatch?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RoomRuntimeBridge {
|
|
52
|
+
broadcastMessage(type: string | number, payload?: unknown, options?: BroadcastOptions): void;
|
|
53
|
+
broadcastState(): boolean;
|
|
54
|
+
disconnect(code?: number): Promise<void>;
|
|
55
|
+
send(client: CompatClient, type: string | number, payload?: unknown): void;
|
|
56
|
+
removeClient(client: CompatClient, code?: number, reason?: string): void;
|
|
57
|
+
allowReconnection(
|
|
58
|
+
client: CompatClient,
|
|
59
|
+
seconds: number | 'manual',
|
|
60
|
+
): Promise<CompatClient> & {
|
|
61
|
+
reject(reason?: unknown): void;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export abstract class Room<State = unknown> {
|
|
66
|
+
maxClients = Infinity;
|
|
67
|
+
autoDispose = true;
|
|
68
|
+
patchRate: number | null = 50;
|
|
69
|
+
maxMessagesPerSecond = Infinity;
|
|
70
|
+
seatReservationTimeout = 15;
|
|
71
|
+
state!: State;
|
|
72
|
+
readonly clients = new ClientArray();
|
|
73
|
+
private explicitlyLocked = false;
|
|
74
|
+
private matchmakingPrivate = false;
|
|
75
|
+
private roomMetadata: unknown = {};
|
|
76
|
+
private readonly messageHandlers = new Map<string, Set<MessageHandler>>();
|
|
77
|
+
private readonly wildcardMessageHandlers = new Set<WildcardMessageHandler>();
|
|
78
|
+
private readonly simulationTimers = new Set<ReturnType<typeof setInterval>>();
|
|
79
|
+
private readonly timeoutTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
80
|
+
private runtime: RoomRuntimeBridge | null = null;
|
|
81
|
+
|
|
82
|
+
get locked(): boolean {
|
|
83
|
+
return this.explicitlyLocked || this.hasReachedMaxClients();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get metadata(): unknown {
|
|
87
|
+
return this.roomMetadata;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set metadata(meta: unknown) {
|
|
91
|
+
this.roomMetadata = meta;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get isPrivate(): boolean {
|
|
95
|
+
return this.matchmakingPrivate;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setState(state: State): void {
|
|
99
|
+
this.state = state;
|
|
100
|
+
this.runtime?.broadcastState();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onMessage(type: '*', handler: WildcardMessageHandler): () => void;
|
|
104
|
+
onMessage(type: string | number, handler: MessageHandler): () => void;
|
|
105
|
+
onMessage(type: string | number, _validationSchema: unknown, handler: MessageHandler): () => void;
|
|
106
|
+
onMessage(
|
|
107
|
+
type: string | number,
|
|
108
|
+
handlerOrSchema: MessageHandler | WildcardMessageHandler | unknown,
|
|
109
|
+
maybeHandler?: MessageHandler,
|
|
110
|
+
): () => void {
|
|
111
|
+
const handler = (maybeHandler ?? handlerOrSchema) as MessageHandler | WildcardMessageHandler;
|
|
112
|
+
if (type === '*') {
|
|
113
|
+
this.wildcardMessageHandlers.add(handler as WildcardMessageHandler);
|
|
114
|
+
return () => this.wildcardMessageHandlers.delete(handler as WildcardMessageHandler);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const key = String(type);
|
|
118
|
+
let handlers = this.messageHandlers.get(key);
|
|
119
|
+
if (!handlers) {
|
|
120
|
+
handlers = new Set();
|
|
121
|
+
this.messageHandlers.set(key, handlers);
|
|
122
|
+
}
|
|
123
|
+
handlers.add(handler as MessageHandler);
|
|
124
|
+
return () => handlers?.delete(handler as MessageHandler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onMessageBytes(type: string | number, handler: MessageHandler): () => void;
|
|
128
|
+
onMessageBytes(
|
|
129
|
+
type: string | number,
|
|
130
|
+
_validationSchema: unknown,
|
|
131
|
+
handler: MessageHandler,
|
|
132
|
+
): () => void;
|
|
133
|
+
onMessageBytes(
|
|
134
|
+
type: string | number,
|
|
135
|
+
handlerOrSchema: MessageHandler | unknown,
|
|
136
|
+
maybeHandler?: MessageHandler,
|
|
137
|
+
): () => void {
|
|
138
|
+
return this.onMessage(type, (maybeHandler ?? handlerOrSchema) as MessageHandler);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
send(client: CompatClient, type: string | number, payload?: unknown): void {
|
|
142
|
+
this.runtime?.send(client, type, payload);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
broadcast(type: string | number, payload?: unknown, options?: BroadcastOptions): void {
|
|
146
|
+
this.runtime?.broadcastMessage(type, payload, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
broadcastBytes(type: string | number, payload: Uint8Array, options?: BroadcastOptions): void {
|
|
150
|
+
this.broadcast(type, payload, options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
broadcastPatch(): boolean {
|
|
154
|
+
return this.runtime?.broadcastState() ?? false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setSimulationInterval(callback: (dt: number) => void, ms = 16): void {
|
|
158
|
+
let last = Date.now();
|
|
159
|
+
const timer = setInterval(() => {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
callback(now - last);
|
|
162
|
+
last = now;
|
|
163
|
+
this.runtime?.broadcastState();
|
|
164
|
+
}, ms);
|
|
165
|
+
this.simulationTimers.add(timer);
|
|
166
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setPatchRate(milliseconds: number | null): void {
|
|
170
|
+
this.patchRate = milliseconds;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async setMetadata(meta: unknown): Promise<void> {
|
|
174
|
+
this.metadata = meta;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async setPrivate(bool = true): Promise<void> {
|
|
178
|
+
this.matchmakingPrivate = bool;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async setMatchmaking(updates: {
|
|
182
|
+
metadata?: unknown;
|
|
183
|
+
private?: boolean;
|
|
184
|
+
locked?: boolean;
|
|
185
|
+
maxClients?: number;
|
|
186
|
+
[key: string]: unknown;
|
|
187
|
+
}): Promise<void> {
|
|
188
|
+
if ('metadata' in updates) this.metadata = updates.metadata;
|
|
189
|
+
if (typeof updates.private === 'boolean') this.matchmakingPrivate = updates.private;
|
|
190
|
+
if (typeof updates.locked === 'boolean') this.explicitlyLocked = updates.locked;
|
|
191
|
+
if (typeof updates.maxClients === 'number') this.maxClients = updates.maxClients;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async lock(): Promise<void> {
|
|
195
|
+
this.explicitlyLocked = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async unlock(): Promise<void> {
|
|
199
|
+
this.explicitlyLocked = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
hasReachedMaxClients(): boolean {
|
|
203
|
+
return this.clients.length >= this.maxClients;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setSeatReservationTime(seconds: number): this {
|
|
207
|
+
this.seatReservationTimeout = seconds;
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
hasReservedSeat(_sessionId: string): boolean {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
checkReconnectionToken(reconnectionToken: string): string {
|
|
216
|
+
return reconnectionToken.split(':')[0] ?? reconnectionToken;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async disconnect(closeCode = 4000): Promise<void> {
|
|
220
|
+
await this.runtime?.disconnect(closeCode);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
allowReconnection(
|
|
224
|
+
previousClient: CompatClient,
|
|
225
|
+
seconds: number | 'manual',
|
|
226
|
+
): Promise<CompatClient> & { reject(reason?: unknown): void } {
|
|
227
|
+
if (!this.runtime) throw new Error('Room runtime is not attached');
|
|
228
|
+
return this.runtime.allowReconnection(previousClient, seconds);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
onBeforeShutdown(): void {}
|
|
232
|
+
|
|
233
|
+
onCreate?(_options?: unknown): void;
|
|
234
|
+
onAuth?(
|
|
235
|
+
_client: CompatClient,
|
|
236
|
+
_options?: unknown,
|
|
237
|
+
): boolean | unknown | Promise<boolean | unknown>;
|
|
238
|
+
onJoin?(_client: CompatClient, _options?: unknown): void;
|
|
239
|
+
onDrop?(_client: CompatClient, _code?: number): void;
|
|
240
|
+
onReconnect?(_client: CompatClient): void;
|
|
241
|
+
onLeave?(_client: CompatClient, _code?: number): void;
|
|
242
|
+
onDispose?(): void;
|
|
243
|
+
|
|
244
|
+
_attachRuntime(runtime: RoomRuntimeBridge): void {
|
|
245
|
+
this.runtime = runtime;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_dispatchMessage(client: CompatClient, type: string | number, payload: unknown): void {
|
|
249
|
+
for (const handler of this.messageHandlers.get(String(type)) ?? []) handler(client, payload);
|
|
250
|
+
for (const handler of this.wildcardMessageHandlers) handler(client, type, payload);
|
|
251
|
+
this.runtime?.broadcastState();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_snapshot(): unknown {
|
|
255
|
+
return encodeSnapshotState(this.state);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_disposeRuntime(): void {
|
|
259
|
+
for (const timer of this.simulationTimers) clearInterval(timer);
|
|
260
|
+
for (const timer of this.timeoutTimers) clearTimeout(timer);
|
|
261
|
+
this.simulationTimers.clear();
|
|
262
|
+
this.timeoutTimers.clear();
|
|
263
|
+
this.runtime = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function validate(_format: unknown, handler: MessageHandler): MessageHandler {
|
|
268
|
+
return handler;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function room<T extends Room>(options: Partial<T> & ThisType<T>): new () => T {
|
|
272
|
+
return class extends Room {
|
|
273
|
+
constructor() {
|
|
274
|
+
super();
|
|
275
|
+
Object.assign(this, options);
|
|
276
|
+
if ('state' in options && options.state !== undefined) {
|
|
277
|
+
this.state =
|
|
278
|
+
typeof options.state === 'function' ? (options.state as () => unknown)() : options.state;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} as unknown as new () => T;
|
|
282
|
+
}
|