@pluv/io 0.44.2 → 1.0.1
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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +24 -0
- package/README.md +137 -67
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +65 -31
- package/dist/index.mjs +65 -31
- package/package.json +5 -5
- package/src/AbstractPersistence.ts +8 -2
- package/src/AbstractPlatform.ts +6 -2
- package/src/AbstractWebSocket.ts +11 -3
- package/src/IORoom.ts +113 -37
- package/src/PluvIO.ts +38 -11
- package/src/PluvProcedure.ts +57 -10
- package/src/PluvRouter.ts +14 -3
- package/src/PluvServer.ts +98 -36
- package/src/PubSub.ts +4 -1
- package/src/authorize.ts +4 -1
- package/src/createIO.ts +10 -2
- package/src/types.ts +25 -11
- package/src/utils/pickBy.ts +4 -1
- package/src/version.generated.ts +1 -1
package/src/PluvServer.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { AbstractCrdtDocFactory } from "@pluv/crdt";
|
|
2
2
|
import { noop } from "@pluv/crdt";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
IOLike,
|
|
5
|
+
Id,
|
|
6
|
+
InferIOAuthorize,
|
|
7
|
+
InferIOAuthorizeUser,
|
|
8
|
+
JsonObject,
|
|
9
|
+
Maybe,
|
|
10
|
+
} from "@pluv/types";
|
|
4
11
|
import colors from "kleur";
|
|
5
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
AbstractPlatform,
|
|
14
|
+
InferInitContextType,
|
|
15
|
+
InferRoomContextType,
|
|
16
|
+
} from "./AbstractPlatform";
|
|
6
17
|
import type { IORoomListeners } from "./IORoom";
|
|
7
18
|
import { IORoom } from "./IORoom";
|
|
8
19
|
import type { PluvIO } from "./PluvIO";
|
|
@@ -29,7 +40,11 @@ export type InferIORoom<TServer extends PluvServer<any, any, any, any>> =
|
|
|
29
40
|
|
|
30
41
|
export type PluvServerConfig<
|
|
31
42
|
TPlatform extends AbstractPlatform<any, any> = AbstractPlatform<any, any>,
|
|
32
|
-
TAuthorize extends PluvIOAuthorize<
|
|
43
|
+
TAuthorize extends PluvIOAuthorize<
|
|
44
|
+
TPlatform,
|
|
45
|
+
any,
|
|
46
|
+
InferInitContextType<TPlatform>
|
|
47
|
+
> | null = any,
|
|
33
48
|
TContext extends Record<string, any> = {},
|
|
34
49
|
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
|
|
35
50
|
> = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & {
|
|
@@ -60,11 +75,20 @@ export type CreateRoomOptions<
|
|
|
60
75
|
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
|
|
61
76
|
> = keyof InferRoomContextType<TPlatform> extends never
|
|
62
77
|
? [BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>] | []
|
|
63
|
-
: [
|
|
78
|
+
: [
|
|
79
|
+
Id<
|
|
80
|
+
BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents> &
|
|
81
|
+
InferRoomContextType<TPlatform>
|
|
82
|
+
>,
|
|
83
|
+
];
|
|
64
84
|
|
|
65
85
|
export class PluvServer<
|
|
66
86
|
TPlatform extends AbstractPlatform<any, any> = AbstractPlatform<any, any>,
|
|
67
|
-
TAuthorize extends PluvIOAuthorize<
|
|
87
|
+
TAuthorize extends PluvIOAuthorize<
|
|
88
|
+
TPlatform,
|
|
89
|
+
any,
|
|
90
|
+
InferInitContextType<TPlatform>
|
|
91
|
+
> | null = any,
|
|
68
92
|
TContext extends Record<string, any> = {},
|
|
69
93
|
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
|
|
70
94
|
> implements IOLike<TAuthorize, TEvents>
|
|
@@ -126,17 +150,56 @@ export class PluvServer<
|
|
|
126
150
|
})
|
|
127
151
|
.self(async (data, { context, doc, room, session }) => {
|
|
128
152
|
const oldState = await this._platform.persistence.getStorageState(room);
|
|
153
|
+
/**
|
|
154
|
+
* !HACK
|
|
155
|
+
* @description This is the frontend's initialStorage. We only want to
|
|
156
|
+
* apply this if the server's storage is empty (i.e. no initial storage
|
|
157
|
+
* has been applied)
|
|
158
|
+
*/
|
|
129
159
|
const update = (data as any)?.update as Maybe<string>;
|
|
130
160
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
update,
|
|
134
|
-
];
|
|
161
|
+
if (!oldState) {
|
|
162
|
+
const loadedState = await this._getInitialStorage?.({ context, room });
|
|
135
163
|
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
if (!!loadedState) {
|
|
165
|
+
doc.applyEncodedState({ update: loadedState }).getEncodedState();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
138
168
|
|
|
139
|
-
|
|
169
|
+
/**
|
|
170
|
+
* @description Storage was already initialized. Don't overwrite the current
|
|
171
|
+
* storage state with the incoming initial storage. Return what the current state
|
|
172
|
+
* is without changes.
|
|
173
|
+
* @date May 7, 2025
|
|
174
|
+
*/
|
|
175
|
+
if (!doc.isEmpty()) {
|
|
176
|
+
const encodedState = doc.getEncodedState();
|
|
177
|
+
|
|
178
|
+
return { $storageReceived: { state: encodedState } };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @description Storage was never initialized, and there is an incoming
|
|
183
|
+
* initialStorage from the client. Apply the initialStorage and persist the state
|
|
184
|
+
* as an update.
|
|
185
|
+
* @date May 7, 2025
|
|
186
|
+
*/
|
|
187
|
+
if (!!update) {
|
|
188
|
+
const encodedState = doc.applyEncodedState({ update }).getEncodedState();
|
|
189
|
+
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
!!this._limits.storageMaxSize &&
|
|
193
|
+
storageSize > this._limits.storageMaxSize
|
|
194
|
+
) {
|
|
195
|
+
throw new Error("Storage has exceeded the size limit");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await this._platform.persistence
|
|
199
|
+
.setStorageState(room, encodedState)
|
|
200
|
+
.catch((error) => {
|
|
201
|
+
this._logDebug(error);
|
|
202
|
+
});
|
|
140
203
|
|
|
141
204
|
this._getListeners().onStorageUpdated({
|
|
142
205
|
context,
|
|
@@ -147,12 +210,7 @@ export class PluvServer<
|
|
|
147
210
|
});
|
|
148
211
|
}
|
|
149
212
|
|
|
150
|
-
const encodedState =
|
|
151
|
-
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
152
|
-
|
|
153
|
-
if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
|
|
154
|
-
throw new Error("Storage has exceeded the size limit");
|
|
155
|
-
}
|
|
213
|
+
const encodedState = doc.getEncodedState();
|
|
156
214
|
|
|
157
215
|
return { $storageReceived: { state: encodedState } };
|
|
158
216
|
}),
|
|
@@ -222,7 +280,10 @@ export class PluvServer<
|
|
|
222
280
|
return { $storageUpdated: { state: encodedState } };
|
|
223
281
|
}),
|
|
224
282
|
});
|
|
225
|
-
private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<
|
|
283
|
+
private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<
|
|
284
|
+
TPlatform,
|
|
285
|
+
TContext
|
|
286
|
+
>;
|
|
226
287
|
private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
|
|
227
288
|
private readonly _debug: boolean;
|
|
228
289
|
private readonly _getInitialStorage: GetInitialStorageFn<TContext> | null = null;
|
|
@@ -233,7 +294,8 @@ export class PluvServer<
|
|
|
233
294
|
|
|
234
295
|
public get fetch(): (...args: any[]) => Promise<any> {
|
|
235
296
|
return ((...args: any[]): Promise<any> => {
|
|
236
|
-
if (!this._platform._fetch)
|
|
297
|
+
if (!this._platform._fetch)
|
|
298
|
+
throw new Error(`\`${this._platform._name}\` does not support \`fetch\``);
|
|
237
299
|
|
|
238
300
|
return this._platform._fetch(...args);
|
|
239
301
|
}) as NonNullable<typeof this._platform._fetch>;
|
|
@@ -275,8 +337,13 @@ export class PluvServer<
|
|
|
275
337
|
router = new PluvRouter<TPlatform, TAuthorize, TContext, TEvents>({} as TEvents),
|
|
276
338
|
} = options;
|
|
277
339
|
|
|
278
|
-
const {
|
|
279
|
-
|
|
340
|
+
const {
|
|
341
|
+
onRoomDeleted,
|
|
342
|
+
onRoomMessage,
|
|
343
|
+
onStorageUpdated,
|
|
344
|
+
onUserConnected,
|
|
345
|
+
onUserDisconnected,
|
|
346
|
+
} = options as Partial<BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>>;
|
|
280
347
|
|
|
281
348
|
platform.validateConfig(options);
|
|
282
349
|
|
|
@@ -285,12 +352,9 @@ export class PluvServer<
|
|
|
285
352
|
this._io = io;
|
|
286
353
|
this._limits = limits;
|
|
287
354
|
this._platform = platform;
|
|
288
|
-
this._router = (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
TContext,
|
|
292
|
-
TEvents
|
|
293
|
-
>;
|
|
355
|
+
this._router = (
|
|
356
|
+
router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter
|
|
357
|
+
) as PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
294
358
|
|
|
295
359
|
if (authorize) this._authorize = authorize;
|
|
296
360
|
if (context) this._context = context;
|
|
@@ -309,21 +373,19 @@ export class PluvServer<
|
|
|
309
373
|
room: string,
|
|
310
374
|
...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>
|
|
311
375
|
): IORoom<TPlatform, TAuthorize, TContext, TEvents> {
|
|
312
|
-
const { _meta, debug, onDestroy, onMessage, ...platformRoomContext } = (options[0] ??
|
|
313
|
-
TPlatform,
|
|
314
|
-
TAuthorize,
|
|
315
|
-
TContext,
|
|
316
|
-
TEvents
|
|
317
|
-
>[0] & { _meta?: any };
|
|
376
|
+
const { _meta, debug, onDestroy, onMessage, ...platformRoomContext } = (options[0] ??
|
|
377
|
+
{}) as CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>[0] & { _meta?: any };
|
|
318
378
|
|
|
319
379
|
if (this._platform._config.handleMode !== "io") {
|
|
320
380
|
throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
|
|
321
381
|
}
|
|
322
382
|
|
|
323
|
-
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
|
|
383
|
+
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
|
|
384
|
+
throw new Error("Unsupported room name");
|
|
324
385
|
|
|
325
386
|
const roomContext = platformRoomContext as InferRoomContextType<TPlatform>;
|
|
326
|
-
const context: TContext =
|
|
387
|
+
const context: TContext =
|
|
388
|
+
typeof this._context === "function" ? this._context(roomContext) : this._context;
|
|
327
389
|
|
|
328
390
|
const newRoom = new IORoom<TPlatform, TAuthorize, TContext, TEvents>(room, {
|
|
329
391
|
...(!!_meta ? { _meta } : {}),
|
package/src/PubSub.ts
CHANGED
|
@@ -20,7 +20,10 @@ export class PubSub extends AbstractPubSub {
|
|
|
20
20
|
Promise.resolve(subject.next(payload));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
public subscribe(
|
|
23
|
+
public subscribe(
|
|
24
|
+
roomName: string,
|
|
25
|
+
onMessage: (message: IOPubSubEventMessage<any>) => void,
|
|
26
|
+
): Promise<number> {
|
|
24
27
|
const id = this._currentSubscriptionId++;
|
|
25
28
|
const refs = this._subscriptionsRefs.get(roomName) ?? [];
|
|
26
29
|
|
package/src/authorize.ts
CHANGED
|
@@ -15,7 +15,10 @@ export interface JWT<TUser extends BaseUser> {
|
|
|
15
15
|
user: TUser;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export type JWTEncodeParams<
|
|
18
|
+
export type JWTEncodeParams<
|
|
19
|
+
TUser extends BaseUser | null,
|
|
20
|
+
TPlatform extends AbstractPlatform<any, any>,
|
|
21
|
+
> = {
|
|
19
22
|
maxAge?: number;
|
|
20
23
|
room: string;
|
|
21
24
|
user: TUser extends BaseUser ? TUser : "Error: Cannot create token without authorization!";
|
package/src/createIO.ts
CHANGED
|
@@ -22,10 +22,18 @@ export const createIO = <
|
|
|
22
22
|
TUser extends BaseUser | null = null,
|
|
23
23
|
>(
|
|
24
24
|
params: CreateIOParams<TPlatform, TContext, TUser>,
|
|
25
|
-
): PluvIO<
|
|
25
|
+
): PluvIO<
|
|
26
|
+
TPlatform,
|
|
27
|
+
PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
|
|
28
|
+
TContext
|
|
29
|
+
> => {
|
|
26
30
|
const { authorize, context, crdt, debug, limits, platform } = params;
|
|
27
31
|
|
|
28
|
-
return new PluvIO<
|
|
32
|
+
return new PluvIO<
|
|
33
|
+
TPlatform,
|
|
34
|
+
PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
|
|
35
|
+
TContext
|
|
36
|
+
>({
|
|
29
37
|
authorize,
|
|
30
38
|
context,
|
|
31
39
|
crdt,
|
package/src/types.ts
CHANGED
|
@@ -57,7 +57,9 @@ export interface EventResolverContext<
|
|
|
57
57
|
garbageCollect: () => Promise<void>;
|
|
58
58
|
presence: JsonObject | null;
|
|
59
59
|
room: string;
|
|
60
|
-
session: TKind extends "sync"
|
|
60
|
+
session: TKind extends "sync"
|
|
61
|
+
? WebSocketSession<TAuthorize> | null
|
|
62
|
+
: WebSocketSession<TAuthorize>;
|
|
61
63
|
sessions: readonly WebSocketSession<TAuthorize>[];
|
|
62
64
|
time: number;
|
|
63
65
|
}
|
|
@@ -79,11 +81,12 @@ export interface WebSocketSerializedState {
|
|
|
79
81
|
timers: WebSocketSessionTimers;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
export type WebSocketSession<TAuthorize extends IOAuthorize<any, any> | null = null> =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
export type WebSocketSession<TAuthorize extends IOAuthorize<any, any> | null = null> =
|
|
85
|
+
WebSocketSerializedState & {
|
|
86
|
+
id: string;
|
|
87
|
+
user: InferIOAuthorizeUser<TAuthorize>;
|
|
88
|
+
webSocket: AbstractWebSocket;
|
|
89
|
+
};
|
|
87
90
|
|
|
88
91
|
export type MergeEventRecords<
|
|
89
92
|
TEventRecords extends EventRecord<string, any>[],
|
|
@@ -143,7 +146,9 @@ export type PluvIOAuthorize<
|
|
|
143
146
|
TContext extends Record<string, unknown> = {},
|
|
144
147
|
> =
|
|
145
148
|
| (TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null)
|
|
146
|
-
| ((
|
|
149
|
+
| ((
|
|
150
|
+
context: TContext,
|
|
151
|
+
) => TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null);
|
|
147
152
|
|
|
148
153
|
export interface PluvIOLimits {
|
|
149
154
|
/**
|
|
@@ -184,7 +189,10 @@ export type PluvIOListeners<
|
|
|
184
189
|
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
|
|
185
190
|
> = UndefinedProps<
|
|
186
191
|
BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>,
|
|
187
|
-
Exclude<
|
|
192
|
+
Exclude<
|
|
193
|
+
keyof BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>,
|
|
194
|
+
InferPlatformListeners<TPlatform>
|
|
195
|
+
>
|
|
188
196
|
>;
|
|
189
197
|
|
|
190
198
|
export type PluvIORouter<
|
|
@@ -200,14 +208,20 @@ export type PluvIORouter<
|
|
|
200
208
|
export type InferPlatformConfig<TPlatform extends AbstractPlatform<any, any, any, any>> =
|
|
201
209
|
TPlatform extends AbstractPlatform<any, any, any, infer IConfig> ? IConfig : never;
|
|
202
210
|
|
|
203
|
-
export type InferPlatformAuthorizeProperties<
|
|
204
|
-
|
|
211
|
+
export type InferPlatformAuthorizeProperties<
|
|
212
|
+
TPlatform extends AbstractPlatform<any, any, any, any>,
|
|
213
|
+
> = keyof {
|
|
214
|
+
[P in keyof PlatformConfig["authorize"] as InferPlatformConfig<TPlatform>["authorize"][P] extends
|
|
215
|
+
| true
|
|
216
|
+
| undefined
|
|
205
217
|
? P
|
|
206
218
|
: never]: true;
|
|
207
219
|
};
|
|
208
220
|
|
|
209
221
|
export type InferPlatformListeners<TPlatform extends AbstractPlatform<any, any, any, any>> = keyof {
|
|
210
|
-
[P in keyof PlatformConfig["listeners"] as InferPlatformConfig<TPlatform>["listeners"][P] extends
|
|
222
|
+
[P in keyof PlatformConfig["listeners"] as InferPlatformConfig<TPlatform>["listeners"][P] extends
|
|
223
|
+
| true
|
|
224
|
+
| undefined
|
|
211
225
|
? P
|
|
212
226
|
: never]: true;
|
|
213
227
|
};
|
package/src/utils/pickBy.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export const pickBy = <T extends Record<string, any>>(
|
|
1
|
+
export const pickBy = <T extends Record<string, any>>(
|
|
2
|
+
obj: T,
|
|
3
|
+
by: (value: T[keyof T]) => boolean,
|
|
4
|
+
): Partial<T> => {
|
|
2
5
|
const result: Partial<T> = {};
|
|
3
6
|
const keys: (keyof T)[] = Object.keys(obj);
|
|
4
7
|
|
package/src/version.generated.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '1.0.1'
|