@pluv/platform 0.0.0-stage → 5.2.3

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,177 @@
1
+ import { AbstractEventMap, AbstractListener, AbstractPersistence, AbstractPlatform, AbstractPlatformConfig, AbstractWebSocket, AbstractWebSocketConfig, ConvertWebSocketConfig, IODefs, RoomEffects, RoomJoinActor, RoomPresenceSession, RoomStorageActor, SessionRoom, WebSocketSerializedState, WebSocketSession } from "@pluv/io";
2
+ import { BaseUser, JsonObject } from "@pluv/types";
3
+ type PluvSocket = {
4
+ id: string;
5
+ lastPing: number | null;
6
+ };
7
+ type PluvRoomState = {
8
+ storage?: {
9
+ sql?: {
10
+ exec: (...args: unknown[]) => unknown;
11
+ };
12
+ };
13
+ waitUntil?: (promise: Promise<unknown>) => void;
14
+ };
15
+ type PluvRoomContext = {
16
+ env?: Record<string, unknown>;
17
+ sessions?: PluvSocket[];
18
+ state?: PluvRoomState;
19
+ };
20
+ type PlainError = {
21
+ message: string;
22
+ } | null;
23
+ /** Supervisor method the facet calls when a slow procedure produces frames. */
24
+ type FrameDelivery = {
25
+ deliverFrames(effects: RoomEffects): void | Promise<void>;
26
+ /** Starts the snapshot loop when `storageDirty` is set. */
27
+ noteStorageDirty(): void | Promise<void>;
28
+ /** Sends procedure frames after `deliverFrames` failed. */
29
+ retryUnsent(effects: RoomEffects): void | Promise<void>;
30
+ };
31
+ /** Frame delivery for a prepared storage write. `finish` settles the host call. */
32
+ type StorageFrameDelivery = FrameDelivery & {
33
+ finish(error: PlainError): void | Promise<void>;
34
+ };
35
+ /** Supervisor method the facet calls when a slow presence patch resolves. */
36
+ type PresenceDelivery = {
37
+ deliverPresence(delivery: {
38
+ error: PlainError;
39
+ name: string;
40
+ patch: JsonObject | null;
41
+ senderId: string;
42
+ }): void | Promise<void>;
43
+ };
44
+ declare class PluvWebSocket extends AbstractWebSocket<PluvSocket> {
45
+ private _readyState;
46
+ private _sessionId;
47
+ private _state;
48
+ private _user;
49
+ set presence(presence: JsonObject | null);
50
+ get readyState(): 0 | 1 | 2 | 3;
51
+ get session(): WebSocketSession<any>;
52
+ get sessionId(): string;
53
+ get state(): WebSocketSerializedState;
54
+ set state(state: WebSocketSerializedState);
55
+ get user(): BaseUser | null;
56
+ set user(user: BaseUser);
57
+ constructor(webSocket: PluvSocket, config: AbstractWebSocketConfig);
58
+ addEventListener<TType extends keyof AbstractEventMap>(_type: TType, _handler: AbstractListener<TType>): void;
59
+ close(_code?: number, _reason?: string): void;
60
+ send(_message: ArrayBuffer | ArrayBufferView | string): void;
61
+ terminate(): void;
62
+ }
63
+ type PluvPlatformConfig = AbstractPlatformConfig<PluvRoomContext> & {
64
+ persistence?: AbstractPersistence;
65
+ };
66
+ export declare class PluvPlatform extends AbstractPlatform<PluvWebSocket, {}, PluvRoomContext> {
67
+ readonly id: string;
68
+ readonly _name = "platformPluv";
69
+ private readonly _persistenceProvided;
70
+ constructor(config?: PluvPlatformConfig);
71
+ acceptWebSocket(_webSocket: PluvWebSocket): Promise<void>;
72
+ convertWebSocket(webSocket: PluvSocket, config: ConvertWebSocketConfig): PluvWebSocket;
73
+ getLastPing(webSocket: PluvWebSocket): number | null;
74
+ getSerializedState(_webSocket: PluvSocket): WebSocketSerializedState | null;
75
+ getSessionId(webSocket: PluvSocket): string | null;
76
+ getWebSockets(): readonly PluvSocket[];
77
+ initialize(config: AbstractPlatformConfig<PluvRoomContext>): this;
78
+ parseData(data: string | ArrayBuffer): Record<string, any>;
79
+ randomUUID(): string;
80
+ setSerializedState(webSocket: PluvWebSocket, state: WebSocketSerializedState): WebSocketSerializedState;
81
+ keepAlive(promise: Promise<unknown>): void;
82
+ }
83
+ export declare const platformPluv: (config?: PluvPlatformConfig) => (() => PluvPlatform);
84
+ type Room = SessionRoom<any>;
85
+ type Snapshot = Parameters<Room["~"]["close"]>[0];
86
+ type RoomFacetOptions<T extends IODefs = IODefs> = {
87
+ keepAlive: (promise: Promise<unknown>) => void;
88
+ room: SessionRoom<T>;
89
+ };
90
+ /**
91
+ * Forwards one room call and returns plain JSON. A deferred result that is still
92
+ * waiting becomes `{ suspended: true }`. The frames are delivered later on
93
+ * the `RpcTarget` the caller passed in, which this facet retains until that
94
+ * send finishes.
95
+ */
96
+ export declare class RoomFacet<T extends IODefs = IODefs> {
97
+ #private;
98
+ constructor(options: RoomFacetOptions<T>);
99
+ applyPresencePatch(params: {
100
+ data: unknown;
101
+ name: string;
102
+ onPresence: PresenceDelivery;
103
+ session: RoomPresenceSession;
104
+ }): Promise<JsonObject | null | {
105
+ error?: {
106
+ message: string;
107
+ } | null;
108
+ suspended: true;
109
+ }>;
110
+ applyStorageProcedure(params: {
111
+ actor: RoomStorageActor;
112
+ data: unknown;
113
+ name: string;
114
+ onFrames: StorageFrameDelivery;
115
+ }): Promise<RoomEffects | {
116
+ error?: {
117
+ message: string;
118
+ } | null;
119
+ suspended: true;
120
+ }>;
121
+ close(session: Snapshot): Promise<RoomEffects>;
122
+ destroy(): Promise<null>;
123
+ dropStorageUpdates(frames: Readonly<RoomEffects["broadcast"]>): Promise<void>;
124
+ queueStorageFrames(frames: Readonly<RoomEffects["broadcast"]>): void;
125
+ flushSnapshot(): Promise<{
126
+ broadcast: unknown;
127
+ status: unknown;
128
+ }>;
129
+ initializeStorage(params: {
130
+ actor: RoomJoinActor;
131
+ input: {
132
+ stateVector?: string | null;
133
+ update?: string | null;
134
+ };
135
+ }): Promise<{
136
+ changeKind: string;
137
+ state: string | null;
138
+ storageDirty: boolean;
139
+ }>;
140
+ logDebug(...data: unknown[]): null;
141
+ message(params: {
142
+ event: {
143
+ data: unknown;
144
+ type: string;
145
+ };
146
+ onFrames: FrameDelivery;
147
+ session: Snapshot;
148
+ }): Promise<RoomEffects | {
149
+ error?: {
150
+ message: string;
151
+ } | null;
152
+ suspended: true;
153
+ }>;
154
+ register(...options: Parameters<Room["~"]["register"]>): Promise<{
155
+ limits: Room["limits"];
156
+ result: {
157
+ encodedState: string | null;
158
+ error: {
159
+ message: string;
160
+ } | null;
161
+ kind: string | null;
162
+ operator: unknown;
163
+ user: unknown;
164
+ };
165
+ }>;
166
+ storageDirty(): boolean | Promise<boolean>;
167
+ userConnected(params: {
168
+ encodedState: string | null;
169
+ id: string;
170
+ kind: string;
171
+ operator: unknown;
172
+ user: unknown;
173
+ }): Promise<RoomEffects>;
174
+ whenReady(): Promise<boolean>;
175
+ }
176
+ export type { FrameDelivery, PluvRoomContext, PluvSocket, PresenceDelivery, RoomFacetOptions, StorageFrameDelivery };
177
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/PluvWebSocket.ts","../src/PluvPlatform.ts","../src/platformPluv.ts","../src/RoomFacet.ts"],"mappings":";;KAGY;EACR;EACA;;KAGQ;EACR;IACI;MACI,UAAU;;;EAGlB,aAAa,SAAS;;KAGd;EACR,MAAM;EACN,WAAW;EACX,QAAQ;;KAGA;EAAe;;;KAGf;EACR,cAAc,SAAS,qBAAqB;;EAE5C,2BAA2B;;EAE3B,YAAY,SAAS,qBAAqB;;;KAIlC,uBAAuB;EAC/B,OAAO,OAAO,oBAAoB;;;KAI1B;EACR,gBAAgB;IACZ,OAAO;IACP;IACA,OAAO;IACP;aACO;;cCnCF,sBAAsB,kBAAkB;UACzC;UACA;UACA;UACA;MAEG,SAAS,UAAU;MAInB;MAIA,WAAW;MAaX;MAQA,SAAS;MAIT,MAAM,OAAO;MAIb,QAAQ;MAIR,KAAK,MAAM;EAItB,YAAY,WAAW,YAAY,QAAQ;EAcpC,iBAAiB,oBAAoB,kBACxC,OAAO,OACP,UAAU,iBAAiB;EAGxB,MAAM,gBAAgB;EAItB,KAAK,UAAU,cAAc;EAI7B;;KC9EC,qBAAqB,uBAAuB;EACpD,cAAc;;qBAKL,qBAAqB,iBAAiB,mBAAmB;WAClD;WACA;mBAEC;EAEjB,YAAY,SAAQ;EAMb,gBAAgB,YAAY,gBAAgB;EAI5C,iBAAiB,WAAW,YAAY,QAAQ,yBAAyB;EAQzE,YAAY,WAAW;EAIvB,mBAAmB,YAAY,aAAa;EAI5C,aAAa,WAAW;EAIxB,0BAA0B;EAI1B,WAAW,QAAQ,uBAAuB;EA4B1C,UAAU,eAAe,cAAc;EAMvC;EAIA,mBACH,WAAW,eACX,OAAO,2BACR;EAMa,UAAU,SAAS;;qBCrG1B,eAAY,SAAY,8BAAiC;KCUjE,OAAO;KACP,WAAW,WAAW;KAqEf,iBAAiB,UAAU,SAAS;EAC5C,YAAY,SAAS;EACrB,MAAM,YAAY;;;;;;;;qBAST,UAAU,UAAU,SAAS;;EAKtC,YAAY,SAAS,iBAAiB;EAKzB,mBAAmB;IAC5B;IACA;IACA,YAAY;IACZ,SAAS;MACT,QAAQ;IAAsB;MAAU;;IAA0B;;EA0CzD,sBAAsB;IAC/B,OAAO;IACP;IACA;IACA,UAAU;MACV,QAAQ;IAAgB;MAAU;;IAA0B;;EA+HnD,MAAM,SAAS,WAAW,QAAQ;EAIlC,WAAW;EAMX,mBAAmB,QAAQ,SAAS,4BAA4B;EAItE,mBAAmB,QAAQ,SAAS;EAI9B,iBAAiB;IAAU;IAAoB;;EAI/C,kBAAkB;IAC3B,OAAO;IACP;MAAS;MAA6B;;MACtC;IAAU;IAAoB;IAAsB;;EAIjD,YAAY;EAMN,QAAQ;IACjB;MAAS;MAAe;;IACxB,UAAU;IACV,SAAS;MACT,QAAQ;IAAgB;MAAU;;IAA0B;;EA2DnD,YAAY,SAAS,WAAW,yBAAyB;IAClE,QAAQ;IACR;MACI;MACA;QAAS;;MACT;MACA;MACA;;;EAiBD,0BAA0B;EAIpB,cAAc;IACvB;IACA;IACA;IACA;IACA;MACA,QAAQ;EAIL,aAAa"}
package/dist/index.mjs ADDED
@@ -0,0 +1,446 @@
1
+ import { AbstractPlatform, AbstractWebSocket, CommitGate, deferredResultPromise, isDeferredResult, isErrorMessage, isSuspended } from "@pluv/io";
2
+ import { PersistenceCloudflareTransactionalStorage } from "@pluv/persistence-cloudflare-transactional-storage";
3
+ // src/PluvWebSocket.ts
4
+ var PluvWebSocket = class extends AbstractWebSocket {
5
+ _readyState = this.OPEN;
6
+ _sessionId = null;
7
+ _state;
8
+ _user = null;
9
+ set presence(presence) {
10
+ this._state.presence = presence;
11
+ }
12
+ get readyState() {
13
+ return this._readyState;
14
+ }
15
+ get session() {
16
+ const user = this._user;
17
+ if (!user) throw new Error("WebSocket is not authorized");
18
+ return {
19
+ ...this._state,
20
+ id: this.sessionId,
21
+ user,
22
+ webSocket: this
23
+ };
24
+ }
25
+ get sessionId() {
26
+ if (this._sessionId) return this._sessionId;
27
+ this._sessionId = this.webSocket.id;
28
+ return this._sessionId;
29
+ }
30
+ get state() {
31
+ return this._state;
32
+ }
33
+ set state(state) {
34
+ this._state = state;
35
+ }
36
+ get user() {
37
+ return this._user;
38
+ }
39
+ set user(user) {
40
+ this._user = user;
41
+ }
42
+ constructor(webSocket, config) {
43
+ super(webSocket, config);
44
+ this._state = {
45
+ kind: "user",
46
+ operator: null,
47
+ presence: null,
48
+ quit: false,
49
+ room: config.room,
50
+ seq: { presence: null },
51
+ timers: { ping: webSocket.lastPing ?? Date.now() }
52
+ };
53
+ }
54
+ addEventListener(_type, _handler) {}
55
+ close(_code, _reason) {
56
+ throw new Error("The room facet does not accept sockets.");
57
+ }
58
+ send(_message) {
59
+ throw new Error("The room facet does not accept sockets.");
60
+ }
61
+ terminate() {
62
+ throw new Error("The room facet does not accept sockets.");
63
+ }
64
+ };
65
+ // src/PluvPlatform.ts
66
+ const hasSql = (state) => !!state?.storage?.sql;
67
+ var PluvPlatform = class PluvPlatform extends AbstractPlatform {
68
+ id = crypto.randomUUID();
69
+ _name = "platformPluv";
70
+ _persistenceProvided;
71
+ constructor(config = {}) {
72
+ super(config);
73
+ this._persistenceProvided = !!config.persistence;
74
+ }
75
+ acceptWebSocket(_webSocket) {
76
+ return Promise.reject(/* @__PURE__ */ new Error("The room facet does not accept sockets."));
77
+ }
78
+ convertWebSocket(webSocket, config) {
79
+ return new PluvWebSocket(webSocket, {
80
+ persistence: this.persistence,
81
+ platform: this,
82
+ room: config.room
83
+ });
84
+ }
85
+ getLastPing(webSocket) {
86
+ return webSocket.webSocket.lastPing;
87
+ }
88
+ getSerializedState(_webSocket) {
89
+ return null;
90
+ }
91
+ getSessionId(webSocket) {
92
+ return webSocket.id;
93
+ }
94
+ getWebSockets() {
95
+ return this._roomContext?.sessions ?? [];
96
+ }
97
+ initialize(config) {
98
+ const provided = config.roomContext;
99
+ const roomContext = {
100
+ env: provided?.env ?? {},
101
+ ...provided?.sessions ? { sessions: provided.sessions } : {},
102
+ state: provided?.state ?? {}
103
+ };
104
+ if (!this._persistenceProvided && !hasSql(roomContext.state)) throw new Error("The room facet requires SQLite storage or a persistence adapter.");
105
+ if (!this._persistenceProvided && typeof roomContext.state?.waitUntil !== "function") throw new Error("The room facet requires waitUntil when it uses SQLite storage.");
106
+ const persistence = (this._persistenceProvided ? this.persistence : new PersistenceCloudflareTransactionalStorage()).initialize(roomContext);
107
+ return new PluvPlatform({
108
+ persistence,
109
+ pubSub: this.pubSub,
110
+ roomContext
111
+ })._initialize();
112
+ }
113
+ parseData(data) {
114
+ if (typeof data === "string") return JSON.parse(data);
115
+ return JSON.parse(new TextDecoder("utf8").decode(data));
116
+ }
117
+ randomUUID() {
118
+ return crypto.randomUUID();
119
+ }
120
+ setSerializedState(webSocket, state) {
121
+ webSocket.state = state;
122
+ return state;
123
+ }
124
+ keepAlive(promise) {
125
+ super.keepAlive(promise);
126
+ this._roomContext?.state?.waitUntil?.(promise);
127
+ }
128
+ };
129
+ // src/platformPluv.ts
130
+ const platformPluv = (config = {}) => {
131
+ return () => new PluvPlatform(config);
132
+ };
133
+ // src/plain.ts
134
+ const plain = {
135
+ error: (error) => {
136
+ if (!error) return null;
137
+ if (isErrorMessage(error)) return { message: error.message };
138
+ return { message: "Unexpected error" };
139
+ },
140
+ json: (value) => JSON.parse(JSON.stringify(value)),
141
+ effects: (effects) => {
142
+ return plain.json({
143
+ attachments: effects.attachments,
144
+ broadcast: effects.broadcast,
145
+ closes: effects.closes,
146
+ connectionId: effects.connectionId,
147
+ destroy: effects.destroy,
148
+ error: plain.error(effects.error),
149
+ self: effects.self,
150
+ storageDirty: effects.storageDirty
151
+ });
152
+ }
153
+ };
154
+ // src/RoomFacet.ts
155
+ /** Broadcast, self, and `$error` still need a send. Storage stays on the pending list. */
156
+ const procedureReplay = (effects) => {
157
+ const broadcast = effects.broadcast.filter((frame) => frame.type !== "$storageUpdated");
158
+ if (!broadcast.length && !effects.self.length && !effects.error) return null;
159
+ return {
160
+ ...effects,
161
+ broadcast,
162
+ storageDirty: false
163
+ };
164
+ };
165
+ const retain = (target) => {
166
+ const source = target;
167
+ if (typeof source.dup === "function") return source.dup();
168
+ return source;
169
+ };
170
+ const yieldTurn = () => {
171
+ if (typeof scheduler !== "undefined") return scheduler.wait(0);
172
+ return new Promise((resolve) => {
173
+ setTimeout(resolve, 0);
174
+ });
175
+ };
176
+ const readSettled = async (deferredResult) => {
177
+ let settled = { status: "pending" };
178
+ deferredResult.then((value) => {
179
+ settled = {
180
+ status: "value",
181
+ value
182
+ };
183
+ }, (error) => {
184
+ settled = {
185
+ status: "error",
186
+ error
187
+ };
188
+ });
189
+ await yieldTurn();
190
+ return settled;
191
+ };
192
+ /**
193
+ * Forwards one room call and returns plain JSON. A deferred result that is still
194
+ * waiting becomes `{ suspended: true }`. The frames are delivered later on
195
+ * the `RpcTarget` the caller passed in, which this facet retains until that
196
+ * send finishes.
197
+ */
198
+ var RoomFacet = class {
199
+ #keepAlive;
200
+ #room;
201
+ #storageTurn = new CommitGate();
202
+ constructor(options) {
203
+ this.#keepAlive = options.keepAlive;
204
+ this.#room = options.room;
205
+ }
206
+ async applyPresencePatch(params) {
207
+ const settled = await this.#room["~"].applyPresencePatch({
208
+ data: params.data,
209
+ name: params.name,
210
+ session: params.session
211
+ });
212
+ if (!isDeferredResult(settled)) return plain.json(settled);
213
+ const deferredResult = deferredResultPromise(settled);
214
+ const ready = await readSettled(deferredResult);
215
+ if (ready.status === "value") return plain.json(ready.value);
216
+ if (ready.status === "error") return {
217
+ error: plain.error(ready.error),
218
+ suspended: true
219
+ };
220
+ this.#deliver({
221
+ onReject: (sink, error) => {
222
+ return sink.deliverPresence({
223
+ error: plain.error(error),
224
+ name: params.name,
225
+ patch: null,
226
+ senderId: params.session.id
227
+ });
228
+ },
229
+ send: (sink, patch) => {
230
+ return sink.deliverPresence({
231
+ error: null,
232
+ name: params.name,
233
+ patch: plain.json(patch),
234
+ senderId: params.session.id
235
+ });
236
+ },
237
+ sink: params.onPresence,
238
+ deferredResult
239
+ });
240
+ return { suspended: true };
241
+ }
242
+ async applyStorageProcedure(params) {
243
+ const settled = await this.#room["~"].applyStorageProcedure({
244
+ actor: params.actor,
245
+ data: params.data,
246
+ name: params.name
247
+ });
248
+ if (!isDeferredResult(settled)) return plain.effects(settled);
249
+ const pending = deferredResultPromise(settled);
250
+ const ready = await readSettled(pending);
251
+ if (ready.status === "error") throw ready.error;
252
+ if (ready.status === "value") {
253
+ const value = ready.value;
254
+ if ("broadcast" in value) return plain.effects(value);
255
+ if (this.#storageTurn.idle) {
256
+ const turn = this.#storageTurn.reserve();
257
+ try {
258
+ return plain.effects(await this.#room["~"].commitLoadedStorage(value));
259
+ } finally {
260
+ turn.finish();
261
+ }
262
+ }
263
+ }
264
+ const turn = this.#storageTurn.reserve();
265
+ const loaded = ready.status === "value" ? Promise.resolve(ready.value) : pending;
266
+ this.#deliver({
267
+ onReject: async (sink, error) => {
268
+ try {
269
+ await sink.deliverFrames({
270
+ attachments: [],
271
+ broadcast: [],
272
+ closes: [],
273
+ connectionId: params.actor.id,
274
+ destroy: false,
275
+ error: null,
276
+ self: [],
277
+ storageDirty: false
278
+ });
279
+ } finally {
280
+ turn.finish();
281
+ await sink.finish(plain.error(error));
282
+ }
283
+ },
284
+ send: async (sink, value) => {
285
+ try {
286
+ let finished = false;
287
+ const finish = async (error) => {
288
+ if (finished) return;
289
+ finished = true;
290
+ await sink.finish(error ? plain.error(error) : null);
291
+ };
292
+ try {
293
+ const effects = "broadcast" in value ? value : await this.#room["~"].commitLoadedStorage(value, false);
294
+ const delivered = plain.effects(effects);
295
+ try {
296
+ await sink.deliverFrames(delivered);
297
+ } catch (error) {
298
+ try {
299
+ const replay = procedureReplay(delivered);
300
+ if (replay) await sink.retryUnsent(replay);
301
+ } finally {
302
+ this.#room["~"].queueStorageFrames(delivered.broadcast);
303
+ if (delivered.storageDirty) await sink.noteStorageDirty();
304
+ }
305
+ throw error;
306
+ }
307
+ await this.#room["~"].dropStorageUpdates(delivered.broadcast);
308
+ await finish(null);
309
+ } catch (error) {
310
+ console.error(error);
311
+ try {
312
+ if (await this.#room["~"].storageDirty()) await sink.noteStorageDirty();
313
+ } finally {
314
+ await finish(error);
315
+ }
316
+ }
317
+ } finally {
318
+ turn.finish();
319
+ }
320
+ },
321
+ sink: params.onFrames,
322
+ deferredResult: turn.previous.then(() => loaded, () => loaded)
323
+ });
324
+ return { suspended: true };
325
+ }
326
+ async close(session) {
327
+ return plain.effects(await this.#room["~"].close(session));
328
+ }
329
+ async destroy() {
330
+ await this.#room["~"].destroy();
331
+ return null;
332
+ }
333
+ async dropStorageUpdates(frames) {
334
+ await this.#room["~"].dropStorageUpdates(frames);
335
+ }
336
+ queueStorageFrames(frames) {
337
+ this.#room["~"].queueStorageFrames(frames);
338
+ }
339
+ async flushSnapshot() {
340
+ return plain.json(await this.#room["~"].flushSnapshot());
341
+ }
342
+ async initializeStorage(params) {
343
+ return plain.json(await this.#room["~"].initializeStorage(params));
344
+ }
345
+ logDebug(...data) {
346
+ this.#room["~"].logDebug(...data);
347
+ return null;
348
+ }
349
+ async message(params) {
350
+ const settled = await this.#room["~"].message(params.session, params.event);
351
+ if (isDeferredResult(settled)) {
352
+ const deferredResult = deferredResultPromise(settled);
353
+ const ready = await readSettled(deferredResult);
354
+ if (ready.status === "value") return plain.effects(ready.value);
355
+ if (ready.status === "error") return {
356
+ error: plain.error(ready.error),
357
+ suspended: true
358
+ };
359
+ this.#deliver({
360
+ onReject: (sink, error) => {
361
+ return sink.deliverFrames(plain.effects({
362
+ attachments: [],
363
+ broadcast: [],
364
+ closes: [],
365
+ connectionId: params.session.id,
366
+ destroy: false,
367
+ error,
368
+ self: [],
369
+ storageDirty: false
370
+ }));
371
+ },
372
+ send: async (sink, effects) => {
373
+ const delivered = plain.effects(effects);
374
+ try {
375
+ await sink.deliverFrames(delivered);
376
+ } catch (error) {
377
+ try {
378
+ const replay = procedureReplay(delivered);
379
+ if (replay) await sink.retryUnsent(replay);
380
+ } finally {
381
+ if (delivered.storageDirty) await sink.noteStorageDirty();
382
+ }
383
+ throw error;
384
+ }
385
+ await this.#room["~"].dropStorageUpdates(delivered.broadcast);
386
+ },
387
+ sink: params.onFrames,
388
+ deferredResult
389
+ });
390
+ return { suspended: true };
391
+ }
392
+ if (isSuspended(settled)) return settled;
393
+ return plain.effects(settled);
394
+ }
395
+ async register(...options) {
396
+ const result = await this.#room["~"].register(...options);
397
+ return {
398
+ limits: plain.json(this.#room.limits),
399
+ result: plain.json({
400
+ encodedState: result.encodedState,
401
+ error: plain.error(result.error),
402
+ kind: result.kind,
403
+ operator: result.operator,
404
+ user: result.user
405
+ })
406
+ };
407
+ }
408
+ storageDirty() {
409
+ return this.#room["~"].storageDirty();
410
+ }
411
+ async userConnected(params) {
412
+ return plain.effects(await this.#room["~"].userConnected(params));
413
+ }
414
+ whenReady() {
415
+ return this.#room["~"].whenReady();
416
+ }
417
+ #deliver(params) {
418
+ const { deferredResult, onReject, send, sink } = params;
419
+ const kept = retain(sink);
420
+ const delivery = deferredResult.then(async (value) => {
421
+ await yieldTurn();
422
+ try {
423
+ await send(kept, value);
424
+ } catch (error) {
425
+ console.error(error);
426
+ }
427
+ }, async (error) => {
428
+ await yieldTurn();
429
+ if (!onReject) {
430
+ console.error(error);
431
+ return;
432
+ }
433
+ try {
434
+ await onReject(kept, error);
435
+ } catch (reportError) {
436
+ console.error(reportError);
437
+ }
438
+ }).finally(() => {
439
+ kept[Symbol.dispose]?.();
440
+ });
441
+ this.#keepAlive(delivery);
442
+ }
443
+ };
444
+ export { PluvPlatform, RoomFacet, platformPluv };
445
+
446
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/PluvWebSocket.ts","../src/PluvPlatform.ts","../src/platformPluv.ts","../src/plain.ts","../src/RoomFacet.ts"],"sourcesContent":["import type {\n AbstractEventMap,\n AbstractListener,\n AbstractWebSocketConfig,\n WebSocketSerializedState,\n WebSocketSession,\n} from \"@pluv/io\";\nimport { AbstractWebSocket } from \"@pluv/io\";\nimport type { BaseUser, JsonObject } from \"@pluv/types\";\nimport type { PluvSocket } from \"./types\";\n\nexport class PluvWebSocket extends AbstractWebSocket<PluvSocket> {\n private _readyState: 0 | 1 | 2 | 3 = this.OPEN;\n private _sessionId: string | null = null;\n private _state: WebSocketSerializedState;\n private _user: BaseUser | null = null;\n\n public set presence(presence: JsonObject | null) {\n this._state.presence = presence;\n }\n\n public get readyState(): 0 | 1 | 2 | 3 {\n return this._readyState;\n }\n\n public get session(): WebSocketSession<any> {\n const user = this._user;\n\n if (!user) throw new Error(\"WebSocket is not authorized\");\n\n return {\n ...this._state,\n id: this.sessionId,\n user,\n webSocket: this,\n };\n }\n\n public get sessionId(): string {\n if (this._sessionId) return this._sessionId;\n\n this._sessionId = this.webSocket.id;\n\n return this._sessionId;\n }\n\n public get state(): WebSocketSerializedState {\n return this._state;\n }\n\n public set state(state: WebSocketSerializedState) {\n this._state = state;\n }\n\n public get user(): BaseUser | null {\n return this._user;\n }\n\n public set user(user: BaseUser) {\n this._user = user;\n }\n\n constructor(webSocket: PluvSocket, config: AbstractWebSocketConfig) {\n super(webSocket, config);\n\n this._state = {\n kind: \"user\",\n operator: null,\n presence: null,\n quit: false,\n room: config.room,\n seq: { presence: null },\n timers: { ping: webSocket.lastPing ?? Date.now() },\n };\n }\n\n public addEventListener<TType extends keyof AbstractEventMap>(\n _type: TType,\n _handler: AbstractListener<TType>,\n ): void {}\n\n public close(_code?: number, _reason?: string): void {\n throw new Error(\"The room facet does not accept sockets.\");\n }\n\n public send(_message: ArrayBuffer | ArrayBufferView | string): void {\n throw new Error(\"The room facet does not accept sockets.\");\n }\n\n public terminate(): void {\n throw new Error(\"The room facet does not accept sockets.\");\n }\n}\n","import type {\n AbstractPersistence,\n AbstractPlatformConfig,\n ConvertWebSocketConfig,\n WebSocketSerializedState,\n} from \"@pluv/io\";\nimport { AbstractPlatform } from \"@pluv/io\";\nimport { PersistenceCloudflareTransactionalStorage } from \"@pluv/persistence-cloudflare-transactional-storage\";\nimport { PluvWebSocket } from \"./PluvWebSocket\";\nimport type { PluvRoomContext, PluvRoomState, PluvSocket } from \"./types\";\n\nexport type PluvPlatformConfig = AbstractPlatformConfig<PluvRoomContext> & {\n persistence?: AbstractPersistence;\n};\n\nconst hasSql = (state: PluvRoomState | undefined): boolean => !!state?.storage?.sql;\n\nexport class PluvPlatform extends AbstractPlatform<PluvWebSocket, {}, PluvRoomContext> {\n public readonly id = crypto.randomUUID();\n public readonly _name = \"platformPluv\";\n\n private readonly _persistenceProvided: boolean;\n\n constructor(config: PluvPlatformConfig = {}) {\n super(config);\n\n this._persistenceProvided = !!config.persistence;\n }\n\n public acceptWebSocket(_webSocket: PluvWebSocket): Promise<void> {\n return Promise.reject(new Error(\"The room facet does not accept sockets.\"));\n }\n\n public convertWebSocket(webSocket: PluvSocket, config: ConvertWebSocketConfig): PluvWebSocket {\n return new PluvWebSocket(webSocket, {\n persistence: this.persistence,\n platform: this,\n room: config.room,\n });\n }\n\n public getLastPing(webSocket: PluvWebSocket): number | null {\n return webSocket.webSocket.lastPing;\n }\n\n public getSerializedState(_webSocket: PluvSocket): WebSocketSerializedState | null {\n return null;\n }\n\n public getSessionId(webSocket: PluvSocket): string | null {\n return webSocket.id;\n }\n\n public getWebSockets(): readonly PluvSocket[] {\n return this._roomContext?.sessions ?? [];\n }\n\n public initialize(config: AbstractPlatformConfig<PluvRoomContext>): this {\n const provided = config.roomContext;\n const roomContext: PluvRoomContext = {\n env: provided?.env ?? {},\n ...(provided?.sessions ? { sessions: provided.sessions } : {}),\n state: provided?.state ?? {},\n };\n if (!this._persistenceProvided && !hasSql(roomContext.state)) {\n throw new Error(\"The room facet requires SQLite storage or a persistence adapter.\");\n }\n\n if (!this._persistenceProvided && typeof roomContext.state?.waitUntil !== \"function\") {\n throw new Error(\"The room facet requires waitUntil when it uses SQLite storage.\");\n }\n\n const persistence = (\n this._persistenceProvided\n ? this.persistence\n : new PersistenceCloudflareTransactionalStorage()\n ).initialize(roomContext);\n\n return new PluvPlatform({\n persistence,\n pubSub: this.pubSub,\n roomContext,\n })._initialize() as this;\n }\n\n public parseData(data: string | ArrayBuffer): Record<string, any> {\n if (typeof data === \"string\") return JSON.parse(data);\n\n return JSON.parse(new TextDecoder(\"utf8\").decode(data));\n }\n\n public randomUUID(): string {\n return crypto.randomUUID();\n }\n\n public setSerializedState(\n webSocket: PluvWebSocket,\n state: WebSocketSerializedState,\n ): WebSocketSerializedState {\n webSocket.state = state;\n\n return state;\n }\n\n public override keepAlive(promise: Promise<unknown>): void {\n super.keepAlive(promise);\n this._roomContext?.state?.waitUntil?.(promise);\n }\n}\n","import type { PluvPlatformConfig } from \"./PluvPlatform\";\nimport { PluvPlatform } from \"./PluvPlatform\";\n\nexport const platformPluv = (config: PluvPlatformConfig = {}): (() => PluvPlatform) => {\n return () => new PluvPlatform(config);\n};\n","import { isErrorMessage, type RoomEffects } from \"@pluv/io\";\nimport type { PlainError } from \"./types\";\n\nexport const plain = {\n error: (error: unknown): PlainError => {\n if (!error) return null;\n\n if (isErrorMessage(error)) return { message: error.message };\n\n return { message: \"Unexpected error\" };\n },\n json: <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T,\n effects: (effects: RoomEffects): RoomEffects => {\n return plain.json({\n attachments: effects.attachments,\n broadcast: effects.broadcast,\n closes: effects.closes,\n connectionId: effects.connectionId,\n destroy: effects.destroy,\n error: plain.error(effects.error),\n self: effects.self,\n storageDirty: effects.storageDirty,\n });\n },\n};\n","import type {\n IODefs,\n RoomEffects,\n RoomJoinActor,\n RoomPresenceSession,\n RoomStorageActor,\n SessionRoom,\n} from \"@pluv/io\";\nimport { CommitGate, deferredResultPromise, isDeferredResult, isSuspended } from \"@pluv/io\";\nimport type { JsonObject } from \"@pluv/types\";\nimport { plain } from \"./plain\";\nimport type { FrameDelivery, PresenceDelivery, StorageFrameDelivery } from \"./types\";\n\ntype Room = SessionRoom<any>;\ntype Snapshot = Parameters<Room[\"~\"][\"close\"]>[0];\n\n/** Broadcast, self, and `$error` still need a send. Storage stays on the pending list. */\nconst procedureReplay = (effects: RoomEffects): RoomEffects | null => {\n const broadcast = effects.broadcast.filter((frame) => frame.type !== \"$storageUpdated\");\n\n if (!broadcast.length && !effects.self.length && !effects.error) return null;\n\n return {\n ...effects,\n broadcast,\n storageDirty: false,\n };\n};\n\n// A stub passed into an RPC is disposed when the call returns. `dup()` keeps a copy.\n// https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#the-dup-method\ntype Retained<T extends object> = T & {\n dup?: () => Retained<T>;\n [Symbol.dispose]?: () => void;\n};\n\nconst retain = <T extends object>(target: T): Retained<T> => {\n const source = target as Retained<T>;\n\n if (typeof source.dup === \"function\") return source.dup();\n\n return source;\n};\n\n// A turn that settles after this RPC returns still calls back into the caller.\n// Yield once so that call is not waiting on this one.\n// https://developers.cloudflare.com/workers/runtime-apis/scheduler/\nconst yieldTurn = (): Promise<void> => {\n if (typeof scheduler !== \"undefined\") return scheduler.wait(0);\n\n return new Promise((resolve) => {\n setTimeout(resolve, 0);\n });\n};\n\n// An already-settled turn belongs in this RPC result. Delivering it through the\n// caller joins the queue that is still waiting here.\nconst readSettled = async <T>(\n deferredResult: Promise<T>,\n): Promise<\n { status: \"value\"; value: T } | { status: \"error\"; error: unknown } | { status: \"pending\" }\n> => {\n let settled:\n | { status: \"value\"; value: T }\n | { status: \"error\"; error: unknown }\n | { status: \"pending\" } = { status: \"pending\" };\n\n deferredResult.then(\n (value) => {\n settled = { status: \"value\", value };\n },\n (error: unknown) => {\n settled = { status: \"error\", error };\n },\n );\n\n // Microtasks already queued, including a fast async resolver, finish here.\n // A turn still waiting on I/O stays pending.\n await yieldTurn();\n\n return settled;\n};\n\nexport type RoomFacetOptions<T extends IODefs = IODefs> = {\n keepAlive: (promise: Promise<unknown>) => void;\n room: SessionRoom<T>;\n};\n\n/**\n * Forwards one room call and returns plain JSON. A deferred result that is still\n * waiting becomes `{ suspended: true }`. The frames are delivered later on\n * the `RpcTarget` the caller passed in, which this facet retains until that\n * send finishes.\n */\nexport class RoomFacet<T extends IODefs = IODefs> {\n readonly #keepAlive: (promise: Promise<unknown>) => void;\n readonly #room: Room;\n readonly #storageTurn = new CommitGate();\n\n constructor(options: RoomFacetOptions<T>) {\n this.#keepAlive = options.keepAlive;\n this.#room = options.room;\n }\n\n public async applyPresencePatch(params: {\n data: unknown;\n name: string;\n onPresence: PresenceDelivery;\n session: RoomPresenceSession;\n }): Promise<JsonObject | null | { error?: { message: string } | null; suspended: true }> {\n const settled = await this.#room[\"~\"].applyPresencePatch({\n data: params.data,\n name: params.name,\n session: params.session,\n });\n\n if (!isDeferredResult(settled)) return plain.json(settled);\n\n const deferredResult = deferredResultPromise(settled);\n const ready = await readSettled(deferredResult);\n\n if (ready.status === \"value\") return plain.json(ready.value);\n\n if (ready.status === \"error\") {\n return { error: plain.error(ready.error), suspended: true };\n }\n\n this.#deliver({\n onReject: (sink, error) => {\n return sink.deliverPresence({\n error: plain.error(error),\n name: params.name,\n patch: null,\n senderId: params.session.id,\n });\n },\n send: (sink, patch) => {\n return sink.deliverPresence({\n error: null,\n name: params.name,\n patch: plain.json(patch),\n senderId: params.session.id,\n });\n },\n sink: params.onPresence,\n deferredResult,\n });\n\n return { suspended: true };\n }\n\n public async applyStorageProcedure(params: {\n actor: RoomStorageActor;\n data: unknown;\n name: string;\n onFrames: StorageFrameDelivery;\n }): Promise<RoomEffects | { error?: { message: string } | null; suspended: true }> {\n const settled = await this.#room[\"~\"].applyStorageProcedure({\n actor: params.actor,\n data: params.data,\n name: params.name,\n });\n\n if (!isDeferredResult(settled)) return plain.effects(settled);\n\n const pending = deferredResultPromise(settled);\n const ready = await readSettled(pending);\n\n // Already settled, so this RPC can reject the host call. `{ suspended: true }`\n // would wait for a `finish` that never runs.\n if (ready.status === \"error\") throw ready.error;\n\n // These frames are already the commit. A loaded procedure is the other value.\n if (ready.status === \"value\") {\n const value = ready.value;\n\n if (\"broadcast\" in value) return plain.effects(value);\n\n // The earlier commit has finished, and this load has too. Commit while\n // the caller's queue is still held.\n if (this.#storageTurn.idle) {\n const turn = this.#storageTurn.reserve();\n\n try {\n return plain.effects(await this.#room[\"~\"].commitLoadedStorage(value));\n } finally {\n turn.finish();\n }\n }\n }\n\n const turn = this.#storageTurn.reserve();\n const loaded = ready.status === \"value\" ? Promise.resolve(ready.value) : pending;\n\n // `{ suspended: true }` leaves the host call open until `finish`.\n // Waiting for the previous commit here would hold the caller's queue.\n // `send` writes after that turn, off this RPC.\n this.#deliver({\n onReject: async (sink, error) => {\n // The host call rejects with this error. The caller logs it.\n try {\n await sink.deliverFrames({\n attachments: [],\n broadcast: [],\n closes: [],\n connectionId: params.actor.id,\n destroy: false,\n error: null,\n self: [],\n storageDirty: false,\n });\n } finally {\n turn.finish();\n await sink.finish(plain.error(error));\n }\n },\n send: async (sink, value) => {\n try {\n let finished = false;\n\n // `null` resolves the host call with empty effects. The frames\n // were already sent. An error rejects the call.\n const finish = async (error: unknown): Promise<void> => {\n if (finished) return;\n\n finished = true;\n await sink.finish(error ? plain.error(error) : null);\n };\n\n try {\n // `false` runs off the queue, so this write does not pull\n // updates another turn is writing.\n const effects =\n \"broadcast\" in value\n ? value\n : await this.#room[\"~\"].commitLoadedStorage(value, false);\n const delivered = plain.effects(effects);\n\n try {\n await sink.deliverFrames(delivered);\n } catch (error) {\n try {\n const replay = procedureReplay(delivered);\n\n if (replay) await sink.retryUnsent(replay);\n } finally {\n // The successful send never queued this frame. A failed one\n // still needs the snapshot loop to broadcast it.\n this.#room[\"~\"].queueStorageFrames(delivered.broadcast);\n\n if (delivered.storageDirty) await sink.noteStorageDirty();\n }\n\n // The write has already landed, and the snapshot loop still broadcasts it.\n // The host call rejects anyway. Falling through would `finish(null)`.\n throw error;\n }\n\n await this.#room[\"~\"].dropStorageUpdates(delivered.broadcast);\n await finish(null);\n } catch (error) {\n console.error(error);\n\n try {\n if (await this.#room[\"~\"].storageDirty()) await sink.noteStorageDirty();\n } finally {\n await finish(error);\n }\n }\n } finally {\n turn.finish();\n }\n },\n sink: params.onFrames,\n deferredResult: turn.previous.then(\n () => loaded,\n () => loaded,\n ),\n });\n\n return { suspended: true };\n }\n\n public async close(session: Snapshot): Promise<RoomEffects> {\n return plain.effects(await this.#room[\"~\"].close(session));\n }\n\n public async destroy(): Promise<null> {\n await this.#room[\"~\"].destroy();\n\n return null;\n }\n\n public async dropStorageUpdates(frames: Readonly<RoomEffects[\"broadcast\"]>): Promise<void> {\n await this.#room[\"~\"].dropStorageUpdates(frames);\n }\n\n public queueStorageFrames(frames: Readonly<RoomEffects[\"broadcast\"]>): void {\n this.#room[\"~\"].queueStorageFrames(frames);\n }\n\n public async flushSnapshot(): Promise<{ broadcast: unknown; status: unknown }> {\n return plain.json(await this.#room[\"~\"].flushSnapshot());\n }\n\n public async initializeStorage(params: {\n actor: RoomJoinActor;\n input: { stateVector?: string | null; update?: string | null };\n }): Promise<{ changeKind: string; state: string | null; storageDirty: boolean }> {\n return plain.json(await this.#room[\"~\"].initializeStorage(params));\n }\n\n public logDebug(...data: unknown[]): null {\n this.#room[\"~\"].logDebug(...data);\n\n return null;\n }\n\n public async message(params: {\n event: { data: unknown; type: string };\n onFrames: FrameDelivery;\n session: Snapshot;\n }): Promise<RoomEffects | { error?: { message: string } | null; suspended: true }> {\n const settled = await this.#room[\"~\"].message(params.session, params.event);\n\n if (isDeferredResult(settled)) {\n const deferredResult = deferredResultPromise(settled);\n const ready = await readSettled(deferredResult);\n\n if (ready.status === \"value\") return plain.effects(ready.value);\n\n if (ready.status === \"error\") {\n return { error: plain.error(ready.error), suspended: true };\n }\n\n this.#deliver({\n onReject: (sink, error) => {\n return sink.deliverFrames(\n plain.effects({\n attachments: [],\n broadcast: [],\n closes: [],\n connectionId: params.session.id,\n destroy: false,\n error,\n self: [],\n storageDirty: false,\n }),\n );\n },\n send: async (sink, effects) => {\n const delivered = plain.effects(effects);\n\n try {\n await sink.deliverFrames(delivered);\n } catch (error) {\n try {\n const replay = procedureReplay(delivered);\n\n if (replay) await sink.retryUnsent(replay);\n } finally {\n if (delivered.storageDirty) await sink.noteStorageDirty();\n }\n\n throw error;\n }\n\n await this.#room[\"~\"].dropStorageUpdates(delivered.broadcast);\n },\n sink: params.onFrames,\n deferredResult,\n });\n\n return { suspended: true };\n }\n\n if (isSuspended(settled)) return settled;\n\n return plain.effects(settled);\n }\n\n public async register(...options: Parameters<Room[\"~\"][\"register\"]>): Promise<{\n limits: Room[\"limits\"];\n result: {\n encodedState: string | null;\n error: { message: string } | null;\n kind: string | null;\n operator: unknown;\n user: unknown;\n };\n }> {\n const result = await this.#room[\"~\"].register(...options);\n\n return {\n limits: plain.json(this.#room.limits),\n result: plain.json({\n encodedState: result.encodedState,\n error: plain.error(result.error),\n kind: result.kind,\n operator: result.operator,\n user: result.user,\n }),\n };\n }\n\n public storageDirty(): boolean | Promise<boolean> {\n return this.#room[\"~\"].storageDirty();\n }\n\n public async userConnected(params: {\n encodedState: string | null;\n id: string;\n kind: string;\n operator: unknown;\n user: unknown;\n }): Promise<RoomEffects> {\n return plain.effects(await this.#room[\"~\"].userConnected(params as never));\n }\n\n public whenReady(): Promise<boolean> {\n return this.#room[\"~\"].whenReady();\n }\n\n #deliver<TValue, TSink extends object>(params: {\n /** Sends `$error` when the deferred result rejects. */\n onReject?: (kept: TSink, error: unknown) => void | Promise<void>;\n /**\n * Sends the resolved value. A throw is logged here. Storage finishes\n * the host call in `send` and does not rethrow.\n */\n send: (kept: TSink, value: TValue) => void | Promise<void>;\n /** Supervisor stub passed into this RPC. Retained until delivery finishes. */\n sink: TSink;\n /** Resolves to frames or a presence patch. */\n deferredResult: Promise<TValue>;\n }): void {\n const { deferredResult, onReject, send, sink } = params;\n const kept = retain(sink);\n const delivery = deferredResult\n .then(\n async (value) => {\n await yieldTurn();\n\n try {\n await send(kept, value);\n } catch (error) {\n console.error(error);\n }\n },\n async (error: unknown) => {\n await yieldTurn();\n\n if (!onReject) {\n console.error(error);\n\n return;\n }\n\n try {\n await onReject(kept, error);\n } catch (reportError) {\n console.error(reportError);\n }\n },\n )\n .finally(() => {\n kept[Symbol.dispose]?.();\n });\n\n this.#keepAlive(delivery);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ export type WorkerCode = {
2
+ compatibilityDate: string;
3
+ /** `null` keeps the uploaded module off the parent's fetch. */
4
+ globalOutbound?: Fetcher | null;
5
+ mainModule: string;
6
+ modules: Record<string, string>;
7
+ env?: Record<string, unknown>;
8
+ };
9
+ export type WorkerLoader = {
10
+ get: (id: string, getCode: () => WorkerCode) => {
11
+ getDurableObjectClass: (name: string) => unknown;
12
+ };
13
+ };
14
+ export type FacetParent = {
15
+ facets: {
16
+ get: (name: string, getStartupOptions: () => Promise<{
17
+ class: unknown;
18
+ }>) => unknown;
19
+ };
20
+ };
21
+ export type LoadUserFacetOptions = {
22
+ version: string;
23
+ code: () => WorkerCode;
24
+ };
25
+ /**
26
+ * Starts the user room facet on the first call. The id is omitted so the facet
27
+ * inherits the parent Durable Object id. `loader.get` is keyed by the module version.
28
+ */
29
+ export declare const loadUserFacet: (ctx: FacetParent, loader: WorkerLoader, options: LoadUserFacetOptions) => unknown;
30
+ //# sourceMappingURL=loadUserFacet.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadUserFacet.d.mts","names":[],"sources":["../src/loadUserFacet.ts"],"mappings":"A,YAAY;EACR;;EAEA,iBAAiB;EACjB;EACA,SAAS;EACT,MAAM;;YAGE;EACR,MACI,YACA,eAAe;IAEf,wBAAwB;;;YAIpB;EACR;IACI,MAAM,cAAc,yBAAyB;MAAU;;;;YAInD;EACR;EACA,YAAY;;;;;;qBAOH,gBAAa,KACjB,aAAW,QACR,cAAY,SACX"}
@@ -0,0 +1,13 @@
1
+ // src/loadUserFacet.ts
2
+ /**
3
+ * Starts the user room facet on the first call. The id is omitted so the facet
4
+ * inherits the parent Durable Object id. `loader.get` is keyed by the module version.
5
+ */
6
+ const loadUserFacet = (ctx, loader, options) => {
7
+ return ctx.facets.get("user", async () => {
8
+ return { class: loader.get(options.version, options.code).getDurableObjectClass("UserRoomFacet") };
9
+ });
10
+ };
11
+ export { loadUserFacet };
12
+
13
+ //# sourceMappingURL=loadUserFacet.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadUserFacet.mjs","names":[],"sources":["../src/loadUserFacet.ts"],"sourcesContent":["export type WorkerCode = {\n compatibilityDate: string;\n /** `null` keeps the uploaded module off the parent's fetch. */\n globalOutbound?: Fetcher | null;\n mainModule: string;\n modules: Record<string, string>;\n env?: Record<string, unknown>;\n};\n\nexport type WorkerLoader = {\n get: (\n id: string,\n getCode: () => WorkerCode,\n ) => {\n getDurableObjectClass: (name: string) => unknown;\n };\n};\n\nexport type FacetParent = {\n facets: {\n get: (name: string, getStartupOptions: () => Promise<{ class: unknown }>) => unknown;\n };\n};\n\nexport type LoadUserFacetOptions = {\n version: string;\n code: () => WorkerCode;\n};\n\n/**\n * Starts the user room facet on the first call. The id is omitted so the facet\n * inherits the parent Durable Object id. `loader.get` is keyed by the module version.\n */\nexport const loadUserFacet = (\n ctx: FacetParent,\n loader: WorkerLoader,\n options: LoadUserFacetOptions,\n): unknown => {\n return ctx.facets.get(\"user\", async () => {\n const worker = loader.get(options.version, options.code);\n\n return { class: worker.getDurableObjectClass(\"UserRoomFacet\") };\n });\n};\n"],"mappings":";;;;;AAiCA,MAAa,iBACT,KACA,QACA,YACU;CACV,OAAO,IAAI,OAAO,IAAI,QAAQ,YAAY;EAGtC,OAAO,EAAE,OAFM,OAAO,IAAI,QAAQ,SAAS,QAAQ,IAE9B,CAAC,CAAC,sBAAsB,eAAe,EAAE;CAClE,CAAC;AACL"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
- "name": "@pluv/platform",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@pluv/platform",
3
+ "version": "5.2.3",
4
+ "description": "Hosted room facet for @pluv/io",
5
+ "author": {
6
+ "email": "david@pluv.io",
7
+ "name": "i3dly",
8
+ "url": "https://github.com/i3dly"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/pluv-io/pluv",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/pluv-io/pluv.git",
15
+ "directory": "packages/platform"
16
+ },
17
+ "type": "module",
18
+ "types": "./dist/index.d.mts",
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "dev": "tsdown --watch",
28
+ "typecheck": "tsc --noEmit",
29
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
30
+ },
31
+ "dependencies": {
32
+ "@pluv/io": "workspace:^",
33
+ "@pluv/persistence-cloudflare-transactional-storage": "workspace:^",
34
+ "@pluv/types": "workspace:^"
35
+ },
36
+ "devDependencies": {
37
+ "@cloudflare/workers-types": "^5.20261025.1",
38
+ "@pluv/tsconfig": "workspace:^",
39
+ "tsdown": "^0.23.2",
40
+ "typescript": "7.1.0-beta"
41
+ },
42
+ "exports": {
43
+ ".": "./dist/index.mjs",
44
+ "./load-user-facet": "./dist/loadUserFacet.mjs",
45
+ "./*": "./*"
46
+ }
47
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.