@uploadista/event-emitter-durable-object 0.0.8 → 0.0.9
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/dist/index.cjs +1 -1
- package/dist/index.d.cts +122 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +136 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +5 -5
- package/src/do-event-emitter.ts +57 -32
- package/src/durable-object-impl.ts +132 -58
- package/src/event-emitter-durable-object.ts +25 -2
- package/src/index.ts +6 -2
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
let e=require(`@uploadista/core/errors`),t=require(`@uploadista/core/types`),n=require(`effect`),r=require(`cloudflare:workers`);function i({durableObject:t}){function r(e){let n=t.idFromName(e);return t.get(n)}return{emit:(t,i)=>n.Effect.tryPromise({try:async()=>{console.log(`[DO EventEmitter] Emitting to eventKey: ${t}`,i.substring(0,200)),await r(t).emit(i),console.log(`[DO EventEmitter] Successfully emitted to eventKey: ${t}`)},catch:n=>(console.error(`[DO EventEmitter] Failed to emit to eventKey: ${t}`,n),e.UploadistaError.fromCode(`UNKNOWN_ERROR`,{cause:n}))}),subscribe:(t,i)=>n.Effect.tryPromise({try:async()=>{await r(t).subscribe(i)},catch:t=>e.UploadistaError.fromCode(`UNKNOWN_ERROR`,{cause:t})}),unsubscribe:t=>n.Effect.tryPromise({try:async()=>{await r(t).unsubscribe()},catch:t=>e.UploadistaError.fromCode(`UNKNOWN_ERROR`,{cause:t})})}}const a=e=>n.Layer.succeed(t.BaseEventEmitterService,i(e));var o=class extends r.DurableObject{async fetch(e){console.log(`[DO fetch] WebSocket connection request: ${e.url}`);let t=new WebSocketPair,[n,r]=Object.values(t);return this.ctx.acceptWebSocket(r),console.log(`[DO fetch] WebSocket accepted, total connections: ${this.ctx.getWebSockets().length}`),new Response(null,{status:101,webSocket:n})}async emit(e){let t=this.ctx.getWebSockets();console.log(`[DO emit] Broadcasting message to ${t.length} WebSocket(s):`,e.substring(0,200));for(let n of t)try{n.send(e)}catch(e){console.error(`Failed to send message to WebSocket:`,e)}}async subscribe(e){}async unsubscribe(){let e=this.ctx.getWebSockets();for(let t of e)t.close(1e3,`Unsubscribed`)}async webSocketMessage(e,t){console.log(`WebSocket message received: ${t}`)}async webSocketClose(e,t,n,r){if(e.readyState===WebSocket.OPEN){let n=t===1006||t<1e3||t>4999?1e3:t;e.close(n,`Durable Object closing WebSocket`)}}async webSocketError(e,t){console.error(`WebSocket error:`,t),e.readyState===WebSocket.OPEN&&e.close(1011,`WebSocket error occurred`)}};exports.UploadistaDurableObjectImpl=o,exports.durableObjectBaseEventEmitter=i,exports.durableObjectEventEmitter=a;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,20 +1,136 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseEventEmitter, WebSocketConnection } from "@uploadista/core/types";
|
|
2
2
|
import { Layer } from "effect";
|
|
3
3
|
import { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
4
|
+
import { DurableObject } from "cloudflare:workers";
|
|
4
5
|
|
|
5
6
|
//#region src/event-emitter-durable-object.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* RPC interface for event emitter Durable Objects.
|
|
10
|
+
*
|
|
11
|
+
* Defines the methods that can be called on a Durable Object instance via RPC.
|
|
12
|
+
*/
|
|
6
13
|
type EventEmitterDurableObjectBranded<T> = Rpc.DurableObjectBranded & {
|
|
7
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Emit a message to all connected WebSocket clients.
|
|
16
|
+
* @param message - The message to broadcast (typically a JSON string)
|
|
17
|
+
*/
|
|
18
|
+
emit: (message: T) => Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe a WebSocket connection to this DO instance.
|
|
21
|
+
* Note: The actual WebSocket connection is established via fetch()
|
|
22
|
+
* @param connection - WebSocket connection details
|
|
23
|
+
*/
|
|
8
24
|
subscribe: (connection: WebSocketConnection) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Unsubscribe from events by closing all WebSocket connections.
|
|
27
|
+
*/
|
|
9
28
|
unsubscribe: () => Promise<void>;
|
|
10
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Durable Object namespace type for event emitters.
|
|
32
|
+
*
|
|
33
|
+
* @template T - Type of messages (typically string for JSON messages)
|
|
34
|
+
*/
|
|
11
35
|
type EventEmitterDurableObject<T> = DurableObjectNamespace<EventEmitterDurableObjectBranded<T>>;
|
|
12
36
|
//#endregion
|
|
13
37
|
//#region src/do-event-emitter.d.ts
|
|
14
|
-
type
|
|
15
|
-
durableObject: EventEmitterDurableObject<
|
|
38
|
+
type DurableObjectEventEmitterConfig = {
|
|
39
|
+
durableObject: EventEmitterDurableObject<string>;
|
|
16
40
|
};
|
|
17
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Creates a BaseEventEmitter implementation using Cloudflare Durable Objects.
|
|
43
|
+
*
|
|
44
|
+
* This implementation:
|
|
45
|
+
* - Routes events to Durable Object instances by eventKey
|
|
46
|
+
* - Each eventKey gets its own DO instance
|
|
47
|
+
* - WebSocket connections are managed by the DO
|
|
48
|
+
* - No external broadcaster needed - DO is single source of truth
|
|
49
|
+
*
|
|
50
|
+
* @param config - Configuration with Durable Object namespace
|
|
51
|
+
* @returns BaseEventEmitter implementation
|
|
52
|
+
*/
|
|
53
|
+
declare function durableObjectBaseEventEmitter({
|
|
54
|
+
durableObject
|
|
55
|
+
}: DurableObjectEventEmitterConfig): BaseEventEmitter;
|
|
56
|
+
/**
|
|
57
|
+
* Creates a Layer for BaseEventEmitterService using Durable Objects.
|
|
58
|
+
*
|
|
59
|
+
* Use this when creating an Uploadista server with Durable Objects:
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const server = await createUploadistaServer({
|
|
64
|
+
* eventEmitter: durableObjectEventEmitter({
|
|
65
|
+
* durableObject: env.UPLOADISTA_DO,
|
|
66
|
+
* }),
|
|
67
|
+
* // ... other config
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare const durableObjectEventEmitter: (config: DurableObjectEventEmitterConfig) => Layer.Layer<unknown, never, never>;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/durable-object-impl.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* Base Durable Object implementation for Uploadista event emission.
|
|
76
|
+
*
|
|
77
|
+
* This class provides:
|
|
78
|
+
* - Hibernatable WebSocket connections
|
|
79
|
+
* - Event broadcasting to all connected clients
|
|
80
|
+
* - Automatic connection management
|
|
81
|
+
* - RPC methods for emit/subscribe/unsubscribe
|
|
82
|
+
*
|
|
83
|
+
* Extend this class in your Worker to create event emitter Durable Objects:
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* export class UploadistaDurableObject extends UploadistaDurableObjectImpl {}
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare class UploadistaDurableObjectImpl extends DurableObject {
|
|
91
|
+
/**
|
|
92
|
+
* Handles WebSocket upgrade requests.
|
|
93
|
+
* Creates a hibernatable WebSocket connection.
|
|
94
|
+
*/
|
|
95
|
+
fetch(request: Request): Promise<Response>;
|
|
96
|
+
/**
|
|
97
|
+
* RPC method: Emit a message to all connected WebSocket clients.
|
|
98
|
+
*
|
|
99
|
+
* @param message - The message to broadcast (typically a JSON string)
|
|
100
|
+
*/
|
|
101
|
+
emit(message: string): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* RPC method: Subscribe a WebSocket connection.
|
|
104
|
+
*
|
|
105
|
+
* Note: This is called via RPC, the actual WebSocket connection
|
|
106
|
+
* is established via the fetch() handler.
|
|
107
|
+
*/
|
|
108
|
+
subscribe(_connection: WebSocketConnection): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* RPC method: Unsubscribe from events.
|
|
111
|
+
*
|
|
112
|
+
* Closes all WebSocket connections for this DO instance.
|
|
113
|
+
*/
|
|
114
|
+
unsubscribe(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Hibernation API: Handle incoming WebSocket messages.
|
|
117
|
+
*
|
|
118
|
+
* Called automatically when a message arrives on a hibernated WebSocket.
|
|
119
|
+
*/
|
|
120
|
+
webSocketMessage(_ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Hibernation API: Handle WebSocket close events.
|
|
123
|
+
*
|
|
124
|
+
* Called automatically when a WebSocket connection closes.
|
|
125
|
+
*/
|
|
126
|
+
webSocketClose(ws: WebSocket, code: number, _reason: string, _wasClean: boolean): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Hibernation API: Handle WebSocket errors.
|
|
129
|
+
*
|
|
130
|
+
* Called automatically when a WebSocket error occurs.
|
|
131
|
+
*/
|
|
132
|
+
webSocketError(ws: WebSocket, error: unknown): Promise<void>;
|
|
133
|
+
}
|
|
18
134
|
//#endregion
|
|
19
|
-
export { type EventEmitterDurableObject,
|
|
135
|
+
export { type DurableObjectEventEmitterConfig, type EventEmitterDurableObject, UploadistaDurableObjectImpl, durableObjectBaseEventEmitter, durableObjectEventEmitter };
|
|
20
136
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/event-emitter-durable-object.ts","../src/do-event-emitter.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/event-emitter-durable-object.ts","../src/do-event-emitter.ts","../src/durable-object-impl.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAQA;AAAsD,KAA1C,gCAA0C,CAAA,CAAA,CAAA,GAAJ,GAAA,CAAI,oBAAA,GAAA;EAKpC;;;;EAYG,IAAA,EAAA,CAAA,OAAA,EAZH,CAYG,EAAA,GAZG,OAYH,CAAA,IAAA,CAAA;EAAO;AAQ5B;;;;EAAiE,SAAA,EAAA,CAAA,UAAA,EAbvC,mBAauC,EAAA,GAbf,OAae,CAAA,IAAA,CAAA;;;;ECxBrD,WAAA,EAAA,GAAA,GDgBS,OChBT,CAAA,IAAA,CAAA;AAgBZ,CAAA;;;;;AAkEA;KD1DY,+BAA+B,uBACzC,iCAAiC;;;KCzBvB,+BAAA;iBACK;;ADFjB;;;;;;;;AAyBA;;;;AAAiE,iBCRjD,6BAAA,CDQiD;EAAA;AAAA,CAAA,ECN9D,+BDM8D,CAAA,ECN5B,gBDM4B;;;;ACxBjE;AAgBA;;;;;AAkEA;;;;ACnEA;;AAKyC,cD8D5B,yBC9D4B,EAAA,CAAA,MAAA,ED+D/B,+BC/D+B,EAAA,GD+DA,KAAA,CAAA,KC/DA,CAAA,OAAA,EAAA,KAAA,EAAA,KAAA,CAAA;;;;;;;AFrBzC;;;;;;;;AAyBA;;;;AAAiE,cETpD,2BAAA,SAAoC,aAAA,CFSgB;;;;ACxBjE;EAgBgB,KAAA,CAAA,OAAA,ECIO,ODJP,CAAA,ECIiB,ODJY,CCIJ,QDJI,CAAA;EAC3C;;;;AAiEF;yBCtC+B;;;AA7B/B;;;;EA6B+B,SAAA,CAAA,WAAA,EAmBA,mBAnBA,CAAA,EAmBsB,OAnBtB,CAAA,IAAA,CAAA;EAmBA;;;;;EAuBuC,WAAA,CAAA,CAAA,EAZ/C,OAY+C,CAAA,IAAA,CAAA;EAW9D;;;;;EAlFsD,gBAAA,CAAA,GAAA,EAuEhC,SAvEgC,EAAA,OAAA,EAuEZ,WAvEY,GAAA,MAAA,CAAA,EAuEQ,OAvER,CAAA,IAAA,CAAA;;;;;;qBAkFtD,+DAGc;;;;;;qBAgBK,4BAAyB"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { BaseEventEmitter, WebSocketConnection } from "@uploadista/core/types";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import { DurableObject } from "cloudflare:workers";
|
|
4
|
+
import { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
5
|
+
|
|
6
|
+
//#region src/event-emitter-durable-object.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* RPC interface for event emitter Durable Objects.
|
|
10
|
+
*
|
|
11
|
+
* Defines the methods that can be called on a Durable Object instance via RPC.
|
|
12
|
+
*/
|
|
13
|
+
type EventEmitterDurableObjectBranded<T> = Rpc.DurableObjectBranded & {
|
|
14
|
+
/**
|
|
15
|
+
* Emit a message to all connected WebSocket clients.
|
|
16
|
+
* @param message - The message to broadcast (typically a JSON string)
|
|
17
|
+
*/
|
|
18
|
+
emit: (message: T) => Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe a WebSocket connection to this DO instance.
|
|
21
|
+
* Note: The actual WebSocket connection is established via fetch()
|
|
22
|
+
* @param connection - WebSocket connection details
|
|
23
|
+
*/
|
|
24
|
+
subscribe: (connection: WebSocketConnection) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Unsubscribe from events by closing all WebSocket connections.
|
|
27
|
+
*/
|
|
28
|
+
unsubscribe: () => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Durable Object namespace type for event emitters.
|
|
32
|
+
*
|
|
33
|
+
* @template T - Type of messages (typically string for JSON messages)
|
|
34
|
+
*/
|
|
35
|
+
type EventEmitterDurableObject<T> = DurableObjectNamespace<EventEmitterDurableObjectBranded<T>>;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/do-event-emitter.d.ts
|
|
38
|
+
type DurableObjectEventEmitterConfig = {
|
|
39
|
+
durableObject: EventEmitterDurableObject<string>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Creates a BaseEventEmitter implementation using Cloudflare Durable Objects.
|
|
43
|
+
*
|
|
44
|
+
* This implementation:
|
|
45
|
+
* - Routes events to Durable Object instances by eventKey
|
|
46
|
+
* - Each eventKey gets its own DO instance
|
|
47
|
+
* - WebSocket connections are managed by the DO
|
|
48
|
+
* - No external broadcaster needed - DO is single source of truth
|
|
49
|
+
*
|
|
50
|
+
* @param config - Configuration with Durable Object namespace
|
|
51
|
+
* @returns BaseEventEmitter implementation
|
|
52
|
+
*/
|
|
53
|
+
declare function durableObjectBaseEventEmitter({
|
|
54
|
+
durableObject
|
|
55
|
+
}: DurableObjectEventEmitterConfig): BaseEventEmitter;
|
|
56
|
+
/**
|
|
57
|
+
* Creates a Layer for BaseEventEmitterService using Durable Objects.
|
|
58
|
+
*
|
|
59
|
+
* Use this when creating an Uploadista server with Durable Objects:
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const server = await createUploadistaServer({
|
|
64
|
+
* eventEmitter: durableObjectEventEmitter({
|
|
65
|
+
* durableObject: env.UPLOADISTA_DO,
|
|
66
|
+
* }),
|
|
67
|
+
* // ... other config
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare const durableObjectEventEmitter: (config: DurableObjectEventEmitterConfig) => Layer.Layer<unknown, never, never>;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/durable-object-impl.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* Base Durable Object implementation for Uploadista event emission.
|
|
76
|
+
*
|
|
77
|
+
* This class provides:
|
|
78
|
+
* - Hibernatable WebSocket connections
|
|
79
|
+
* - Event broadcasting to all connected clients
|
|
80
|
+
* - Automatic connection management
|
|
81
|
+
* - RPC methods for emit/subscribe/unsubscribe
|
|
82
|
+
*
|
|
83
|
+
* Extend this class in your Worker to create event emitter Durable Objects:
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* export class UploadistaDurableObject extends UploadistaDurableObjectImpl {}
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare class UploadistaDurableObjectImpl extends DurableObject {
|
|
91
|
+
/**
|
|
92
|
+
* Handles WebSocket upgrade requests.
|
|
93
|
+
* Creates a hibernatable WebSocket connection.
|
|
94
|
+
*/
|
|
95
|
+
fetch(request: Request): Promise<Response>;
|
|
96
|
+
/**
|
|
97
|
+
* RPC method: Emit a message to all connected WebSocket clients.
|
|
98
|
+
*
|
|
99
|
+
* @param message - The message to broadcast (typically a JSON string)
|
|
100
|
+
*/
|
|
101
|
+
emit(message: string): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* RPC method: Subscribe a WebSocket connection.
|
|
104
|
+
*
|
|
105
|
+
* Note: This is called via RPC, the actual WebSocket connection
|
|
106
|
+
* is established via the fetch() handler.
|
|
107
|
+
*/
|
|
108
|
+
subscribe(_connection: WebSocketConnection): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* RPC method: Unsubscribe from events.
|
|
111
|
+
*
|
|
112
|
+
* Closes all WebSocket connections for this DO instance.
|
|
113
|
+
*/
|
|
114
|
+
unsubscribe(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Hibernation API: Handle incoming WebSocket messages.
|
|
117
|
+
*
|
|
118
|
+
* Called automatically when a message arrives on a hibernated WebSocket.
|
|
119
|
+
*/
|
|
120
|
+
webSocketMessage(_ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Hibernation API: Handle WebSocket close events.
|
|
123
|
+
*
|
|
124
|
+
* Called automatically when a WebSocket connection closes.
|
|
125
|
+
*/
|
|
126
|
+
webSocketClose(ws: WebSocket, code: number, _reason: string, _wasClean: boolean): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Hibernation API: Handle WebSocket errors.
|
|
129
|
+
*
|
|
130
|
+
* Called automatically when a WebSocket error occurs.
|
|
131
|
+
*/
|
|
132
|
+
webSocketError(ws: WebSocket, error: unknown): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { type DurableObjectEventEmitterConfig, type EventEmitterDurableObject, UploadistaDurableObjectImpl, durableObjectBaseEventEmitter, durableObjectEventEmitter };
|
|
136
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/event-emitter-durable-object.ts","../src/do-event-emitter.ts","../src/durable-object-impl.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAQA;AAAsD,KAA1C,gCAA0C,CAAA,CAAA,CAAA,GAAJ,GAAA,CAAI,oBAAA,GAAA;EAKpC;;;;EAYG,IAAA,EAAA,CAAA,OAAA,EAZH,CAYG,EAAA,GAZG,OAYH,CAAA,IAAA,CAAA;EAAO;AAQ5B;;;;EAAiE,SAAA,EAAA,CAAA,UAAA,EAbvC,mBAauC,EAAA,GAbf,OAae,CAAA,IAAA,CAAA;;;;ECxBrD,WAAA,EAAA,GAAA,GDgBS,OChBT,CAAA,IAAA,CAAA;AAgBZ,CAAA;;;;;AAkEA;KD1DY,+BAA+B,uBACzC,iCAAiC;;;KCzBvB,+BAAA;iBACK;;ADFjB;;;;;;;;AAyBA;;;;AAAiE,iBCRjD,6BAAA,CDQiD;EAAA;AAAA,CAAA,ECN9D,+BDM8D,CAAA,ECN5B,gBDM4B;;;;ACxBjE;AAgBA;;;;;AAkEA;;;;ACnEA;;AAKyC,cD8D5B,yBC9D4B,EAAA,CAAA,MAAA,ED+D/B,+BC/D+B,EAAA,GD+DA,KAAA,CAAA,KC/DA,CAAA,OAAA,EAAA,KAAA,EAAA,KAAA,CAAA;;;;;;;AFrBzC;;;;;;;;AAyBA;;;;AAAiE,cETpD,2BAAA,SAAoC,aAAA,CFSgB;;;;ACxBjE;EAgBgB,KAAA,CAAA,OAAA,ECIO,ODJP,CAAA,ECIiB,ODJY,CCIJ,QDJI,CAAA;EAC3C;;;;AAiEF;yBCtC+B;;;AA7B/B;;;;EA6B+B,SAAA,CAAA,WAAA,EAmBA,mBAnBA,CAAA,EAmBsB,OAnBtB,CAAA,IAAA,CAAA;EAmBA;;;;;EAuBuC,WAAA,CAAA,CAAA,EAZ/C,OAY+C,CAAA,IAAA,CAAA;EAW9D;;;;;EAlFsD,gBAAA,CAAA,GAAA,EAuEhC,SAvEgC,EAAA,OAAA,EAuEZ,WAvEY,GAAA,MAAA,CAAA,EAuEQ,OAvER,CAAA,IAAA,CAAA;;;;;;qBAkFtD,+DAGc;;;;;;qBAgBK,4BAAyB"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{UploadistaError as e}from"@uploadista/core/errors";import{BaseEventEmitterService as t}from"@uploadista/core/types";import{Effect as n,Layer as r}from"effect";import{DurableObject as i}from"cloudflare:workers";function a({durableObject:t}){function r(e){let n=t.idFromName(e);return t.get(n)}return{emit:(t,i)=>n.tryPromise({try:async()=>{console.log(`[DO EventEmitter] Emitting to eventKey: ${t}`,i.substring(0,200)),await r(t).emit(i),console.log(`[DO EventEmitter] Successfully emitted to eventKey: ${t}`)},catch:n=>(console.error(`[DO EventEmitter] Failed to emit to eventKey: ${t}`,n),e.fromCode(`UNKNOWN_ERROR`,{cause:n}))}),subscribe:(t,i)=>n.tryPromise({try:async()=>{await r(t).subscribe(i)},catch:t=>e.fromCode(`UNKNOWN_ERROR`,{cause:t})}),unsubscribe:t=>n.tryPromise({try:async()=>{await r(t).unsubscribe()},catch:t=>e.fromCode(`UNKNOWN_ERROR`,{cause:t})})}}const o=e=>r.succeed(t,a(e));var s=class extends i{async fetch(e){console.log(`[DO fetch] WebSocket connection request: ${e.url}`);let t=new WebSocketPair,[n,r]=Object.values(t);return this.ctx.acceptWebSocket(r),console.log(`[DO fetch] WebSocket accepted, total connections: ${this.ctx.getWebSockets().length}`),new Response(null,{status:101,webSocket:n})}async emit(e){let t=this.ctx.getWebSockets();console.log(`[DO emit] Broadcasting message to ${t.length} WebSocket(s):`,e.substring(0,200));for(let n of t)try{n.send(e)}catch(e){console.error(`Failed to send message to WebSocket:`,e)}}async subscribe(e){}async unsubscribe(){let e=this.ctx.getWebSockets();for(let t of e)t.close(1e3,`Unsubscribed`)}async webSocketMessage(e,t){console.log(`WebSocket message received: ${t}`)}async webSocketClose(e,t,n,r){if(e.readyState===WebSocket.OPEN){let n=t===1006||t<1e3||t>4999?1e3:t;e.close(n,`Durable Object closing WebSocket`)}}async webSocketError(e,t){console.error(`WebSocket error:`,t),e.readyState===WebSocket.OPEN&&e.close(1011,`WebSocket error occurred`)}};export{s as UploadistaDurableObjectImpl,a as durableObjectBaseEventEmitter,o as durableObjectEventEmitter};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/do-event-emitter.ts","../src/durable-object-impl.ts"],"sourcesContent":["import { UploadistaError } from \"@uploadista/core/errors\";\nimport type { WebSocketConnection } from \"@uploadista/core/types\";\nimport {\n type BaseEventEmitter,\n BaseEventEmitterService,\n} from \"@uploadista/core/types\";\nimport { Effect, Layer } from \"effect\";\nimport type { EventEmitterDurableObject } from \"./event-emitter-durable-object\";\n\nexport type DurableObjectEventEmitterConfig = {\n durableObject: EventEmitterDurableObject<string>;\n};\n\n/**\n * Creates a BaseEventEmitter implementation using Cloudflare Durable Objects.\n *\n * This implementation:\n * - Routes events to Durable Object instances by eventKey\n * - Each eventKey gets its own DO instance\n * - WebSocket connections are managed by the DO\n * - No external broadcaster needed - DO is single source of truth\n *\n * @param config - Configuration with Durable Object namespace\n * @returns BaseEventEmitter implementation\n */\nexport function durableObjectBaseEventEmitter({\n durableObject,\n}: DurableObjectEventEmitterConfig): BaseEventEmitter {\n function getStub(eventKey: string) {\n const id = durableObject.idFromName(eventKey);\n return durableObject.get(id);\n }\n\n return {\n emit: (eventKey: string, message: string) => {\n return Effect.tryPromise({\n try: async () => {\n console.log(`[DO EventEmitter] Emitting to eventKey: ${eventKey}`, message.substring(0, 200));\n const stub = getStub(eventKey);\n // Call the emit RPC method on the Durable Object\n await (stub as any).emit(message);\n console.log(`[DO EventEmitter] Successfully emitted to eventKey: ${eventKey}`);\n },\n catch: (cause) => {\n console.error(`[DO EventEmitter] Failed to emit to eventKey: ${eventKey}`, cause);\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", { cause });\n },\n });\n },\n subscribe: (eventKey: string, connection: WebSocketConnection) => {\n return Effect.tryPromise({\n try: async () => {\n const stub = getStub(eventKey);\n // Call the subscribe RPC method on the Durable Object\n await (stub as any).subscribe(connection);\n },\n catch: (cause) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", { cause });\n },\n });\n },\n unsubscribe: (eventKey: string) => {\n return Effect.tryPromise({\n try: async () => {\n const stub = getStub(eventKey);\n // Call the unsubscribe RPC method on the Durable Object\n await (stub as any).unsubscribe();\n },\n catch: (cause) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", { cause });\n },\n });\n },\n };\n}\n\n/**\n * Creates a Layer for BaseEventEmitterService using Durable Objects.\n *\n * Use this when creating an Uploadista server with Durable Objects:\n *\n * @example\n * ```typescript\n * const server = await createUploadistaServer({\n * eventEmitter: durableObjectEventEmitter({\n * durableObject: env.UPLOADISTA_DO,\n * }),\n * // ... other config\n * });\n * ```\n */\nexport const durableObjectEventEmitter = (\n config: DurableObjectEventEmitterConfig,\n) =>\n Layer.succeed(\n BaseEventEmitterService,\n durableObjectBaseEventEmitter(config),\n );\n","import { DurableObject } from \"cloudflare:workers\";\nimport type { WebSocketConnection } from \"@uploadista/core/types\";\n\n// WebSocketPair is available globally in Cloudflare Workers\ndeclare const WebSocketPair: {\n new (): { 0: WebSocket; 1: WebSocket };\n};\n\n/**\n * Base Durable Object implementation for Uploadista event emission.\n *\n * This class provides:\n * - Hibernatable WebSocket connections\n * - Event broadcasting to all connected clients\n * - Automatic connection management\n * - RPC methods for emit/subscribe/unsubscribe\n *\n * Extend this class in your Worker to create event emitter Durable Objects:\n *\n * @example\n * ```typescript\n * export class UploadistaDurableObject extends UploadistaDurableObjectImpl {}\n * ```\n */\nexport class UploadistaDurableObjectImpl extends DurableObject {\n /**\n * Handles WebSocket upgrade requests.\n * Creates a hibernatable WebSocket connection.\n */\n async fetch(request: Request): Promise<Response> {\n console.log(`[DO fetch] WebSocket connection request: ${request.url}`);\n\n // Creates two ends of a WebSocket connection\n const pair = new WebSocketPair();\n const [client, server] = Object.values(pair);\n\n // Accept WebSocket with hibernation support\n // This allows the DO to be evicted from memory during idle periods\n this.ctx.acceptWebSocket(server);\n\n console.log(`[DO fetch] WebSocket accepted, total connections: ${this.ctx.getWebSockets().length}`);\n\n return new Response(null, {\n status: 101,\n webSocket: client as unknown,\n } as ResponseInit);\n }\n\n /**\n * RPC method: Emit a message to all connected WebSocket clients.\n *\n * @param message - The message to broadcast (typically a JSON string)\n */\n async emit(message: string): Promise<void> {\n const websockets = this.ctx.getWebSockets();\n console.log(`[DO emit] Broadcasting message to ${websockets.length} WebSocket(s):`, message.substring(0, 200));\n\n for (const ws of websockets) {\n try {\n ws.send(message);\n } catch (error) {\n console.error(\"Failed to send message to WebSocket:\", error);\n }\n }\n }\n\n /**\n * RPC method: Subscribe a WebSocket connection.\n *\n * Note: This is called via RPC, the actual WebSocket connection\n * is established via the fetch() handler.\n */\n async subscribe(_connection: WebSocketConnection): Promise<void> {\n // The connection is already established via fetch()\n // This method exists for API compatibility\n return;\n }\n\n /**\n * RPC method: Unsubscribe from events.\n *\n * Closes all WebSocket connections for this DO instance.\n */\n async unsubscribe(): Promise<void> {\n const websockets = this.ctx.getWebSockets();\n for (const ws of websockets) {\n ws.close(1000, \"Unsubscribed\");\n }\n }\n\n /**\n * Hibernation API: Handle incoming WebSocket messages.\n *\n * Called automatically when a message arrives on a hibernated WebSocket.\n */\n async webSocketMessage(_ws: WebSocket, message: ArrayBuffer | string) {\n // Log message for debugging\n console.log(`WebSocket message received: ${message}`);\n }\n\n /**\n * Hibernation API: Handle WebSocket close events.\n *\n * Called automatically when a WebSocket connection closes.\n */\n async webSocketClose(\n ws: WebSocket,\n code: number,\n _reason: string,\n _wasClean: boolean,\n ) {\n // Clean up the connection\n if (ws.readyState === WebSocket.OPEN) {\n // Use a valid close code instead of potentially reserved ones\n const validCloseCode =\n code === 1006 || code < 1000 || code > 4999 ? 1000 : code;\n ws.close(validCloseCode, \"Durable Object closing WebSocket\");\n }\n }\n\n /**\n * Hibernation API: Handle WebSocket errors.\n *\n * Called automatically when a WebSocket error occurs.\n */\n async webSocketError(ws: WebSocket, error: unknown) {\n console.error(\"WebSocket error:\", error);\n if (ws.readyState === WebSocket.OPEN) {\n ws.close(1011, \"WebSocket error occurred\");\n }\n }\n}\n"],"mappings":"yNAyBA,SAAgB,EAA8B,CAC5C,iBACoD,CACpD,SAAS,EAAQ,EAAkB,CACjC,IAAM,EAAK,EAAc,WAAW,EAAS,CAC7C,OAAO,EAAc,IAAI,EAAG,CAG9B,MAAO,CACL,MAAO,EAAkB,IAChB,EAAO,WAAW,CACvB,IAAK,SAAY,CACf,QAAQ,IAAI,2CAA2C,IAAY,EAAQ,UAAU,EAAG,IAAI,CAAC,CAG7F,MAFa,EAAQ,EAAS,CAEV,KAAK,EAAQ,CACjC,QAAQ,IAAI,uDAAuD,IAAW,EAEhF,MAAQ,IACN,QAAQ,MAAM,iDAAiD,IAAY,EAAM,CAC1E,EAAgB,SAAS,gBAAiB,CAAE,QAAO,CAAC,EAE9D,CAAC,CAEJ,WAAY,EAAkB,IACrB,EAAO,WAAW,CACvB,IAAK,SAAY,CAGf,MAFa,EAAQ,EAAS,CAEV,UAAU,EAAW,EAE3C,MAAQ,GACC,EAAgB,SAAS,gBAAiB,CAAE,QAAO,CAAC,CAE9D,CAAC,CAEJ,YAAc,GACL,EAAO,WAAW,CACvB,IAAK,SAAY,CAGf,MAFa,EAAQ,EAAS,CAEV,aAAa,EAEnC,MAAQ,GACC,EAAgB,SAAS,gBAAiB,CAAE,QAAO,CAAC,CAE9D,CAAC,CAEL,CAkBH,MAAa,EACX,GAEA,EAAM,QACJ,EACA,EAA8B,EAAO,CACtC,CCzEH,IAAa,EAAb,cAAiD,CAAc,CAK7D,MAAM,MAAM,EAAqC,CAC/C,QAAQ,IAAI,4CAA4C,EAAQ,MAAM,CAGtE,IAAM,EAAO,IAAI,cACX,CAAC,EAAQ,GAAU,OAAO,OAAO,EAAK,CAQ5C,OAJA,KAAK,IAAI,gBAAgB,EAAO,CAEhC,QAAQ,IAAI,qDAAqD,KAAK,IAAI,eAAe,CAAC,SAAS,CAE5F,IAAI,SAAS,KAAM,CACxB,OAAQ,IACR,UAAW,EACZ,CAAiB,CAQpB,MAAM,KAAK,EAAgC,CACzC,IAAM,EAAa,KAAK,IAAI,eAAe,CAC3C,QAAQ,IAAI,qCAAqC,EAAW,OAAO,gBAAiB,EAAQ,UAAU,EAAG,IAAI,CAAC,CAE9G,IAAK,IAAM,KAAM,EACf,GAAI,CACF,EAAG,KAAK,EAAQ,OACT,EAAO,CACd,QAAQ,MAAM,uCAAwC,EAAM,EAWlE,MAAM,UAAU,EAAiD,EAWjE,MAAM,aAA6B,CACjC,IAAM,EAAa,KAAK,IAAI,eAAe,CAC3C,IAAK,IAAM,KAAM,EACf,EAAG,MAAM,IAAM,eAAe,CASlC,MAAM,iBAAiB,EAAgB,EAA+B,CAEpE,QAAQ,IAAI,+BAA+B,IAAU,CAQvD,MAAM,eACJ,EACA,EACA,EACA,EACA,CAEA,GAAI,EAAG,aAAe,UAAU,KAAM,CAEpC,IAAM,EACJ,IAAS,MAAQ,EAAO,KAAQ,EAAO,KAAO,IAAO,EACvD,EAAG,MAAM,EAAgB,mCAAmC,EAShE,MAAM,eAAe,EAAe,EAAgB,CAClD,QAAQ,MAAM,mBAAoB,EAAM,CACpC,EAAG,aAAe,UAAU,MAC9B,EAAG,MAAM,KAAM,2BAA2B"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/event-emitter-durable-object",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.9",
|
|
5
5
|
"description": "Durable Object event emitter for Uploadista",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@cloudflare/workers-types": "4.20251014.0",
|
|
17
|
-
"effect": "3.
|
|
18
|
-
"@uploadista/core": "0.0.
|
|
17
|
+
"effect": "3.19.0",
|
|
18
|
+
"@uploadista/core": "0.0.9"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"tsdown": "0.
|
|
22
|
-
"@uploadista/typescript-config": "0.0.
|
|
21
|
+
"tsdown": "0.16.0",
|
|
22
|
+
"@uploadista/typescript-config": "0.0.9"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsdown",
|
package/src/do-event-emitter.ts
CHANGED
|
@@ -1,60 +1,70 @@
|
|
|
1
1
|
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
2
3
|
import {
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
UploadEventEmitter,
|
|
4
|
+
type BaseEventEmitter,
|
|
5
|
+
BaseEventEmitterService,
|
|
6
6
|
} from "@uploadista/core/types";
|
|
7
7
|
import { Effect, Layer } from "effect";
|
|
8
|
-
import type {
|
|
9
|
-
EventEmitterDurableObject,
|
|
10
|
-
EventEmitterDurableObjectBranded,
|
|
11
|
-
} from "./event-emitter-durable-object";
|
|
8
|
+
import type { EventEmitterDurableObject } from "./event-emitter-durable-object";
|
|
12
9
|
|
|
13
|
-
export type
|
|
14
|
-
durableObject: EventEmitterDurableObject<
|
|
10
|
+
export type DurableObjectEventEmitterConfig = {
|
|
11
|
+
durableObject: EventEmitterDurableObject<string>;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Creates a BaseEventEmitter implementation using Cloudflare Durable Objects.
|
|
16
|
+
*
|
|
17
|
+
* This implementation:
|
|
18
|
+
* - Routes events to Durable Object instances by eventKey
|
|
19
|
+
* - Each eventKey gets its own DO instance
|
|
20
|
+
* - WebSocket connections are managed by the DO
|
|
21
|
+
* - No external broadcaster needed - DO is single source of truth
|
|
22
|
+
*
|
|
23
|
+
* @param config - Configuration with Durable Object namespace
|
|
24
|
+
* @returns BaseEventEmitter implementation
|
|
25
|
+
*/
|
|
26
|
+
export function durableObjectBaseEventEmitter({
|
|
18
27
|
durableObject,
|
|
19
|
-
}:
|
|
20
|
-
function getStub(
|
|
21
|
-
const id = durableObject.idFromName(
|
|
22
|
-
return durableObject.get(
|
|
23
|
-
id,
|
|
24
|
-
) as unknown as EventEmitterDurableObjectBranded<T>;
|
|
28
|
+
}: DurableObjectEventEmitterConfig): BaseEventEmitter {
|
|
29
|
+
function getStub(eventKey: string) {
|
|
30
|
+
const id = durableObject.idFromName(eventKey);
|
|
31
|
+
return durableObject.get(id);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
return {
|
|
28
|
-
emit: (
|
|
29
|
-
const stub = getStub(key);
|
|
35
|
+
emit: (eventKey: string, message: string) => {
|
|
30
36
|
return Effect.tryPromise({
|
|
31
37
|
try: async () => {
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
console.log(`[DO EventEmitter] Emitting to eventKey: ${eventKey}`, message.substring(0, 200));
|
|
39
|
+
const stub = getStub(eventKey);
|
|
40
|
+
// Call the emit RPC method on the Durable Object
|
|
41
|
+
await (stub as any).emit(message);
|
|
42
|
+
console.log(`[DO EventEmitter] Successfully emitted to eventKey: ${eventKey}`);
|
|
34
43
|
},
|
|
35
44
|
catch: (cause) => {
|
|
45
|
+
console.error(`[DO EventEmitter] Failed to emit to eventKey: ${eventKey}`, cause);
|
|
36
46
|
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
37
47
|
},
|
|
38
48
|
});
|
|
39
49
|
},
|
|
40
|
-
subscribe: (
|
|
50
|
+
subscribe: (eventKey: string, connection: WebSocketConnection) => {
|
|
41
51
|
return Effect.tryPromise({
|
|
42
52
|
try: async () => {
|
|
43
|
-
const stub = getStub(
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
const stub = getStub(eventKey);
|
|
54
|
+
// Call the subscribe RPC method on the Durable Object
|
|
55
|
+
await (stub as any).subscribe(connection);
|
|
46
56
|
},
|
|
47
57
|
catch: (cause) => {
|
|
48
58
|
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
49
59
|
},
|
|
50
60
|
});
|
|
51
61
|
},
|
|
52
|
-
unsubscribe: (
|
|
62
|
+
unsubscribe: (eventKey: string) => {
|
|
53
63
|
return Effect.tryPromise({
|
|
54
64
|
try: async () => {
|
|
55
|
-
const stub = getStub(
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
const stub = getStub(eventKey);
|
|
66
|
+
// Call the unsubscribe RPC method on the Durable Object
|
|
67
|
+
await (stub as any).unsubscribe();
|
|
58
68
|
},
|
|
59
69
|
catch: (cause) => {
|
|
60
70
|
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
@@ -64,10 +74,25 @@ export function makeEventEmitterDurableObjectStore<T>({
|
|
|
64
74
|
};
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Creates a Layer for BaseEventEmitterService using Durable Objects.
|
|
79
|
+
*
|
|
80
|
+
* Use this when creating an Uploadista server with Durable Objects:
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const server = await createUploadistaServer({
|
|
85
|
+
* eventEmitter: durableObjectEventEmitter({
|
|
86
|
+
* durableObject: env.UPLOADISTA_DO,
|
|
87
|
+
* }),
|
|
88
|
+
* // ... other config
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export const durableObjectEventEmitter = (
|
|
93
|
+
config: DurableObjectEventEmitterConfig,
|
|
69
94
|
) =>
|
|
70
95
|
Layer.succeed(
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
BaseEventEmitterService,
|
|
97
|
+
durableObjectBaseEventEmitter(config),
|
|
73
98
|
);
|
|
@@ -1,58 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
3
|
+
|
|
4
|
+
// WebSocketPair is available globally in Cloudflare Workers
|
|
5
|
+
declare const WebSocketPair: {
|
|
6
|
+
new (): { 0: WebSocket; 1: WebSocket };
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base Durable Object implementation for Uploadista event emission.
|
|
11
|
+
*
|
|
12
|
+
* This class provides:
|
|
13
|
+
* - Hibernatable WebSocket connections
|
|
14
|
+
* - Event broadcasting to all connected clients
|
|
15
|
+
* - Automatic connection management
|
|
16
|
+
* - RPC methods for emit/subscribe/unsubscribe
|
|
17
|
+
*
|
|
18
|
+
* Extend this class in your Worker to create event emitter Durable Objects:
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* export class UploadistaDurableObject extends UploadistaDurableObjectImpl {}
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class UploadistaDurableObjectImpl extends DurableObject {
|
|
26
|
+
/**
|
|
27
|
+
* Handles WebSocket upgrade requests.
|
|
28
|
+
* Creates a hibernatable WebSocket connection.
|
|
29
|
+
*/
|
|
30
|
+
async fetch(request: Request): Promise<Response> {
|
|
31
|
+
console.log(`[DO fetch] WebSocket connection request: ${request.url}`);
|
|
32
|
+
|
|
33
|
+
// Creates two ends of a WebSocket connection
|
|
34
|
+
const pair = new WebSocketPair();
|
|
35
|
+
const [client, server] = Object.values(pair);
|
|
36
|
+
|
|
37
|
+
// Accept WebSocket with hibernation support
|
|
38
|
+
// This allows the DO to be evicted from memory during idle periods
|
|
39
|
+
this.ctx.acceptWebSocket(server);
|
|
40
|
+
|
|
41
|
+
console.log(`[DO fetch] WebSocket accepted, total connections: ${this.ctx.getWebSockets().length}`);
|
|
42
|
+
|
|
43
|
+
return new Response(null, {
|
|
44
|
+
status: 101,
|
|
45
|
+
webSocket: client as unknown,
|
|
46
|
+
} as ResponseInit);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* RPC method: Emit a message to all connected WebSocket clients.
|
|
51
|
+
*
|
|
52
|
+
* @param message - The message to broadcast (typically a JSON string)
|
|
53
|
+
*/
|
|
54
|
+
async emit(message: string): Promise<void> {
|
|
55
|
+
const websockets = this.ctx.getWebSockets();
|
|
56
|
+
console.log(`[DO emit] Broadcasting message to ${websockets.length} WebSocket(s):`, message.substring(0, 200));
|
|
57
|
+
|
|
58
|
+
for (const ws of websockets) {
|
|
59
|
+
try {
|
|
60
|
+
ws.send(message);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Failed to send message to WebSocket:", error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* RPC method: Subscribe a WebSocket connection.
|
|
69
|
+
*
|
|
70
|
+
* Note: This is called via RPC, the actual WebSocket connection
|
|
71
|
+
* is established via the fetch() handler.
|
|
72
|
+
*/
|
|
73
|
+
async subscribe(_connection: WebSocketConnection): Promise<void> {
|
|
74
|
+
// The connection is already established via fetch()
|
|
75
|
+
// This method exists for API compatibility
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* RPC method: Unsubscribe from events.
|
|
81
|
+
*
|
|
82
|
+
* Closes all WebSocket connections for this DO instance.
|
|
83
|
+
*/
|
|
84
|
+
async unsubscribe(): Promise<void> {
|
|
85
|
+
const websockets = this.ctx.getWebSockets();
|
|
86
|
+
for (const ws of websockets) {
|
|
87
|
+
ws.close(1000, "Unsubscribed");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Hibernation API: Handle incoming WebSocket messages.
|
|
93
|
+
*
|
|
94
|
+
* Called automatically when a message arrives on a hibernated WebSocket.
|
|
95
|
+
*/
|
|
96
|
+
async webSocketMessage(_ws: WebSocket, message: ArrayBuffer | string) {
|
|
97
|
+
// Log message for debugging
|
|
98
|
+
console.log(`WebSocket message received: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hibernation API: Handle WebSocket close events.
|
|
103
|
+
*
|
|
104
|
+
* Called automatically when a WebSocket connection closes.
|
|
105
|
+
*/
|
|
106
|
+
async webSocketClose(
|
|
107
|
+
ws: WebSocket,
|
|
108
|
+
code: number,
|
|
109
|
+
_reason: string,
|
|
110
|
+
_wasClean: boolean,
|
|
111
|
+
) {
|
|
112
|
+
// Clean up the connection
|
|
113
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
114
|
+
// Use a valid close code instead of potentially reserved ones
|
|
115
|
+
const validCloseCode =
|
|
116
|
+
code === 1006 || code < 1000 || code > 4999 ? 1000 : code;
|
|
117
|
+
ws.close(validCloseCode, "Durable Object closing WebSocket");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Hibernation API: Handle WebSocket errors.
|
|
123
|
+
*
|
|
124
|
+
* Called automatically when a WebSocket error occurs.
|
|
125
|
+
*/
|
|
126
|
+
async webSocketError(ws: WebSocket, error: unknown) {
|
|
127
|
+
console.error("WebSocket error:", error);
|
|
128
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
129
|
+
ws.close(1011, "WebSocket error occurred");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
2
|
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* RPC interface for event emitter Durable Objects.
|
|
6
|
+
*
|
|
7
|
+
* Defines the methods that can be called on a Durable Object instance via RPC.
|
|
8
|
+
*/
|
|
4
9
|
export type EventEmitterDurableObjectBranded<T> = Rpc.DurableObjectBranded & {
|
|
5
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Emit a message to all connected WebSocket clients.
|
|
12
|
+
* @param message - The message to broadcast (typically a JSON string)
|
|
13
|
+
*/
|
|
14
|
+
emit: (message: T) => Promise<void>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Subscribe a WebSocket connection to this DO instance.
|
|
18
|
+
* Note: The actual WebSocket connection is established via fetch()
|
|
19
|
+
* @param connection - WebSocket connection details
|
|
20
|
+
*/
|
|
6
21
|
subscribe: (connection: WebSocketConnection) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Unsubscribe from events by closing all WebSocket connections.
|
|
25
|
+
*/
|
|
7
26
|
unsubscribe: () => Promise<void>;
|
|
8
27
|
};
|
|
9
28
|
|
|
10
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Durable Object namespace type for event emitters.
|
|
31
|
+
*
|
|
32
|
+
* @template T - Type of messages (typically string for JSON messages)
|
|
33
|
+
*/
|
|
11
34
|
export type EventEmitterDurableObject<T> = DurableObjectNamespace<
|
|
12
35
|
EventEmitterDurableObjectBranded<T>
|
|
13
36
|
>;
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
durableObjectEventEmitter,
|
|
3
|
+
durableObjectBaseEventEmitter,
|
|
4
|
+
type DurableObjectEventEmitterConfig,
|
|
5
|
+
} from "./do-event-emitter";
|
|
6
|
+
export { UploadistaDurableObjectImpl } from "./durable-object-impl";
|
|
3
7
|
export type { EventEmitterDurableObject } from "./event-emitter-durable-object";
|
package/dist/index.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { UploadEvent, UploadEventEmitter, WebSocketConnection } from "@uploadista/core/types";
|
|
2
|
-
import { Layer } from "effect";
|
|
3
|
-
import { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
4
|
-
|
|
5
|
-
//#region src/event-emitter-durable-object.d.ts
|
|
6
|
-
type EventEmitterDurableObjectBranded<T> = Rpc.DurableObjectBranded & {
|
|
7
|
-
emit: (event: T) => Promise<void>;
|
|
8
|
-
subscribe: (connection: WebSocketConnection) => Promise<void>;
|
|
9
|
-
unsubscribe: () => Promise<void>;
|
|
10
|
-
};
|
|
11
|
-
type EventEmitterDurableObject<T> = DurableObjectNamespace<EventEmitterDurableObjectBranded<T>>;
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/do-event-emitter.d.ts
|
|
14
|
-
type UploadEventEmitterDurableObjectStoreConfig = {
|
|
15
|
-
durableObject: EventEmitterDurableObject<UploadEvent>;
|
|
16
|
-
};
|
|
17
|
-
declare const uploadEventEmitterDurableObjectStore: (config: UploadEventEmitterDurableObjectStoreConfig) => Layer.Layer<UploadEventEmitter, never, never>;
|
|
18
|
-
//#endregion
|
|
19
|
-
export { type EventEmitterDurableObject, uploadEventEmitterDurableObjectStore };
|
|
20
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/event-emitter-durable-object.ts","../src/do-event-emitter.ts"],"sourcesContent":[],"mappings":";;;;;KAGY,sCAAsC,GAAA,CAAI;gBACtC,MAAM;0BACI,wBAAwB;EAFtC,WAAA,EAAA,GAAA,GAGS,OAHT,CAAA,IAAA,CAAA;CAAsC;AAClC,KAMJ,yBANI,CAAA,CAAA,CAAA,GAM2B,sBAN3B,CAOd,gCAPc,CAOmB,CAPnB,CAAA,CAAA;;;KCQJ,0CAAA;iBACK,0BAA0B;ADV3C,CAAA;AACgB,cC8DH,oCD9DG,EAAA,CAAA,MAAA,EC+DN,0CD/DM,EAAA,GC+DoC,KAAA,CAAA,KD/DpC,CC+DoC,kBD/DpC,EAAA,KAAA,EAAA,KAAA,CAAA"}
|
package/dist/index.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{UploadistaError as e}from"@uploadista/core/errors";import{UploadEventEmitter as t}from"@uploadista/core/types";import{Effect as n,Layer as r}from"effect";function i({durableObject:t}){function r(e){let n=t.idFromName(e);return t.get(n)}return{emit:(t,i)=>{let a=r(t);return n.tryPromise({try:async()=>{await a.emit(i)},catch:t=>e.fromCode(`UNKNOWN_ERROR`,{cause:t})})},subscribe:(t,i)=>n.tryPromise({try:async()=>{await r(t).subscribe(i)},catch:t=>e.fromCode(`UNKNOWN_ERROR`,{cause:t})}),unsubscribe:t=>n.tryPromise({try:async()=>{await r(t).unsubscribe()},catch:t=>e.fromCode(`UNKNOWN_ERROR`,{cause:t})})}}const a=e=>r.succeed(t,i(e));export{a as uploadEventEmitterDurableObjectStore};
|
|
2
|
-
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/do-event-emitter.ts"],"sourcesContent":["import { UploadistaError } from \"@uploadista/core/errors\";\nimport {\n type EventEmitter,\n type UploadEvent,\n UploadEventEmitter,\n} from \"@uploadista/core/types\";\nimport { Effect, Layer } from \"effect\";\nimport type {\n EventEmitterDurableObject,\n EventEmitterDurableObjectBranded,\n} from \"./event-emitter-durable-object\";\n\nexport type UploadEventEmitterDurableObjectStoreConfig = {\n durableObject: EventEmitterDurableObject<UploadEvent>;\n};\n\nexport function makeEventEmitterDurableObjectStore<T>({\n durableObject,\n}: UploadEventEmitterDurableObjectStoreConfig): EventEmitter<T> {\n function getStub(key: string) {\n const id = durableObject.idFromName(key);\n return durableObject.get(\n id,\n ) as unknown as EventEmitterDurableObjectBranded<T>;\n }\n\n return {\n emit: (key: string, event: T) => {\n const stub = getStub(key);\n return Effect.tryPromise({\n try: async () => {\n await stub.emit(event);\n return;\n },\n catch: (cause) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", { cause });\n },\n });\n },\n subscribe: (key: string, connection) => {\n return Effect.tryPromise({\n try: async () => {\n const stub = getStub(key);\n await stub.subscribe(connection);\n return;\n },\n catch: (cause) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", { cause });\n },\n });\n },\n unsubscribe: (key: string) => {\n return Effect.tryPromise({\n try: async () => {\n const stub = getStub(key);\n await stub.unsubscribe();\n return;\n },\n catch: (cause) => {\n return UploadistaError.fromCode(\"UNKNOWN_ERROR\", { cause });\n },\n });\n },\n };\n}\n\nexport const uploadEventEmitterDurableObjectStore = (\n config: UploadEventEmitterDurableObjectStoreConfig,\n) =>\n Layer.succeed(\n UploadEventEmitter,\n makeEventEmitterDurableObjectStore<UploadEvent>(config),\n );\n"],"mappings":"iKAgBA,SAAgB,EAAsC,CACpD,iBAC8D,CAC9D,SAAS,EAAQ,EAAa,CAC5B,IAAM,EAAK,EAAc,WAAW,EAAI,CACxC,OAAO,EAAc,IACnB,EACD,CAGH,MAAO,CACL,MAAO,EAAa,IAAa,CAC/B,IAAM,EAAO,EAAQ,EAAI,CACzB,OAAO,EAAO,WAAW,CACvB,IAAK,SAAY,CACf,MAAM,EAAK,KAAK,EAAM,EAGxB,MAAQ,GACC,EAAgB,SAAS,gBAAiB,CAAE,QAAO,CAAC,CAE9D,CAAC,EAEJ,WAAY,EAAa,IAChB,EAAO,WAAW,CACvB,IAAK,SAAY,CAEf,MADa,EAAQ,EAAI,CACd,UAAU,EAAW,EAGlC,MAAQ,GACC,EAAgB,SAAS,gBAAiB,CAAE,QAAO,CAAC,CAE9D,CAAC,CAEJ,YAAc,GACL,EAAO,WAAW,CACvB,IAAK,SAAY,CAEf,MADa,EAAQ,EAAI,CACd,aAAa,EAG1B,MAAQ,GACC,EAAgB,SAAS,gBAAiB,CAAE,QAAO,CAAC,CAE9D,CAAC,CAEL,CAGH,MAAa,EACX,GAEA,EAAM,QACJ,EACA,EAAgD,EAAO,CACxD"}
|