@pluv/io 1.0.0 → 1.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +15 -0
- package/README.md +5 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +67 -32
- package/dist/index.mjs +67 -32
- 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 +101 -37
- 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
|
-
|
|
134
|
-
|
|
161
|
+
if (!oldState) {
|
|
162
|
+
const loadedState = await this._getInitialStorage?.({ context, room });
|
|
163
|
+
|
|
164
|
+
if (!!loadedState) {
|
|
165
|
+
doc.applyEncodedState({ update: loadedState }).getEncodedState();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
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();
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
return { $storageReceived: { changeKind: "unchanged", state: encodedState } };
|
|
179
|
+
}
|
|
138
180
|
|
|
139
|
-
|
|
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,
|
|
@@ -145,16 +208,13 @@ export class PluvServer<
|
|
|
145
208
|
user: session?.user,
|
|
146
209
|
webSocket: session?.webSocket.webSocket,
|
|
147
210
|
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const encodedState = (await this._platform.persistence.getStorageState(room)) ?? doc.getEncodedState();
|
|
151
|
-
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
152
211
|
|
|
153
|
-
|
|
154
|
-
throw new Error("Storage has exceeded the size limit");
|
|
212
|
+
return { $storageReceived: { changeKind: "initialized", state: encodedState } };
|
|
155
213
|
}
|
|
156
214
|
|
|
157
|
-
|
|
215
|
+
const encodedState = doc.getEncodedState();
|
|
216
|
+
|
|
217
|
+
return { $storageReceived: { changeKind: "empty", state: encodedState } };
|
|
158
218
|
}),
|
|
159
219
|
$ping: this._procedure.self((data, { session }) => {
|
|
160
220
|
if (!session) return {};
|
|
@@ -222,7 +282,10 @@ export class PluvServer<
|
|
|
222
282
|
return { $storageUpdated: { state: encodedState } };
|
|
223
283
|
}),
|
|
224
284
|
});
|
|
225
|
-
private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<
|
|
285
|
+
private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<
|
|
286
|
+
TPlatform,
|
|
287
|
+
TContext
|
|
288
|
+
>;
|
|
226
289
|
private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
|
|
227
290
|
private readonly _debug: boolean;
|
|
228
291
|
private readonly _getInitialStorage: GetInitialStorageFn<TContext> | null = null;
|
|
@@ -233,7 +296,8 @@ export class PluvServer<
|
|
|
233
296
|
|
|
234
297
|
public get fetch(): (...args: any[]) => Promise<any> {
|
|
235
298
|
return ((...args: any[]): Promise<any> => {
|
|
236
|
-
if (!this._platform._fetch)
|
|
299
|
+
if (!this._platform._fetch)
|
|
300
|
+
throw new Error(`\`${this._platform._name}\` does not support \`fetch\``);
|
|
237
301
|
|
|
238
302
|
return this._platform._fetch(...args);
|
|
239
303
|
}) as NonNullable<typeof this._platform._fetch>;
|
|
@@ -275,8 +339,13 @@ export class PluvServer<
|
|
|
275
339
|
router = new PluvRouter<TPlatform, TAuthorize, TContext, TEvents>({} as TEvents),
|
|
276
340
|
} = options;
|
|
277
341
|
|
|
278
|
-
const {
|
|
279
|
-
|
|
342
|
+
const {
|
|
343
|
+
onRoomDeleted,
|
|
344
|
+
onRoomMessage,
|
|
345
|
+
onStorageUpdated,
|
|
346
|
+
onUserConnected,
|
|
347
|
+
onUserDisconnected,
|
|
348
|
+
} = options as Partial<BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>>;
|
|
280
349
|
|
|
281
350
|
platform.validateConfig(options);
|
|
282
351
|
|
|
@@ -285,12 +354,9 @@ export class PluvServer<
|
|
|
285
354
|
this._io = io;
|
|
286
355
|
this._limits = limits;
|
|
287
356
|
this._platform = platform;
|
|
288
|
-
this._router = (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
TContext,
|
|
292
|
-
TEvents
|
|
293
|
-
>;
|
|
357
|
+
this._router = (
|
|
358
|
+
router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter
|
|
359
|
+
) as PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
294
360
|
|
|
295
361
|
if (authorize) this._authorize = authorize;
|
|
296
362
|
if (context) this._context = context;
|
|
@@ -309,21 +375,19 @@ export class PluvServer<
|
|
|
309
375
|
room: string,
|
|
310
376
|
...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>
|
|
311
377
|
): 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 };
|
|
378
|
+
const { _meta, debug, onDestroy, onMessage, ...platformRoomContext } = (options[0] ??
|
|
379
|
+
{}) as CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>[0] & { _meta?: any };
|
|
318
380
|
|
|
319
381
|
if (this._platform._config.handleMode !== "io") {
|
|
320
382
|
throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
|
|
321
383
|
}
|
|
322
384
|
|
|
323
|
-
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
|
|
385
|
+
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
|
|
386
|
+
throw new Error("Unsupported room name");
|
|
324
387
|
|
|
325
388
|
const roomContext = platformRoomContext as InferRoomContextType<TPlatform>;
|
|
326
|
-
const context: TContext =
|
|
389
|
+
const context: TContext =
|
|
390
|
+
typeof this._context === "function" ? this._context(roomContext) : this._context;
|
|
327
391
|
|
|
328
392
|
const newRoom = new IORoom<TPlatform, TAuthorize, TContext, TEvents>(room, {
|
|
329
393
|
...(!!_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 = '1.0.
|
|
2
|
+
export const version = '1.0.2'
|