@pluv/io 5.0.1 → 5.0.2
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/index.d.mts +542 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1424 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +8 -5
- package/CHANGELOG.md +0 -2353
- package/eslint.config.mjs +0 -4
- package/src/AbstractPersistence.ts +0 -29
- package/src/AbstractPlatform.ts +0 -105
- package/src/AbstractPubSub.ts +0 -20
- package/src/AbstractWebSocket.ts +0 -134
- package/src/IORoom.ts +0 -1245
- package/src/Persistence.ts +0 -81
- package/src/PluvIO.ts +0 -239
- package/src/PluvProcedure.ts +0 -203
- package/src/PluvRouter.ts +0 -61
- package/src/PluvServer.ts +0 -487
- package/src/PubSub.ts +0 -70
- package/src/authorize.ts +0 -76
- package/src/constants.ts +0 -6
- package/src/createIO.ts +0 -55
- package/src/index.ts +0 -55
- package/src/types.ts +0 -287
- package/src/utils/index.ts +0 -2
- package/src/utils/oneLine.ts +0 -7
- package/src/utils/pickBy.ts +0 -16
- package/src/version.ts +0 -3
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -14
package/src/PluvServer.ts
DELETED
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
import type { AbstractCrdtDocFactory, CrdtLibraryType, NoopCrdtDocFactory } from "@pluv/crdt";
|
|
2
|
-
import { noop } from "@pluv/crdt";
|
|
3
|
-
import type {
|
|
4
|
-
AreAllOptional,
|
|
5
|
-
IOLike,
|
|
6
|
-
Id,
|
|
7
|
-
InferIOAuthorize,
|
|
8
|
-
InferIOAuthorizeUser,
|
|
9
|
-
JsonObject,
|
|
10
|
-
Maybe,
|
|
11
|
-
NonNilProps,
|
|
12
|
-
} from "@pluv/types";
|
|
13
|
-
import colors from "kleur";
|
|
14
|
-
import type {
|
|
15
|
-
AbstractPlatform,
|
|
16
|
-
InferInitContextType,
|
|
17
|
-
InferRoomContextType,
|
|
18
|
-
} from "./AbstractPlatform";
|
|
19
|
-
import { IORoom } from "./IORoom";
|
|
20
|
-
import type { PluvIO } from "./PluvIO";
|
|
21
|
-
import { PluvProcedure } from "./PluvProcedure";
|
|
22
|
-
import type { PluvRouterEventConfig } from "./PluvRouter";
|
|
23
|
-
import { PluvRouter } from "./PluvRouter";
|
|
24
|
-
import type { JWTEncodeParams } from "./authorize";
|
|
25
|
-
import { PING_TIMEOUT_MS } from "./constants";
|
|
26
|
-
import type {
|
|
27
|
-
BasePluvIOListeners,
|
|
28
|
-
GetInitialStorageFn,
|
|
29
|
-
PluvContext,
|
|
30
|
-
PluvIOAuthorize,
|
|
31
|
-
PluvIOLimits,
|
|
32
|
-
PluvIOListeners,
|
|
33
|
-
} from "./types";
|
|
34
|
-
import { oneLine, pickBy } from "./utils";
|
|
35
|
-
import { __PLUV_VERSION } from "./version";
|
|
36
|
-
|
|
37
|
-
export type InferIORoom<TServer extends PluvServer<any, any, any, any, any>> =
|
|
38
|
-
TServer extends PluvServer<
|
|
39
|
-
infer IPlatform,
|
|
40
|
-
infer IAuthorize,
|
|
41
|
-
infer IContext,
|
|
42
|
-
infer ICrdt,
|
|
43
|
-
infer IEvents
|
|
44
|
-
>
|
|
45
|
-
? IORoom<IPlatform, IAuthorize, IContext, ICrdt, IEvents>
|
|
46
|
-
: never;
|
|
47
|
-
|
|
48
|
-
export type PluvServerConfig<
|
|
49
|
-
TPlatform extends AbstractPlatform<any, any> = AbstractPlatform<any, any>,
|
|
50
|
-
TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null =
|
|
51
|
-
any,
|
|
52
|
-
TContext extends Record<string, any> = {},
|
|
53
|
-
TCrdt extends CrdtLibraryType<any> = CrdtLibraryType<any>,
|
|
54
|
-
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
|
|
55
|
-
> = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & {
|
|
56
|
-
authorize?: TAuthorize;
|
|
57
|
-
context?: PluvContext<TPlatform, TContext>;
|
|
58
|
-
crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any, any> };
|
|
59
|
-
debug?: boolean;
|
|
60
|
-
limits: PluvIOLimits;
|
|
61
|
-
io: PluvIO<TPlatform, TAuthorize, TContext>;
|
|
62
|
-
platform: () => TPlatform;
|
|
63
|
-
router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
64
|
-
} & (TCrdt extends CrdtLibraryType<NoopCrdtDocFactory>
|
|
65
|
-
? { getInitialStorage?: "[ERROR]: Must specify crdt to use getInitialStorage" }
|
|
66
|
-
: { getInitialStorage: GetInitialStorageFn<TContext> });
|
|
67
|
-
|
|
68
|
-
type BaseCreateRoomOptions<
|
|
69
|
-
TPlatform extends AbstractPlatform<any, any>,
|
|
70
|
-
TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null,
|
|
71
|
-
TContext extends Record<string, any>,
|
|
72
|
-
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
|
|
73
|
-
> = {
|
|
74
|
-
debug?: boolean;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type CreateRoomOptions<
|
|
78
|
-
TPlatform extends AbstractPlatform<any, any>,
|
|
79
|
-
TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null,
|
|
80
|
-
TContext extends Record<string, any>,
|
|
81
|
-
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
|
|
82
|
-
> = keyof Omit<InferRoomContextType<TPlatform>, "meta"> extends never
|
|
83
|
-
? [BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>] | []
|
|
84
|
-
: [
|
|
85
|
-
Id<
|
|
86
|
-
BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents> &
|
|
87
|
-
InferRoomContextType<TPlatform>
|
|
88
|
-
>,
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
export class PluvServer<
|
|
92
|
-
TPlatform extends AbstractPlatform<any, any> = AbstractPlatform<any, any>,
|
|
93
|
-
TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null =
|
|
94
|
-
any,
|
|
95
|
-
TContext extends Record<string, any> = {},
|
|
96
|
-
TCrdt extends CrdtLibraryType<any> = CrdtLibraryType<any>,
|
|
97
|
-
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
|
|
98
|
-
> implements IOLike<TAuthorize, TCrdt, TEvents> {
|
|
99
|
-
public readonly version: string = __PLUV_VERSION as any;
|
|
100
|
-
|
|
101
|
-
private readonly _config: NonNilProps<
|
|
102
|
-
PluvServerConfig<TPlatform, TAuthorize, TContext, TCrdt, TEvents>
|
|
103
|
-
>;
|
|
104
|
-
private readonly _docFactory: AbstractCrdtDocFactory<any, any>;
|
|
105
|
-
|
|
106
|
-
public get fetch(): (...args: any[]) => Promise<any> {
|
|
107
|
-
return (...args: any[]): Promise<any> => {
|
|
108
|
-
const platform = this._config.platform();
|
|
109
|
-
platform.validateConfig(this._config);
|
|
110
|
-
|
|
111
|
-
if (!platform._fetch) {
|
|
112
|
-
throw new Error(`\`${platform._name}\` does not support \`fetch\``);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return platform._fetch(...args);
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* @ignore
|
|
121
|
-
* @readonly
|
|
122
|
-
* @deprecated Internal use only. Changes to this will never be marked as breaking.
|
|
123
|
-
*/
|
|
124
|
-
public get _defs() {
|
|
125
|
-
return {
|
|
126
|
-
authorize: this._config.authorize,
|
|
127
|
-
context: this._config.context,
|
|
128
|
-
crdt: this._config.crdt,
|
|
129
|
-
events: this._router._defs.events,
|
|
130
|
-
platform: this._config.platform(),
|
|
131
|
-
} as {
|
|
132
|
-
authorize: TAuthorize;
|
|
133
|
-
context: PluvContext<TPlatform, TContext>;
|
|
134
|
-
crdt: TCrdt;
|
|
135
|
-
events: TEvents;
|
|
136
|
-
platform: TPlatform;
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private get _baseRouter(): PluvRouter<TPlatform, TAuthorize, TContext, {}> {
|
|
141
|
-
const listeners = this._getListeners();
|
|
142
|
-
|
|
143
|
-
return new PluvRouter({
|
|
144
|
-
$getOthers: this._procedure.sync((data, { room, session, sessions }) => {
|
|
145
|
-
const currentTime = Date.now();
|
|
146
|
-
|
|
147
|
-
const others = sessions
|
|
148
|
-
.filter((wsSession) => {
|
|
149
|
-
if (wsSession.id === session?.id) return false;
|
|
150
|
-
if (wsSession.quit) return false;
|
|
151
|
-
if (currentTime - wsSession.timers.ping > PING_TIMEOUT_MS) return false;
|
|
152
|
-
|
|
153
|
-
return true;
|
|
154
|
-
})
|
|
155
|
-
.reduce<{
|
|
156
|
-
[connectionId: string]: {
|
|
157
|
-
connectionId: string;
|
|
158
|
-
room: string | null;
|
|
159
|
-
user: JsonObject | null;
|
|
160
|
-
};
|
|
161
|
-
}>(
|
|
162
|
-
(acc, { id, presence, timers, user }) => ({
|
|
163
|
-
...acc,
|
|
164
|
-
[id]: {
|
|
165
|
-
connectionId: id,
|
|
166
|
-
presence,
|
|
167
|
-
room,
|
|
168
|
-
timers: { presence: timers.presence },
|
|
169
|
-
user,
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
172
|
-
{},
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
return { $othersReceived: { others } };
|
|
176
|
-
}),
|
|
177
|
-
$initializeSession: this._procedure
|
|
178
|
-
.broadcast((data, { session }) => {
|
|
179
|
-
const presence = (data as any)?.presence;
|
|
180
|
-
const timers = session.timers;
|
|
181
|
-
|
|
182
|
-
if (!session) return {};
|
|
183
|
-
|
|
184
|
-
session.presence = presence;
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
$userJoined: {
|
|
188
|
-
connectionId: session.id,
|
|
189
|
-
user: session.user,
|
|
190
|
-
presence,
|
|
191
|
-
timers: { presence: timers.presence },
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
})
|
|
195
|
-
.self(async (data, { context, doc, platform, room, session }) => {
|
|
196
|
-
const oldState = await platform.persistence.getStorageState(room);
|
|
197
|
-
/**
|
|
198
|
-
* !HACK
|
|
199
|
-
* @description This is the frontend's initialStorage. We only want to
|
|
200
|
-
* apply this if the server's storage is empty (i.e. no initial storage
|
|
201
|
-
* has been applied)
|
|
202
|
-
*/
|
|
203
|
-
const update = (data as any)?.update as Maybe<string>;
|
|
204
|
-
|
|
205
|
-
if (!oldState) {
|
|
206
|
-
const loadedState = await this._getInitialStorage({ context, room });
|
|
207
|
-
|
|
208
|
-
if (!!loadedState && !this._docFactory.isEmpty(loadedState)) {
|
|
209
|
-
doc.applyEncodedState({ update: loadedState });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @description Storage was already initialized. Don't overwrite the current
|
|
215
|
-
* storage state with the incoming initial storage. Return what the current state
|
|
216
|
-
* is without changes.
|
|
217
|
-
* @date May 7, 2025
|
|
218
|
-
*/
|
|
219
|
-
if (!doc.isEmpty()) {
|
|
220
|
-
const encodedState = doc.getEncodedState();
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
$storageReceived: { changeKind: "unchanged", state: encodedState },
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @description Storage was never initialized, and there is an incoming
|
|
229
|
-
* initialStorage from the client. Apply the initialStorage and persist the state
|
|
230
|
-
* as an update.
|
|
231
|
-
* @date May 7, 2025
|
|
232
|
-
*/
|
|
233
|
-
if (!!update) {
|
|
234
|
-
const encodedState = doc.applyEncodedState({ update }).getEncodedState();
|
|
235
|
-
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
!!this._config.limits.storageMaxSize &&
|
|
239
|
-
storageSize > this._config.limits.storageMaxSize
|
|
240
|
-
) {
|
|
241
|
-
throw new Error("Storage has exceeded the size limit");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
await platform.persistence
|
|
245
|
-
.setStorageState(room, encodedState)
|
|
246
|
-
.catch((error) => {
|
|
247
|
-
this._logDebug(error);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
listeners.onStorageUpdated({
|
|
251
|
-
context,
|
|
252
|
-
encodedState,
|
|
253
|
-
platform,
|
|
254
|
-
room,
|
|
255
|
-
user: session?.user,
|
|
256
|
-
webSocket: session?.webSocket.webSocket,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
$storageReceived: { changeKind: "initialized", state: encodedState },
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const encodedState = doc.getEncodedState();
|
|
265
|
-
|
|
266
|
-
return { $storageReceived: { changeKind: "empty", state: encodedState } };
|
|
267
|
-
}),
|
|
268
|
-
$ping: this._procedure.self((data, { platform, session }) => {
|
|
269
|
-
if (!session) return {};
|
|
270
|
-
|
|
271
|
-
const currentTime = new Date().getTime();
|
|
272
|
-
const prevState = session.webSocket.state;
|
|
273
|
-
|
|
274
|
-
platform.setSerializedState(session.webSocket, {
|
|
275
|
-
...prevState,
|
|
276
|
-
timers: {
|
|
277
|
-
...prevState.timers,
|
|
278
|
-
ping: currentTime,
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return { $pong: {} };
|
|
283
|
-
}),
|
|
284
|
-
$updatePresence: this._procedure.broadcast((data, context) => {
|
|
285
|
-
const presence = (data as any)?.presence;
|
|
286
|
-
const { session } = context;
|
|
287
|
-
|
|
288
|
-
if (!session) return {};
|
|
289
|
-
|
|
290
|
-
const cleanedPatch = pickBy(
|
|
291
|
-
presence ?? {},
|
|
292
|
-
(value) => typeof value !== "undefined",
|
|
293
|
-
);
|
|
294
|
-
const updated = Object.assign(Object.create(null), session.presence, cleanedPatch);
|
|
295
|
-
const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
!!this._config.limits.presenceMaxSize &&
|
|
299
|
-
bytes > this._config.limits.presenceMaxSize
|
|
300
|
-
) {
|
|
301
|
-
throw new Error(oneLine`
|
|
302
|
-
Large presence. Presence must be at most
|
|
303
|
-
${this._config.limits.presenceMaxSize.toLocaleString()} bytes.
|
|
304
|
-
Current size: ${bytes.toLocaleString()} bytes
|
|
305
|
-
`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
context.presence = updated;
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
$presenceUpdated: {
|
|
312
|
-
presence: updated,
|
|
313
|
-
timers: { presence: session.timers.presence },
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
}),
|
|
317
|
-
$updateStorage: this._procedure.broadcast((data, { context, doc, platform, room }) => {
|
|
318
|
-
const origin = (data as any)?.origin as Maybe<string>;
|
|
319
|
-
const update: string | null = (data as any)?.update ?? null;
|
|
320
|
-
|
|
321
|
-
if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
|
|
322
|
-
|
|
323
|
-
const updated = update === null ? doc : doc.applyEncodedState({ update });
|
|
324
|
-
const encodedState = updated.getEncodedState();
|
|
325
|
-
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
!!this._config.limits.storageMaxSize &&
|
|
329
|
-
storageSize > this._config.limits.storageMaxSize
|
|
330
|
-
) {
|
|
331
|
-
throw new Error(oneLine`
|
|
332
|
-
Large Storage. Storage must be at most
|
|
333
|
-
${this._config.limits.storageMaxSize.toLocaleString()} bytes.
|
|
334
|
-
Current size: ${storageSize.toLocaleString()} bytes
|
|
335
|
-
`);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
platform.persistence.setStorageState(room, encodedState).then(() => {
|
|
339
|
-
listeners.onStorageUpdated({
|
|
340
|
-
context,
|
|
341
|
-
encodedState,
|
|
342
|
-
platform,
|
|
343
|
-
room,
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
return { $storageUpdated: { state: encodedState } };
|
|
348
|
-
}),
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private get _procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}> {
|
|
353
|
-
return new PluvProcedure();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private get _router(): PluvRouter<TPlatform, TAuthorize, TContext, TEvents> {
|
|
357
|
-
return (
|
|
358
|
-
this._config.router
|
|
359
|
-
? PluvRouter.merge(this._baseRouter, this._config.router)
|
|
360
|
-
: this._baseRouter
|
|
361
|
-
) as PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
constructor(options: PluvServerConfig<TPlatform, TAuthorize, TContext, TCrdt, TEvents>) {
|
|
365
|
-
this._config = {
|
|
366
|
-
crdt: noop,
|
|
367
|
-
debug: false,
|
|
368
|
-
router: new PluvRouter<TPlatform, TAuthorize, TContext, TEvents>({} as TEvents),
|
|
369
|
-
...options,
|
|
370
|
-
} as NonNilProps<PluvServerConfig<TPlatform, TAuthorize, TContext, TCrdt, TEvents>>;
|
|
371
|
-
|
|
372
|
-
const {
|
|
373
|
-
onRoomDestroyed,
|
|
374
|
-
onRoomMessage,
|
|
375
|
-
onStorageDestroyed,
|
|
376
|
-
onStorageUpdated,
|
|
377
|
-
onUserConnected,
|
|
378
|
-
onUserDisconnected,
|
|
379
|
-
} = options as Partial<BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>>;
|
|
380
|
-
|
|
381
|
-
this._docFactory = this._config.crdt.doc(() => ({}));
|
|
382
|
-
(this as any)._listeners = {
|
|
383
|
-
onRoomDestroyed: (event) => onRoomDestroyed?.(event),
|
|
384
|
-
onRoomMessage: (event) => onRoomMessage?.(event),
|
|
385
|
-
onStorageDestroyed: (event) => onStorageDestroyed?.(event),
|
|
386
|
-
onStorageUpdated: (event) => onStorageUpdated?.(event),
|
|
387
|
-
onUserConnected: (event) => onUserConnected?.(event),
|
|
388
|
-
onUserDisconnected: (event) => onUserDisconnected?.(event),
|
|
389
|
-
} as BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
public createRoom(
|
|
393
|
-
room: string,
|
|
394
|
-
...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>
|
|
395
|
-
): IORoom<TPlatform, TAuthorize, TContext, TCrdt, TEvents> {
|
|
396
|
-
const { _meta, debug, ...platformRoomContext } = (options[0] ?? {}) as CreateRoomOptions<
|
|
397
|
-
TPlatform,
|
|
398
|
-
TAuthorize,
|
|
399
|
-
TContext,
|
|
400
|
-
TEvents
|
|
401
|
-
>[0] & {
|
|
402
|
-
_meta?: any;
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
const platform = this._config.platform();
|
|
406
|
-
|
|
407
|
-
platform.validateConfig(this._config);
|
|
408
|
-
|
|
409
|
-
if (platform._config.handleMode !== "io") {
|
|
410
|
-
throw new Error(`\`createRoom\` is unsupported for \`${platform._name}\``);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
|
|
414
|
-
throw new Error("Unsupported room name");
|
|
415
|
-
|
|
416
|
-
const roomContext = platformRoomContext as InferRoomContextType<TPlatform>;
|
|
417
|
-
const listeners = this._getListeners();
|
|
418
|
-
const logDebug = this._logDebug.bind(this);
|
|
419
|
-
|
|
420
|
-
const newRoom = new IORoom<TPlatform, TAuthorize, TContext, TCrdt, TEvents>(room, {
|
|
421
|
-
...(!!_meta ? { _meta } : {}),
|
|
422
|
-
authorize: this._config.authorize ?? undefined,
|
|
423
|
-
context: this._config.context,
|
|
424
|
-
crdt: this._config.crdt,
|
|
425
|
-
debug: debug ?? this._config.debug,
|
|
426
|
-
getInitialStorage: this._getInitialStorage,
|
|
427
|
-
async onRoomDestroyed(event) {
|
|
428
|
-
logDebug(`${colors.blue("Deleting empty room:")} ${room}`);
|
|
429
|
-
|
|
430
|
-
// Always fire server-level onRoomDestroyed
|
|
431
|
-
await Promise.resolve(listeners.onRoomDestroyed(event));
|
|
432
|
-
|
|
433
|
-
logDebug(`${colors.blue("Deleted room:")} ${room}`);
|
|
434
|
-
},
|
|
435
|
-
async onStorageDestroyed(event) {
|
|
436
|
-
logDebug(`${colors.blue("Destroying storage for room:")} ${room}`);
|
|
437
|
-
|
|
438
|
-
// Always fire server-level onStorageDestroyed
|
|
439
|
-
await Promise.resolve(listeners.onStorageDestroyed(event));
|
|
440
|
-
|
|
441
|
-
await event.platform.persistence.deleteStorageState(room);
|
|
442
|
-
|
|
443
|
-
logDebug(`${colors.blue("Destroyed storage for room:")} ${room}`);
|
|
444
|
-
},
|
|
445
|
-
async onMessage(event) {
|
|
446
|
-
await Promise.resolve(listeners.onRoomMessage(event));
|
|
447
|
-
},
|
|
448
|
-
async onUserConnected(event) {
|
|
449
|
-
await Promise.resolve(listeners.onUserConnected(event));
|
|
450
|
-
},
|
|
451
|
-
async onUserDisconnected(event) {
|
|
452
|
-
await Promise.resolve(listeners.onUserDisconnected(event));
|
|
453
|
-
},
|
|
454
|
-
platform,
|
|
455
|
-
roomContext,
|
|
456
|
-
router: this._router,
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
this._logDebug(`${colors.blue("Created room:")} ${room}`);
|
|
460
|
-
|
|
461
|
-
return newRoom;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
public async createToken(
|
|
465
|
-
params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>,
|
|
466
|
-
): Promise<string> {
|
|
467
|
-
return await this._config.io.createToken(params);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private _getInitialStorage: GetInitialStorageFn<TContext> = (...args) => {
|
|
471
|
-
const getInitialStorage =
|
|
472
|
-
typeof this._config.getInitialStorage !== "string"
|
|
473
|
-
? (this._config.getInitialStorage ??
|
|
474
|
-
((() => null) as GetInitialStorageFn<TContext>))
|
|
475
|
-
: ((() => null) as GetInitialStorageFn<TContext>);
|
|
476
|
-
|
|
477
|
-
return getInitialStorage(...args);
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
private _getListeners(): BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents> {
|
|
481
|
-
return (this as any)._listeners;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private _logDebug(...data: any[]): void {
|
|
485
|
-
if (this._config.debug) console.log(...data);
|
|
486
|
-
}
|
|
487
|
-
}
|
package/src/PubSub.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { Subject } from "wonka";
|
|
2
|
-
import { makeSubject, subscribe } from "wonka";
|
|
3
|
-
import type { IOPubSubEventMessage } from "./AbstractPubSub";
|
|
4
|
-
import { AbstractPubSub } from "./AbstractPubSub";
|
|
5
|
-
|
|
6
|
-
export class PubSub extends AbstractPubSub {
|
|
7
|
-
private _currentSubscriptionId: number = 0;
|
|
8
|
-
// Map of roomName to subjects
|
|
9
|
-
private _subjects = new Map<string, Subject<IOPubSubEventMessage<any>>>();
|
|
10
|
-
// Map of roomName to subscription ids
|
|
11
|
-
private _subscriptionsRefs = new Map<string, readonly number[]>();
|
|
12
|
-
// Map of
|
|
13
|
-
private _unsubscriptions = new Map<number, [roomName: string, unsubscribe: () => void]>();
|
|
14
|
-
|
|
15
|
-
public async publish(roomName: string, payload: IOPubSubEventMessage<any>): Promise<void> {
|
|
16
|
-
const subject = this._subjects.get(roomName);
|
|
17
|
-
|
|
18
|
-
if (!subject) return;
|
|
19
|
-
|
|
20
|
-
Promise.resolve(subject.next(payload));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public subscribe(
|
|
24
|
-
roomName: string,
|
|
25
|
-
onMessage: (message: IOPubSubEventMessage<any>) => void,
|
|
26
|
-
): Promise<number> {
|
|
27
|
-
const id = this._currentSubscriptionId++;
|
|
28
|
-
const refs = this._subscriptionsRefs.get(roomName) ?? [];
|
|
29
|
-
|
|
30
|
-
const subject = makeSubject<IOPubSubEventMessage<any>>();
|
|
31
|
-
|
|
32
|
-
this._subjects.set(roomName, subject);
|
|
33
|
-
this._subscriptionsRefs.set(roomName, [...refs, id]);
|
|
34
|
-
|
|
35
|
-
const unsubscribe = subscribe(onMessage)(subject.source).unsubscribe;
|
|
36
|
-
|
|
37
|
-
this._unsubscriptions.set(id, [roomName, unsubscribe]);
|
|
38
|
-
|
|
39
|
-
return Promise.resolve(id);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public unsubscribe(subscriptionId: number): void {
|
|
43
|
-
const [roomName, unsubscribe] = this._unsubscriptions.get(subscriptionId) ?? [];
|
|
44
|
-
|
|
45
|
-
if (!roomName || !unsubscribe) return;
|
|
46
|
-
|
|
47
|
-
const refs = this._subscriptionsRefs.get(roomName);
|
|
48
|
-
|
|
49
|
-
if (!refs) return;
|
|
50
|
-
|
|
51
|
-
if (refs.length === 1) {
|
|
52
|
-
this._subjects.delete(roomName);
|
|
53
|
-
this._subscriptionsRefs.delete(roomName);
|
|
54
|
-
this._unsubscriptions.delete(subscriptionId);
|
|
55
|
-
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const index = refs.indexOf(subscriptionId);
|
|
60
|
-
|
|
61
|
-
this._subscriptionsRefs.set(
|
|
62
|
-
roomName,
|
|
63
|
-
index === -1 ? refs : [...refs.slice(0, index), ...refs.slice(index + 1)],
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
unsubscribe();
|
|
67
|
-
|
|
68
|
-
this._unsubscriptions.delete(subscriptionId);
|
|
69
|
-
}
|
|
70
|
-
}
|
package/src/authorize.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import hkdf from "@panva/hkdf";
|
|
2
|
-
import type { BaseUser } from "@pluv/types";
|
|
3
|
-
import { EncryptJWT, jwtDecrypt } from "jose";
|
|
4
|
-
import type { AbstractPlatform, InferInitContextType } from "./AbstractPlatform";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_MAX_AGE_MS = 60_000;
|
|
7
|
-
|
|
8
|
-
export const getEncryptionKey = async (secret: string): Promise<Uint8Array> => {
|
|
9
|
-
return await hkdf("sha256", secret, "", "Pluv.io Generated Encryption Key", 32);
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export interface JWT<TUser extends BaseUser> {
|
|
13
|
-
room: string;
|
|
14
|
-
sub: string;
|
|
15
|
-
user: TUser;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type JWTEncodeParams<
|
|
19
|
-
TUser extends BaseUser | null,
|
|
20
|
-
TPlatform extends AbstractPlatform<any, any>,
|
|
21
|
-
> = {
|
|
22
|
-
maxAge?: number;
|
|
23
|
-
room: string;
|
|
24
|
-
user: TUser extends BaseUser ? TUser : "Error: Cannot create token without authorization!";
|
|
25
|
-
} & InferInitContextType<TPlatform>;
|
|
26
|
-
|
|
27
|
-
export interface AuthorizeParams {
|
|
28
|
-
platform: AbstractPlatform<any, any>;
|
|
29
|
-
secret: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AuthorizeModule {
|
|
33
|
-
decode: <TUser extends BaseUser>(jwt: string) => Promise<TUser | null>;
|
|
34
|
-
encode: <TUser extends BaseUser, TPlatform extends AbstractPlatform<any, any>>(
|
|
35
|
-
params: JWTEncodeParams<TUser, TPlatform>,
|
|
36
|
-
) => Promise<string>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const now = () => (Date.now() / 1_000) | 0;
|
|
40
|
-
|
|
41
|
-
export const authorize = (params: AuthorizeParams) => {
|
|
42
|
-
const { platform, secret } = params;
|
|
43
|
-
|
|
44
|
-
const encode = async <TUser extends BaseUser, TPlatform extends AbstractPlatform<any, any>>(
|
|
45
|
-
encodeParams: JWTEncodeParams<TUser, TPlatform>,
|
|
46
|
-
): Promise<string> => {
|
|
47
|
-
const { maxAge = DEFAULT_MAX_AGE_MS, room, user } = encodeParams;
|
|
48
|
-
|
|
49
|
-
const encryptionSecret = await getEncryptionKey(secret);
|
|
50
|
-
|
|
51
|
-
const token = await new EncryptJWT({
|
|
52
|
-
room,
|
|
53
|
-
sub: room ? `${room}|${user.id}` : user.id,
|
|
54
|
-
user,
|
|
55
|
-
})
|
|
56
|
-
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
|
|
57
|
-
.setIssuedAt()
|
|
58
|
-
.setExpirationTime(now() + maxAge)
|
|
59
|
-
.setJti(platform.randomUUID())
|
|
60
|
-
.encrypt(encryptionSecret);
|
|
61
|
-
|
|
62
|
-
return token.toString();
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const decode = async <TUser extends BaseUser>(jwt: string): Promise<JWT<TUser> | null> => {
|
|
66
|
-
const encryptionSecret = await getEncryptionKey(secret);
|
|
67
|
-
|
|
68
|
-
const { payload } = await jwtDecrypt(jwt, encryptionSecret, {
|
|
69
|
-
clockTolerance: 15,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return (payload as any) ?? null;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return { decode, encode };
|
|
76
|
-
};
|
package/src/constants.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export const GARBAGE_COLLECT_INTERVAL_MS = 60_000;
|
|
2
|
-
export const MAX_PRESENCE_SIZE_BYTES = 512;
|
|
3
|
-
export const MAX_STORAGE_SIZE_BYTES = 31_457_280;
|
|
4
|
-
export const MAX_USER_ID_LENGTH = 128;
|
|
5
|
-
export const MAX_USER_SIZE_BYTES = 512;
|
|
6
|
-
export const PING_TIMEOUT_MS = 30_000;
|
package/src/createIO.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { CrdtLibraryType, NoopCrdtDocFactory } from "@pluv/crdt";
|
|
2
|
-
import type { BaseUser } from "@pluv/types";
|
|
3
|
-
import type { AbstractPlatform, InferInitContextType } from "./AbstractPlatform";
|
|
4
|
-
import type { PluvIOConfig } from "./PluvIO";
|
|
5
|
-
import { PluvIO } from "./PluvIO";
|
|
6
|
-
import type { PluvContext, PluvIOAuthorize, PluvIOLimits } from "./types";
|
|
7
|
-
|
|
8
|
-
export type CreateIOParams<
|
|
9
|
-
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
10
|
-
TContext extends Record<string, any> = {},
|
|
11
|
-
TUser extends BaseUser | null = null,
|
|
12
|
-
TCrdt extends CrdtLibraryType<any> = CrdtLibraryType<NoopCrdtDocFactory>,
|
|
13
|
-
> = {
|
|
14
|
-
authorize?: PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>;
|
|
15
|
-
context?: PluvContext<TPlatform, TContext>;
|
|
16
|
-
crdt?: TCrdt;
|
|
17
|
-
debug?: boolean;
|
|
18
|
-
limits?: PluvIOLimits;
|
|
19
|
-
platform: () => TPlatform;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const createIO = <
|
|
23
|
-
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
24
|
-
TContext extends Record<string, any> = {},
|
|
25
|
-
TUser extends BaseUser | null = null,
|
|
26
|
-
TCrdt extends CrdtLibraryType<any> = CrdtLibraryType<NoopCrdtDocFactory>,
|
|
27
|
-
>(
|
|
28
|
-
params: CreateIOParams<TPlatform, TContext, TUser, TCrdt>,
|
|
29
|
-
): PluvIO<
|
|
30
|
-
TPlatform,
|
|
31
|
-
PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
|
|
32
|
-
TContext,
|
|
33
|
-
TCrdt
|
|
34
|
-
> => {
|
|
35
|
-
const { authorize, context, crdt, debug, limits, platform } = params;
|
|
36
|
-
|
|
37
|
-
return new PluvIO<
|
|
38
|
-
TPlatform,
|
|
39
|
-
PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
|
|
40
|
-
TContext,
|
|
41
|
-
TCrdt
|
|
42
|
-
>({
|
|
43
|
-
authorize,
|
|
44
|
-
context,
|
|
45
|
-
crdt,
|
|
46
|
-
debug,
|
|
47
|
-
limits,
|
|
48
|
-
platform,
|
|
49
|
-
} as PluvIOConfig<
|
|
50
|
-
TPlatform,
|
|
51
|
-
PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
|
|
52
|
-
TContext,
|
|
53
|
-
TCrdt
|
|
54
|
-
>);
|
|
55
|
-
};
|