@pluv/io 0.0.0-experimental-20250527040415

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