@pluv/io 2.0.2 → 2.2.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +15 -0
- package/dist/index.d.mts +19 -16
- package/dist/index.d.ts +19 -16
- package/dist/index.js +124 -91
- package/dist/index.mjs +124 -91
- package/package.json +5 -5
- package/src/IORoom.ts +11 -5
- package/src/PluvIO.ts +7 -5
- package/src/PluvServer.ts +238 -214
- package/src/createIO.ts +1 -1
- package/src/types.ts +13 -8
- package/src/version.generated.ts +1 -1
package/src/PluvServer.ts
CHANGED
|
@@ -50,12 +50,12 @@ export type PluvServerConfig<
|
|
|
50
50
|
> = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & {
|
|
51
51
|
authorize?: TAuthorize;
|
|
52
52
|
context?: PluvContext<TPlatform, TContext>;
|
|
53
|
-
crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any> };
|
|
53
|
+
crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any, any> };
|
|
54
54
|
debug?: boolean;
|
|
55
55
|
getInitialStorage?: GetInitialStorageFn<TContext>;
|
|
56
56
|
limits: PluvIOLimits;
|
|
57
57
|
io: PluvIO<TPlatform, TAuthorize, TContext>;
|
|
58
|
-
platform: TPlatform;
|
|
58
|
+
platform: () => TPlatform;
|
|
59
59
|
router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
60
60
|
};
|
|
61
61
|
|
|
@@ -96,211 +96,27 @@ export class PluvServer<
|
|
|
96
96
|
public readonly version: string = __PLUV_VERSION as any;
|
|
97
97
|
|
|
98
98
|
private readonly _authorize: TAuthorize = null as TAuthorize;
|
|
99
|
-
private readonly _baseRouter: PluvRouter<TPlatform, TAuthorize, TContext, {}> = new PluvRouter({
|
|
100
|
-
$getOthers: this._procedure.sync((data, { room, session, sessions }) => {
|
|
101
|
-
const currentTime = Date.now();
|
|
102
|
-
|
|
103
|
-
const others = sessions
|
|
104
|
-
.filter((wsSession) => {
|
|
105
|
-
if (wsSession.id === session?.id) return false;
|
|
106
|
-
if (wsSession.quit) return false;
|
|
107
|
-
if (currentTime - wsSession.timers.ping > PING_TIMEOUT_MS) return false;
|
|
108
|
-
|
|
109
|
-
return true;
|
|
110
|
-
})
|
|
111
|
-
.reduce<{
|
|
112
|
-
[connectionId: string]: {
|
|
113
|
-
connectionId: string;
|
|
114
|
-
room: string | null;
|
|
115
|
-
user: JsonObject | null;
|
|
116
|
-
};
|
|
117
|
-
}>(
|
|
118
|
-
(acc, { id, presence, timers, user }) => ({
|
|
119
|
-
...acc,
|
|
120
|
-
[id]: {
|
|
121
|
-
connectionId: id,
|
|
122
|
-
presence,
|
|
123
|
-
room,
|
|
124
|
-
timers: { presence: timers.presence },
|
|
125
|
-
user,
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
128
|
-
{},
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
return { $othersReceived: { others } };
|
|
132
|
-
}),
|
|
133
|
-
$initializeSession: this._procedure
|
|
134
|
-
.broadcast((data, { session }) => {
|
|
135
|
-
const presence = (data as any)?.presence;
|
|
136
|
-
const timers = session.timers;
|
|
137
|
-
|
|
138
|
-
if (!session) return {};
|
|
139
|
-
|
|
140
|
-
session.presence = presence;
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
$userJoined: {
|
|
144
|
-
connectionId: session.id,
|
|
145
|
-
user: session.user,
|
|
146
|
-
presence,
|
|
147
|
-
timers: { presence: timers.presence },
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
})
|
|
151
|
-
.self(async (data, { context, doc, room, session }) => {
|
|
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
|
-
*/
|
|
159
|
-
const update = (data as any)?.update as Maybe<string>;
|
|
160
|
-
|
|
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();
|
|
177
|
-
|
|
178
|
-
return { $storageReceived: { changeKind: "unchanged", 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
|
-
});
|
|
203
|
-
|
|
204
|
-
this._getListeners().onStorageUpdated({
|
|
205
|
-
context,
|
|
206
|
-
encodedState,
|
|
207
|
-
room,
|
|
208
|
-
user: session?.user,
|
|
209
|
-
webSocket: session?.webSocket.webSocket,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
return { $storageReceived: { changeKind: "initialized", state: encodedState } };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const encodedState = doc.getEncodedState();
|
|
216
|
-
|
|
217
|
-
return { $storageReceived: { changeKind: "empty", state: encodedState } };
|
|
218
|
-
}),
|
|
219
|
-
$ping: this._procedure.self((data, { session }) => {
|
|
220
|
-
if (!session) return {};
|
|
221
|
-
|
|
222
|
-
const currentTime = new Date().getTime();
|
|
223
|
-
const prevState = session.webSocket.state;
|
|
224
|
-
|
|
225
|
-
this._platform.setSerializedState(session.webSocket, {
|
|
226
|
-
...prevState,
|
|
227
|
-
timers: {
|
|
228
|
-
...prevState.timers,
|
|
229
|
-
ping: currentTime,
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
return { $pong: {} };
|
|
234
|
-
}),
|
|
235
|
-
$updatePresence: this._procedure.broadcast((data, context) => {
|
|
236
|
-
const presence = (data as any)?.presence;
|
|
237
|
-
const { session } = context;
|
|
238
|
-
|
|
239
|
-
if (!session) return {};
|
|
240
|
-
|
|
241
|
-
const cleanedPatch = pickBy(presence ?? {}, (value) => typeof value !== "undefined");
|
|
242
|
-
const updated = Object.assign(Object.create(null), session.presence, cleanedPatch);
|
|
243
|
-
const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
|
|
244
|
-
|
|
245
|
-
if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
`Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
context.presence = updated;
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
$presenceUpdated: {
|
|
255
|
-
presence: updated,
|
|
256
|
-
timers: { presence: session.timers.presence },
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
}),
|
|
260
|
-
$updateStorage: this._procedure.broadcast((data, { context, doc, room }) => {
|
|
261
|
-
const origin = (data as any)?.origin as Maybe<string>;
|
|
262
|
-
const update: string | null = (data as any)?.update ?? null;
|
|
263
|
-
|
|
264
|
-
if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
|
|
265
|
-
|
|
266
|
-
const updated = update === null ? doc : doc.applyEncodedState({ update });
|
|
267
|
-
const encodedState = updated.getEncodedState();
|
|
268
|
-
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
269
|
-
|
|
270
|
-
if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
|
|
271
|
-
throw new Error("Storage has exceeded the size limit");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
this._platform.persistence.setStorageState(room, encodedState).then(() => {
|
|
275
|
-
this._getListeners().onStorageUpdated({
|
|
276
|
-
context,
|
|
277
|
-
encodedState,
|
|
278
|
-
room,
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return { $storageUpdated: { state: encodedState } };
|
|
283
|
-
}),
|
|
284
|
-
});
|
|
285
99
|
private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<
|
|
286
100
|
TPlatform,
|
|
287
101
|
TContext
|
|
288
102
|
>;
|
|
289
|
-
private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
|
|
103
|
+
private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any, any> };
|
|
290
104
|
private readonly _debug: boolean;
|
|
291
105
|
private readonly _getInitialStorage: GetInitialStorageFn<TContext> | null = null;
|
|
292
106
|
private readonly _io: PluvIO<TPlatform, TAuthorize, TContext>;
|
|
293
107
|
private readonly _limits: PluvIOLimits;
|
|
294
|
-
private readonly _platform: TPlatform;
|
|
295
|
-
private readonly
|
|
108
|
+
private readonly _platform: () => TPlatform;
|
|
109
|
+
private readonly _inputRouter: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
296
110
|
|
|
297
111
|
public get fetch(): (...args: any[]) => Promise<any> {
|
|
112
|
+
const platform = this._platform();
|
|
113
|
+
|
|
298
114
|
return ((...args: any[]): Promise<any> => {
|
|
299
|
-
if (!
|
|
300
|
-
throw new Error(`\`${
|
|
115
|
+
if (!platform._fetch)
|
|
116
|
+
throw new Error(`\`${platform._name}\` does not support \`fetch\``);
|
|
301
117
|
|
|
302
|
-
return
|
|
303
|
-
}) as NonNullable<typeof
|
|
118
|
+
return platform._fetch(...args);
|
|
119
|
+
}) as NonNullable<typeof platform._fetch>;
|
|
304
120
|
}
|
|
305
121
|
|
|
306
122
|
/**
|
|
@@ -313,7 +129,7 @@ export class PluvServer<
|
|
|
313
129
|
authorize: this._authorize,
|
|
314
130
|
context: this._context,
|
|
315
131
|
events: this._router._defs.events,
|
|
316
|
-
platform: this._platform,
|
|
132
|
+
platform: this._platform(),
|
|
317
133
|
} as {
|
|
318
134
|
authorize: TAuthorize;
|
|
319
135
|
context: TContext;
|
|
@@ -322,10 +138,216 @@ export class PluvServer<
|
|
|
322
138
|
};
|
|
323
139
|
}
|
|
324
140
|
|
|
141
|
+
private get _baseRouter(): PluvRouter<TPlatform, TAuthorize, TContext, {}> {
|
|
142
|
+
return new PluvRouter({
|
|
143
|
+
$getOthers: this._procedure.sync((data, { room, session, sessions }) => {
|
|
144
|
+
const currentTime = Date.now();
|
|
145
|
+
|
|
146
|
+
const others = sessions
|
|
147
|
+
.filter((wsSession) => {
|
|
148
|
+
if (wsSession.id === session?.id) return false;
|
|
149
|
+
if (wsSession.quit) return false;
|
|
150
|
+
if (currentTime - wsSession.timers.ping > PING_TIMEOUT_MS) return false;
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
})
|
|
154
|
+
.reduce<{
|
|
155
|
+
[connectionId: string]: {
|
|
156
|
+
connectionId: string;
|
|
157
|
+
room: string | null;
|
|
158
|
+
user: JsonObject | null;
|
|
159
|
+
};
|
|
160
|
+
}>(
|
|
161
|
+
(acc, { id, presence, timers, user }) => ({
|
|
162
|
+
...acc,
|
|
163
|
+
[id]: {
|
|
164
|
+
connectionId: id,
|
|
165
|
+
presence,
|
|
166
|
+
room,
|
|
167
|
+
timers: { presence: timers.presence },
|
|
168
|
+
user,
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
{},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return { $othersReceived: { others } };
|
|
175
|
+
}),
|
|
176
|
+
$initializeSession: this._procedure
|
|
177
|
+
.broadcast((data, { session }) => {
|
|
178
|
+
const presence = (data as any)?.presence;
|
|
179
|
+
const timers = session.timers;
|
|
180
|
+
|
|
181
|
+
if (!session) return {};
|
|
182
|
+
|
|
183
|
+
session.presence = presence;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
$userJoined: {
|
|
187
|
+
connectionId: session.id,
|
|
188
|
+
user: session.user,
|
|
189
|
+
presence,
|
|
190
|
+
timers: { presence: timers.presence },
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
})
|
|
194
|
+
.self(async (data, { context, doc, platform, room, session }) => {
|
|
195
|
+
const oldState = await platform.persistence.getStorageState(room);
|
|
196
|
+
/**
|
|
197
|
+
* !HACK
|
|
198
|
+
* @description This is the frontend's initialStorage. We only want to
|
|
199
|
+
* apply this if the server's storage is empty (i.e. no initial storage
|
|
200
|
+
* has been applied)
|
|
201
|
+
*/
|
|
202
|
+
const update = (data as any)?.update as Maybe<string>;
|
|
203
|
+
|
|
204
|
+
if (!oldState) {
|
|
205
|
+
const loadedState = await this._getInitialStorage?.({ context, room });
|
|
206
|
+
|
|
207
|
+
if (!!loadedState) {
|
|
208
|
+
doc.applyEncodedState({ update: loadedState }).getEncodedState();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @description Storage was already initialized. Don't overwrite the current
|
|
214
|
+
* storage state with the incoming initial storage. Return what the current state
|
|
215
|
+
* is without changes.
|
|
216
|
+
* @date May 7, 2025
|
|
217
|
+
*/
|
|
218
|
+
if (!doc.isEmpty()) {
|
|
219
|
+
const encodedState = doc.getEncodedState();
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
$storageReceived: { changeKind: "unchanged", state: encodedState },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @description Storage was never initialized, and there is an incoming
|
|
228
|
+
* initialStorage from the client. Apply the initialStorage and persist the state
|
|
229
|
+
* as an update.
|
|
230
|
+
* @date May 7, 2025
|
|
231
|
+
*/
|
|
232
|
+
if (!!update) {
|
|
233
|
+
const encodedState = doc.applyEncodedState({ update }).getEncodedState();
|
|
234
|
+
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
!!this._limits.storageMaxSize &&
|
|
238
|
+
storageSize > this._limits.storageMaxSize
|
|
239
|
+
) {
|
|
240
|
+
throw new Error("Storage has exceeded the size limit");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await platform.persistence
|
|
244
|
+
.setStorageState(room, encodedState)
|
|
245
|
+
.catch((error) => {
|
|
246
|
+
this._logDebug(error);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
this._getListeners().onStorageUpdated({
|
|
250
|
+
context,
|
|
251
|
+
encodedState,
|
|
252
|
+
platform,
|
|
253
|
+
room,
|
|
254
|
+
user: session?.user,
|
|
255
|
+
webSocket: session?.webSocket.webSocket,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
$storageReceived: { changeKind: "initialized", state: encodedState },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const encodedState = doc.getEncodedState();
|
|
264
|
+
|
|
265
|
+
return { $storageReceived: { changeKind: "empty", state: encodedState } };
|
|
266
|
+
}),
|
|
267
|
+
$ping: this._procedure.self((data, { platform, session }) => {
|
|
268
|
+
if (!session) return {};
|
|
269
|
+
|
|
270
|
+
const currentTime = new Date().getTime();
|
|
271
|
+
const prevState = session.webSocket.state;
|
|
272
|
+
|
|
273
|
+
platform.setSerializedState(session.webSocket, {
|
|
274
|
+
...prevState,
|
|
275
|
+
timers: {
|
|
276
|
+
...prevState.timers,
|
|
277
|
+
ping: currentTime,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return { $pong: {} };
|
|
282
|
+
}),
|
|
283
|
+
$updatePresence: this._procedure.broadcast((data, context) => {
|
|
284
|
+
const presence = (data as any)?.presence;
|
|
285
|
+
const { session } = context;
|
|
286
|
+
|
|
287
|
+
if (!session) return {};
|
|
288
|
+
|
|
289
|
+
const cleanedPatch = pickBy(
|
|
290
|
+
presence ?? {},
|
|
291
|
+
(value) => typeof value !== "undefined",
|
|
292
|
+
);
|
|
293
|
+
const updated = Object.assign(Object.create(null), session.presence, cleanedPatch);
|
|
294
|
+
const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
|
|
295
|
+
|
|
296
|
+
if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
context.presence = updated;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
$presenceUpdated: {
|
|
306
|
+
presence: updated,
|
|
307
|
+
timers: { presence: session.timers.presence },
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}),
|
|
311
|
+
$updateStorage: this._procedure.broadcast((data, { context, doc, platform, room }) => {
|
|
312
|
+
const origin = (data as any)?.origin as Maybe<string>;
|
|
313
|
+
const update: string | null = (data as any)?.update ?? null;
|
|
314
|
+
|
|
315
|
+
if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
|
|
316
|
+
|
|
317
|
+
const updated = update === null ? doc : doc.applyEncodedState({ update });
|
|
318
|
+
const encodedState = updated.getEncodedState();
|
|
319
|
+
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
320
|
+
|
|
321
|
+
if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
|
|
322
|
+
throw new Error("Storage has exceeded the size limit");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
platform.persistence.setStorageState(room, encodedState).then(() => {
|
|
326
|
+
this._getListeners().onStorageUpdated({
|
|
327
|
+
context,
|
|
328
|
+
encodedState,
|
|
329
|
+
platform,
|
|
330
|
+
room,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return { $storageUpdated: { state: encodedState } };
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
325
339
|
private get _procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}> {
|
|
326
340
|
return new PluvProcedure();
|
|
327
341
|
}
|
|
328
342
|
|
|
343
|
+
private get _router(): PluvRouter<TPlatform, TAuthorize, TContext, TEvents> {
|
|
344
|
+
return (
|
|
345
|
+
this._inputRouter
|
|
346
|
+
? PluvRouter.merge(this._baseRouter, this._inputRouter)
|
|
347
|
+
: this._baseRouter
|
|
348
|
+
) as PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
constructor(options: PluvServerConfig<TPlatform, TAuthorize, TContext, TEvents>) {
|
|
330
352
|
const {
|
|
331
353
|
authorize,
|
|
@@ -347,16 +369,14 @@ export class PluvServer<
|
|
|
347
369
|
onUserDisconnected,
|
|
348
370
|
} = options as Partial<BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>>;
|
|
349
371
|
|
|
350
|
-
platform.validateConfig(options);
|
|
372
|
+
platform().validateConfig(options);
|
|
351
373
|
|
|
352
374
|
this._crdt = crdt;
|
|
353
375
|
this._debug = debug;
|
|
376
|
+
this._inputRouter = router;
|
|
354
377
|
this._io = io;
|
|
355
378
|
this._limits = limits;
|
|
356
379
|
this._platform = platform;
|
|
357
|
-
this._router = (
|
|
358
|
-
router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter
|
|
359
|
-
) as PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
360
380
|
|
|
361
381
|
if (authorize) this._authorize = authorize;
|
|
362
382
|
if (context) this._context = context;
|
|
@@ -378,8 +398,10 @@ export class PluvServer<
|
|
|
378
398
|
const { _meta, debug, onDestroy, onMessage, ...platformRoomContext } = (options[0] ??
|
|
379
399
|
{}) as CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>[0] & { _meta?: any };
|
|
380
400
|
|
|
381
|
-
|
|
382
|
-
|
|
401
|
+
const platform = this._platform();
|
|
402
|
+
|
|
403
|
+
if (platform._config.handleMode !== "io") {
|
|
404
|
+
throw new Error(`\`createRoom\` is unsupported for \`${platform._name}\``);
|
|
383
405
|
}
|
|
384
406
|
|
|
385
407
|
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
|
|
@@ -388,6 +410,8 @@ export class PluvServer<
|
|
|
388
410
|
const roomContext = platformRoomContext as InferRoomContextType<TPlatform>;
|
|
389
411
|
const context: TContext =
|
|
390
412
|
typeof this._context === "function" ? this._context(roomContext) : this._context;
|
|
413
|
+
const listeners = this._getListeners();
|
|
414
|
+
const logDebug = this._logDebug.bind(this);
|
|
391
415
|
|
|
392
416
|
const newRoom = new IORoom<TPlatform, TAuthorize, TContext, TEvents>(room, {
|
|
393
417
|
...(!!_meta ? { _meta } : {}),
|
|
@@ -395,26 +419,26 @@ export class PluvServer<
|
|
|
395
419
|
context,
|
|
396
420
|
crdt: this._crdt,
|
|
397
421
|
debug: debug ?? this._debug,
|
|
398
|
-
|
|
399
|
-
|
|
422
|
+
async onDestroy(event) {
|
|
423
|
+
logDebug(`${colors.blue("Deleting empty room:")} ${room}`);
|
|
400
424
|
|
|
401
425
|
await Promise.resolve(onDestroy?.(event));
|
|
402
|
-
await Promise.resolve(
|
|
403
|
-
await
|
|
426
|
+
await Promise.resolve(listeners.onRoomDeleted(event));
|
|
427
|
+
await event.platform.persistence.deleteStorageState(room);
|
|
404
428
|
|
|
405
|
-
|
|
429
|
+
logDebug(`${colors.blue("Deleted room:")} ${room}`);
|
|
406
430
|
},
|
|
407
|
-
|
|
431
|
+
async onMessage(event) {
|
|
408
432
|
await Promise.resolve(onMessage?.(event));
|
|
409
|
-
await Promise.resolve(
|
|
433
|
+
await Promise.resolve(listeners.onRoomMessage(event));
|
|
410
434
|
},
|
|
411
|
-
|
|
412
|
-
await Promise.resolve(
|
|
435
|
+
async onUserConnected(event) {
|
|
436
|
+
await Promise.resolve(listeners.onUserConnected(event));
|
|
413
437
|
},
|
|
414
|
-
|
|
415
|
-
await Promise.resolve(
|
|
438
|
+
async onUserDisconnected(event) {
|
|
439
|
+
await Promise.resolve(listeners.onUserDisconnected(event));
|
|
416
440
|
},
|
|
417
|
-
platform
|
|
441
|
+
platform,
|
|
418
442
|
roomContext,
|
|
419
443
|
router: this._router,
|
|
420
444
|
});
|
package/src/createIO.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type PluvContext<TPlatform extends AbstractPlatform, TContext extends Rec
|
|
|
29
29
|
| ((params: InferRoomContextType<TPlatform>) => TContext);
|
|
30
30
|
|
|
31
31
|
export interface CrdtLibraryType {
|
|
32
|
-
doc: (value: any) => AbstractCrdtDocFactory<any>;
|
|
32
|
+
doc: (value: any) => AbstractCrdtDocFactory<any, any>;
|
|
33
33
|
kind: "loro" | "yjs";
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -54,8 +54,9 @@ export interface EventResolverContext<
|
|
|
54
54
|
TContext extends Record<string, any> = {},
|
|
55
55
|
> {
|
|
56
56
|
context: TContext;
|
|
57
|
-
doc: CrdtDocLike<any>;
|
|
57
|
+
doc: CrdtDocLike<any, any>;
|
|
58
58
|
garbageCollect: () => Promise<void>;
|
|
59
|
+
platform: TPlatform;
|
|
59
60
|
presence: JsonObject | null;
|
|
60
61
|
room: string;
|
|
61
62
|
session: TKind extends "sync"
|
|
@@ -176,7 +177,7 @@ export type BasePluvIOListeners<
|
|
|
176
177
|
TContext extends Record<string, any>,
|
|
177
178
|
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
|
|
178
179
|
> = {
|
|
179
|
-
onRoomDeleted: (event: IORoomListenerEvent<TContext>) => void;
|
|
180
|
+
onRoomDeleted: (event: IORoomListenerEvent<TPlatform, TContext>) => void;
|
|
180
181
|
onRoomMessage: (event: IORoomMessageEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
|
|
181
182
|
onStorageUpdated: (event: IOStorageUpdatedEvent<TPlatform, TAuthorize, TContext>) => void;
|
|
182
183
|
onUserConnected: (event: IOUserConnectedEvent<TPlatform, TAuthorize, TContext>) => void;
|
|
@@ -230,9 +231,13 @@ export type InferPlatformListeners<TPlatform extends AbstractPlatform<any, any,
|
|
|
230
231
|
export type InferPlatformRouter<TPlatform extends AbstractPlatform<any, any, any, any>> =
|
|
231
232
|
InferPlatformConfig<TPlatform>["router"];
|
|
232
233
|
|
|
233
|
-
export type IORoomListenerEvent<
|
|
234
|
+
export type IORoomListenerEvent<
|
|
235
|
+
TPlatform extends AbstractPlatform<any, any, any, any>,
|
|
236
|
+
TContext extends Record<string, any>,
|
|
237
|
+
> = {
|
|
234
238
|
context: TContext;
|
|
235
239
|
encodedState: string | null;
|
|
240
|
+
platform: TPlatform;
|
|
236
241
|
room: string;
|
|
237
242
|
};
|
|
238
243
|
|
|
@@ -241,7 +246,7 @@ export type IORoomMessageEvent<
|
|
|
241
246
|
TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null,
|
|
242
247
|
TContext extends Record<string, any>,
|
|
243
248
|
TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
|
|
244
|
-
> = IORoomListenerEvent<TContext> & {
|
|
249
|
+
> = IORoomListenerEvent<TPlatform, TContext> & {
|
|
245
250
|
message: InferEventMessage<InferEventsOutput<TEvents>, keyof InferEventsOutput<TEvents>>;
|
|
246
251
|
user?: InferIOAuthorizeUser<TAuthorize>;
|
|
247
252
|
webSocket?: InferPlatformWebSocketSource<TPlatform>;
|
|
@@ -251,7 +256,7 @@ export type IOStorageUpdatedEvent<
|
|
|
251
256
|
TPlatform extends AbstractPlatform<any>,
|
|
252
257
|
TAuthorize extends IOAuthorize<any, InferInitContextType<TPlatform>> | null,
|
|
253
258
|
TContext extends Record<string, any>,
|
|
254
|
-
> = IORoomListenerEvent<TContext> & {
|
|
259
|
+
> = IORoomListenerEvent<TPlatform, TContext> & {
|
|
255
260
|
user?: InferIOAuthorizeUser<TAuthorize>;
|
|
256
261
|
webSocket?: InferPlatformWebSocketSource<TPlatform>;
|
|
257
262
|
};
|
|
@@ -260,7 +265,7 @@ export type IOUserConnectedEvent<
|
|
|
260
265
|
TPlatform extends AbstractPlatform<any>,
|
|
261
266
|
TAuthorize extends IOAuthorize<any, InferInitContextType<TPlatform>> | null,
|
|
262
267
|
TContext extends Record<string, any>,
|
|
263
|
-
> = IORoomListenerEvent<TContext> & {
|
|
268
|
+
> = IORoomListenerEvent<TPlatform, TContext> & {
|
|
264
269
|
user?: InferIOAuthorizeUser<TAuthorize>;
|
|
265
270
|
webSocket?: InferPlatformWebSocketSource<TPlatform>;
|
|
266
271
|
};
|
|
@@ -269,7 +274,7 @@ export type IOUserDisconnectedEvent<
|
|
|
269
274
|
TPlatform extends AbstractPlatform<any>,
|
|
270
275
|
TAuthorize extends IOAuthorize<any, InferInitContextType<TPlatform>> | null,
|
|
271
276
|
TContext extends Record<string, any>,
|
|
272
|
-
> = IORoomListenerEvent<TContext> & {
|
|
277
|
+
> = IORoomListenerEvent<TPlatform, TContext> & {
|
|
273
278
|
user?: InferIOAuthorizeUser<TAuthorize>;
|
|
274
279
|
};
|
|
275
280
|
|
package/src/version.generated.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '2.0
|
|
2
|
+
export const version = '2.2.0'
|