dynamodb-reactive 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/{client-wZQ9XC9S.d.ts → client-BPAct-Dz.d.ts} +8 -0
- package/dist/client.cjs +15 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.cjs +149 -44
- package/dist/cloudflare.cjs.map +1 -1
- package/dist/cloudflare.d.cts +44 -6
- package/dist/cloudflare.d.ts +44 -6
- package/dist/cloudflare.js +149 -44
- package/dist/cloudflare.js.map +1 -1
- package/dist/cluster.cjs +15 -0
- package/dist/cluster.cjs.map +1 -1
- package/dist/cluster.js +15 -0
- package/dist/cluster.js.map +1 -1
- package/dist/hub.cjs +27 -5
- package/dist/hub.cjs.map +1 -1
- package/dist/hub.d.cts +15 -2
- package/dist/hub.d.ts +15 -2
- package/dist/hub.js +27 -5
- package/dist/hub.js.map +1 -1
- package/dist/react.cjs +15 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +15 -0
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/cloudflare.d.cts
CHANGED
|
@@ -25,6 +25,8 @@ interface DurableObjectStateLike {
|
|
|
25
25
|
storage: {
|
|
26
26
|
get<T>(key: string): Promise<T | undefined>;
|
|
27
27
|
put<T>(key: string, value: T): Promise<void>;
|
|
28
|
+
/** Lets presence forget principals that left instead of storing `false`. */
|
|
29
|
+
delete?(key: string): Promise<boolean>;
|
|
28
30
|
/** Needed for presence. */
|
|
29
31
|
getAlarm?(): Promise<number | null>;
|
|
30
32
|
/** Needed for presence. */
|
|
@@ -39,6 +41,10 @@ type WebSocketPairConstructor = new () => {
|
|
|
39
41
|
0: DurableObjectWebSocket;
|
|
40
42
|
1: DurableObjectWebSocket;
|
|
41
43
|
};
|
|
44
|
+
/** A frame from the client that is not part of the protocol, such as `{ type: 'listening' }`. */
|
|
45
|
+
type HubClientFrame = Record<string, unknown> & {
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
42
48
|
interface DurableObjectHubOptions {
|
|
43
49
|
paths: HubPaths;
|
|
44
50
|
/** Storage key for a path's version. Defaults to `version:<path>`. */
|
|
@@ -50,17 +56,27 @@ interface DurableObjectHubOptions {
|
|
|
50
56
|
*/
|
|
51
57
|
autoPong?: boolean;
|
|
52
58
|
/**
|
|
53
|
-
* Tracks whether
|
|
54
|
-
* worker as online without
|
|
59
|
+
* Tracks whether principals hold a live socket, so the hub can report a
|
|
60
|
+
* worker or listener as online without it polling over HTTP. Requires `autoPong`.
|
|
55
61
|
*/
|
|
56
62
|
presence?: HubPresenceOptions;
|
|
63
|
+
/** Runs after a socket is accepted. */
|
|
64
|
+
onOpen?: (socket: DurableObjectWebSocket, principal: string) => void | Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Runs when a socket closes or errors. The socket may still be listed by
|
|
67
|
+
* `sockets()` during this call, so exclude it when counting.
|
|
68
|
+
*/
|
|
69
|
+
onClose?: (socket: DurableObjectWebSocket, principal: string) => void | Promise<void>;
|
|
70
|
+
/** Handles frames the protocol does not, such as application presence updates. */
|
|
71
|
+
onMessage?: (socket: DurableObjectWebSocket, principal: string, frame: HubClientFrame) => void | Promise<void>;
|
|
57
72
|
/** Workers' `WebSocketPair`; injectable for tests. */
|
|
58
73
|
WebSocketPair?: WebSocketPairConstructor;
|
|
59
74
|
/** Creates a connection id; injectable for tests. */
|
|
60
75
|
randomId?: () => string;
|
|
61
76
|
}
|
|
62
77
|
interface HubPresenceOptions {
|
|
63
|
-
principals
|
|
78
|
+
/** The principals to track, or a predicate for open-ended ones such as anonymous listeners. */
|
|
79
|
+
principals: readonly string[] | ((principal: string) => boolean);
|
|
64
80
|
/**
|
|
65
81
|
* A socket whose last `ping` is older than this is closed as dead, which is how
|
|
66
82
|
* a peer that vanished without a close frame is noticed. Clients must ping more
|
|
@@ -77,7 +93,12 @@ interface HubPresenceOptions {
|
|
|
77
93
|
/** Wires the hub core to a Durable Object's hibernatable WebSockets and storage. */
|
|
78
94
|
declare function createDurableObjectHub(ctx: DurableObjectStateLike, options: DurableObjectHubOptions): {
|
|
79
95
|
hub: {
|
|
80
|
-
message(socket: HubSocket, raw: string):
|
|
96
|
+
message(socket: HubSocket, raw: string): (Record<string, unknown> & {
|
|
97
|
+
type: string;
|
|
98
|
+
}) | undefined;
|
|
99
|
+
socketData<T = unknown>(socket: HubSocket): T | undefined;
|
|
100
|
+
setSocketData(socket: HubSocket, data: unknown): void;
|
|
101
|
+
principalOf(socket: HubSocket): string;
|
|
81
102
|
subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse>;
|
|
82
103
|
unsubscribe(request: UnsubscribeRequest, principal: string): void;
|
|
83
104
|
notify(path: string): Promise<void>;
|
|
@@ -85,9 +106,17 @@ declare function createDurableObjectHub(ctx: DurableObjectStateLike, options: Du
|
|
|
85
106
|
};
|
|
86
107
|
/** The Durable Object's `fetch`: WebSocket upgrades and actions from `hubClient`. */
|
|
87
108
|
fetch(request: Request): Promise<Response>;
|
|
88
|
-
webSocketMessage(socket: DurableObjectWebSocket, message: string | ArrayBuffer): void
|
|
109
|
+
webSocketMessage(socket: DurableObjectWebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
89
110
|
webSocketClose(socket: DurableObjectWebSocket, code: number): Promise<void>;
|
|
90
111
|
webSocketError(socket: DurableObjectWebSocket): Promise<void>;
|
|
112
|
+
/** Every accepted socket, including one whose close is being handled. */
|
|
113
|
+
sockets(): DurableObjectWebSocket[];
|
|
114
|
+
/** The principal a socket authenticated as. */
|
|
115
|
+
principalOf: (socket: DurableObjectWebSocket) => string;
|
|
116
|
+
/** Application data stored with a socket by `setSocketData`. */
|
|
117
|
+
socketData: <T = unknown>(socket: DurableObjectWebSocket) => T | undefined;
|
|
118
|
+
/** Stores application data with a socket; it survives hibernation and shares the 2 KB limit. */
|
|
119
|
+
setSocketData: (socket: DurableObjectWebSocket, data: unknown) => void;
|
|
91
120
|
/** Whether `principal` holds a socket that has pinged within the presence timeout. */
|
|
92
121
|
present(principal: string): boolean;
|
|
93
122
|
/**
|
|
@@ -111,18 +140,27 @@ interface ReactiveRoutes {
|
|
|
111
140
|
socket: string;
|
|
112
141
|
/** Reactive HTTP subscribe and unsubscribe, authenticated by the ticket as a bearer token. */
|
|
113
142
|
reactive: string;
|
|
143
|
+
/** `POST { path, patches? }` from other backends, authenticated by `notifySecret`. */
|
|
144
|
+
notify?: string;
|
|
114
145
|
}
|
|
115
146
|
interface ReactiveRoutesOptions {
|
|
116
147
|
routes: ReactiveRoutes;
|
|
117
148
|
/** Ticket signing secret. Requests get 503 when it is not configured. */
|
|
118
149
|
secret: string | undefined;
|
|
150
|
+
/** Bearer secret for `routes.notify`; the route answers 503 while it is unset. */
|
|
151
|
+
notifySecret?: string | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Browser origins allowed to call the ticket and reactive routes cross-origin,
|
|
154
|
+
* for a hub on a different host from the app.
|
|
155
|
+
*/
|
|
156
|
+
allowedOrigins?: readonly string[];
|
|
119
157
|
/** The hub Durable Object stub that serves these routes. */
|
|
120
158
|
hub: () => HubStub;
|
|
121
159
|
/** Returns the principal for a ticket request, or undefined to answer 401. */
|
|
122
160
|
authenticate(request: Request): string | undefined | Promise<string | undefined>;
|
|
123
161
|
ticketLifetimeSeconds?: number;
|
|
124
162
|
}
|
|
125
|
-
/** Serves the ticket, socket and
|
|
163
|
+
/** Serves the ticket, socket, reactive and notify routes; `undefined` for any other path. */
|
|
126
164
|
declare function handleReactiveRoutes(request: Request, options: ReactiveRoutesOptions): Promise<Response | undefined>;
|
|
127
165
|
|
|
128
166
|
export { HUB_PRINCIPAL_HEADER, createDurableObjectHub, handleReactiveRoutes, hubClient };
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ interface DurableObjectStateLike {
|
|
|
25
25
|
storage: {
|
|
26
26
|
get<T>(key: string): Promise<T | undefined>;
|
|
27
27
|
put<T>(key: string, value: T): Promise<void>;
|
|
28
|
+
/** Lets presence forget principals that left instead of storing `false`. */
|
|
29
|
+
delete?(key: string): Promise<boolean>;
|
|
28
30
|
/** Needed for presence. */
|
|
29
31
|
getAlarm?(): Promise<number | null>;
|
|
30
32
|
/** Needed for presence. */
|
|
@@ -39,6 +41,10 @@ type WebSocketPairConstructor = new () => {
|
|
|
39
41
|
0: DurableObjectWebSocket;
|
|
40
42
|
1: DurableObjectWebSocket;
|
|
41
43
|
};
|
|
44
|
+
/** A frame from the client that is not part of the protocol, such as `{ type: 'listening' }`. */
|
|
45
|
+
type HubClientFrame = Record<string, unknown> & {
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
42
48
|
interface DurableObjectHubOptions {
|
|
43
49
|
paths: HubPaths;
|
|
44
50
|
/** Storage key for a path's version. Defaults to `version:<path>`. */
|
|
@@ -50,17 +56,27 @@ interface DurableObjectHubOptions {
|
|
|
50
56
|
*/
|
|
51
57
|
autoPong?: boolean;
|
|
52
58
|
/**
|
|
53
|
-
* Tracks whether
|
|
54
|
-
* worker as online without
|
|
59
|
+
* Tracks whether principals hold a live socket, so the hub can report a
|
|
60
|
+
* worker or listener as online without it polling over HTTP. Requires `autoPong`.
|
|
55
61
|
*/
|
|
56
62
|
presence?: HubPresenceOptions;
|
|
63
|
+
/** Runs after a socket is accepted. */
|
|
64
|
+
onOpen?: (socket: DurableObjectWebSocket, principal: string) => void | Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Runs when a socket closes or errors. The socket may still be listed by
|
|
67
|
+
* `sockets()` during this call, so exclude it when counting.
|
|
68
|
+
*/
|
|
69
|
+
onClose?: (socket: DurableObjectWebSocket, principal: string) => void | Promise<void>;
|
|
70
|
+
/** Handles frames the protocol does not, such as application presence updates. */
|
|
71
|
+
onMessage?: (socket: DurableObjectWebSocket, principal: string, frame: HubClientFrame) => void | Promise<void>;
|
|
57
72
|
/** Workers' `WebSocketPair`; injectable for tests. */
|
|
58
73
|
WebSocketPair?: WebSocketPairConstructor;
|
|
59
74
|
/** Creates a connection id; injectable for tests. */
|
|
60
75
|
randomId?: () => string;
|
|
61
76
|
}
|
|
62
77
|
interface HubPresenceOptions {
|
|
63
|
-
principals
|
|
78
|
+
/** The principals to track, or a predicate for open-ended ones such as anonymous listeners. */
|
|
79
|
+
principals: readonly string[] | ((principal: string) => boolean);
|
|
64
80
|
/**
|
|
65
81
|
* A socket whose last `ping` is older than this is closed as dead, which is how
|
|
66
82
|
* a peer that vanished without a close frame is noticed. Clients must ping more
|
|
@@ -77,7 +93,12 @@ interface HubPresenceOptions {
|
|
|
77
93
|
/** Wires the hub core to a Durable Object's hibernatable WebSockets and storage. */
|
|
78
94
|
declare function createDurableObjectHub(ctx: DurableObjectStateLike, options: DurableObjectHubOptions): {
|
|
79
95
|
hub: {
|
|
80
|
-
message(socket: HubSocket, raw: string):
|
|
96
|
+
message(socket: HubSocket, raw: string): (Record<string, unknown> & {
|
|
97
|
+
type: string;
|
|
98
|
+
}) | undefined;
|
|
99
|
+
socketData<T = unknown>(socket: HubSocket): T | undefined;
|
|
100
|
+
setSocketData(socket: HubSocket, data: unknown): void;
|
|
101
|
+
principalOf(socket: HubSocket): string;
|
|
81
102
|
subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse>;
|
|
82
103
|
unsubscribe(request: UnsubscribeRequest, principal: string): void;
|
|
83
104
|
notify(path: string): Promise<void>;
|
|
@@ -85,9 +106,17 @@ declare function createDurableObjectHub(ctx: DurableObjectStateLike, options: Du
|
|
|
85
106
|
};
|
|
86
107
|
/** The Durable Object's `fetch`: WebSocket upgrades and actions from `hubClient`. */
|
|
87
108
|
fetch(request: Request): Promise<Response>;
|
|
88
|
-
webSocketMessage(socket: DurableObjectWebSocket, message: string | ArrayBuffer): void
|
|
109
|
+
webSocketMessage(socket: DurableObjectWebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
89
110
|
webSocketClose(socket: DurableObjectWebSocket, code: number): Promise<void>;
|
|
90
111
|
webSocketError(socket: DurableObjectWebSocket): Promise<void>;
|
|
112
|
+
/** Every accepted socket, including one whose close is being handled. */
|
|
113
|
+
sockets(): DurableObjectWebSocket[];
|
|
114
|
+
/** The principal a socket authenticated as. */
|
|
115
|
+
principalOf: (socket: DurableObjectWebSocket) => string;
|
|
116
|
+
/** Application data stored with a socket by `setSocketData`. */
|
|
117
|
+
socketData: <T = unknown>(socket: DurableObjectWebSocket) => T | undefined;
|
|
118
|
+
/** Stores application data with a socket; it survives hibernation and shares the 2 KB limit. */
|
|
119
|
+
setSocketData: (socket: DurableObjectWebSocket, data: unknown) => void;
|
|
91
120
|
/** Whether `principal` holds a socket that has pinged within the presence timeout. */
|
|
92
121
|
present(principal: string): boolean;
|
|
93
122
|
/**
|
|
@@ -111,18 +140,27 @@ interface ReactiveRoutes {
|
|
|
111
140
|
socket: string;
|
|
112
141
|
/** Reactive HTTP subscribe and unsubscribe, authenticated by the ticket as a bearer token. */
|
|
113
142
|
reactive: string;
|
|
143
|
+
/** `POST { path, patches? }` from other backends, authenticated by `notifySecret`. */
|
|
144
|
+
notify?: string;
|
|
114
145
|
}
|
|
115
146
|
interface ReactiveRoutesOptions {
|
|
116
147
|
routes: ReactiveRoutes;
|
|
117
148
|
/** Ticket signing secret. Requests get 503 when it is not configured. */
|
|
118
149
|
secret: string | undefined;
|
|
150
|
+
/** Bearer secret for `routes.notify`; the route answers 503 while it is unset. */
|
|
151
|
+
notifySecret?: string | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Browser origins allowed to call the ticket and reactive routes cross-origin,
|
|
154
|
+
* for a hub on a different host from the app.
|
|
155
|
+
*/
|
|
156
|
+
allowedOrigins?: readonly string[];
|
|
119
157
|
/** The hub Durable Object stub that serves these routes. */
|
|
120
158
|
hub: () => HubStub;
|
|
121
159
|
/** Returns the principal for a ticket request, or undefined to answer 401. */
|
|
122
160
|
authenticate(request: Request): string | undefined | Promise<string | undefined>;
|
|
123
161
|
ticketLifetimeSeconds?: number;
|
|
124
162
|
}
|
|
125
|
-
/** Serves the ticket, socket and
|
|
163
|
+
/** Serves the ticket, socket, reactive and notify routes; `undefined` for any other path. */
|
|
126
164
|
declare function handleReactiveRoutes(request: Request, options: ReactiveRoutesOptions): Promise<Response | undefined>;
|
|
127
165
|
|
|
128
166
|
export { HUB_PRINCIPAL_HEADER, createDurableObjectHub, handleReactiveRoutes, hubClient };
|
package/dist/cloudflare.js
CHANGED
|
@@ -129,20 +129,42 @@ function createReactiveHub(options) {
|
|
|
129
129
|
return { fromVersion, toVersion };
|
|
130
130
|
};
|
|
131
131
|
return {
|
|
132
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Answers the client's `init` and `ping` frames. Any other frame with a
|
|
134
|
+
* string `type` is returned for the application to handle.
|
|
135
|
+
*/
|
|
133
136
|
message(socket, raw) {
|
|
134
137
|
let frame;
|
|
135
138
|
try {
|
|
136
139
|
frame = JSON.parse(raw);
|
|
137
140
|
} catch {
|
|
138
|
-
return;
|
|
141
|
+
return void 0;
|
|
139
142
|
}
|
|
140
|
-
|
|
141
|
-
if (type === "init") {
|
|
143
|
+
if (!isRecord2(frame) || typeof frame.type !== "string") return void 0;
|
|
144
|
+
if (frame.type === "init") {
|
|
142
145
|
send(socket, { type: "connected", connectionId: socketState(socket).connectionId });
|
|
143
|
-
} else if (type === "ping") {
|
|
146
|
+
} else if (frame.type === "ping") {
|
|
144
147
|
send(socket, { type: "pong" });
|
|
148
|
+
} else {
|
|
149
|
+
return frame;
|
|
150
|
+
}
|
|
151
|
+
return void 0;
|
|
152
|
+
},
|
|
153
|
+
/** The application data stored with a socket. */
|
|
154
|
+
socketData(socket) {
|
|
155
|
+
return socketState(socket).data;
|
|
156
|
+
},
|
|
157
|
+
/** Stores application data with a socket; it shares the attachment size limit. */
|
|
158
|
+
setSocketData(socket, data) {
|
|
159
|
+
const next = { ...socketState(socket), data };
|
|
160
|
+
if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {
|
|
161
|
+
throw new ReactiveHubError("Socket data is too large");
|
|
145
162
|
}
|
|
163
|
+
socket.serializeAttachment(next);
|
|
164
|
+
},
|
|
165
|
+
/** The principal a socket authenticated as. */
|
|
166
|
+
principalOf(socket) {
|
|
167
|
+
return socketState(socket).principal;
|
|
146
168
|
},
|
|
147
169
|
async subscribe(request, principal) {
|
|
148
170
|
const handler = resolve(request.path);
|
|
@@ -231,6 +253,7 @@ function createReactiveHub(options) {
|
|
|
231
253
|
// ../client/src/cloudflare.ts
|
|
232
254
|
var HUB_PRINCIPAL_HEADER = "x-reactive-principal";
|
|
233
255
|
var HUB_ACTION_URL = "https://reactive-hub/";
|
|
256
|
+
var PRESENCE_TAG = "presence";
|
|
234
257
|
var PRESENCE_TAG_PREFIX = "presence:";
|
|
235
258
|
var PRESENCE_KEY_PREFIX = "presence:";
|
|
236
259
|
var DEFAULT_PRESENCE_TIMEOUT_MS = 9e4;
|
|
@@ -268,8 +291,14 @@ function createDurableObjectHub(ctx, options) {
|
|
|
268
291
|
);
|
|
269
292
|
}
|
|
270
293
|
}
|
|
271
|
-
const
|
|
272
|
-
const timeoutMs =
|
|
294
|
+
const presence = options.presence;
|
|
295
|
+
const timeoutMs = presence?.timeoutMs ?? DEFAULT_PRESENCE_TIMEOUT_MS;
|
|
296
|
+
const isTracked = (principal) => {
|
|
297
|
+
if (!presence) return false;
|
|
298
|
+
const { principals } = presence;
|
|
299
|
+
return typeof principals === "function" ? principals(principal) : principals.includes(principal);
|
|
300
|
+
};
|
|
301
|
+
const principalOf = (socket) => socket.deserializeAttachment().principal;
|
|
273
302
|
const lastSeen = (socket) => Math.max(
|
|
274
303
|
ctx.getWebSocketAutoResponseTimestamp?.(socket)?.getTime() ?? 0,
|
|
275
304
|
socket.deserializeAttachment().connectedAt ?? 0
|
|
@@ -282,26 +311,25 @@ function createDurableObjectHub(ctx, options) {
|
|
|
282
311
|
const present = liveSockets(principal, closing).length > 0;
|
|
283
312
|
const key = PRESENCE_KEY_PREFIX + principal;
|
|
284
313
|
if ((await ctx.storage.get(key) ?? false) === present) return;
|
|
285
|
-
await ctx.storage.put(key, present);
|
|
286
|
-
await
|
|
314
|
+
if (present || !ctx.storage.delete) await ctx.storage.put(key, present);
|
|
315
|
+
else await ctx.storage.delete(key);
|
|
316
|
+
await presence?.onChange?.(principal, present);
|
|
287
317
|
};
|
|
288
318
|
const scheduleSweep = async () => {
|
|
289
319
|
const { storage } = ctx;
|
|
290
320
|
if (!storage.getAlarm || !storage.setAlarm) {
|
|
291
321
|
throw new Error("Hub presence needs Durable Object alarms");
|
|
292
322
|
}
|
|
293
|
-
const seen =
|
|
294
|
-
(principal) => ctx.getWebSockets(PRESENCE_TAG_PREFIX + principal).map((socket) => lastSeen(socket))
|
|
295
|
-
);
|
|
323
|
+
const seen = ctx.getWebSockets(PRESENCE_TAG).map((socket) => lastSeen(socket));
|
|
296
324
|
if (seen.length === 0) return;
|
|
297
325
|
const due = Math.min(...seen) + timeoutMs + 1;
|
|
298
326
|
const current = await storage.getAlarm();
|
|
299
327
|
if (current === null || current > due) await storage.setAlarm(due);
|
|
300
328
|
};
|
|
301
329
|
const closed = async (socket) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
const principal = principalOf(socket);
|
|
331
|
+
if (isTracked(principal)) await reconcile(principal, socket);
|
|
332
|
+
await options.onClose?.(socket, principal);
|
|
305
333
|
};
|
|
306
334
|
const accept = async (request) => {
|
|
307
335
|
const principal = request.headers.get(HUB_PRINCIPAL_HEADER);
|
|
@@ -310,18 +338,19 @@ function createDurableObjectHub(ctx, options) {
|
|
|
310
338
|
if (!Pair) throw new Error("WebSocketPair is not available in this runtime");
|
|
311
339
|
const pair = new Pair();
|
|
312
340
|
const connectionId = randomId();
|
|
313
|
-
const
|
|
341
|
+
const tracked = isTracked(principal);
|
|
314
342
|
ctx.acceptWebSocket(
|
|
315
343
|
pair[1],
|
|
316
|
-
|
|
344
|
+
tracked ? [connectionId, PRESENCE_TAG, PRESENCE_TAG_PREFIX + principal] : [connectionId]
|
|
317
345
|
);
|
|
318
346
|
pair[1].serializeAttachment(
|
|
319
|
-
createHubSocketState(connectionId, principal,
|
|
347
|
+
createHubSocketState(connectionId, principal, tracked ? Date.now() : void 0)
|
|
320
348
|
);
|
|
321
|
-
if (
|
|
349
|
+
if (tracked) {
|
|
322
350
|
await reconcile(principal);
|
|
323
351
|
await scheduleSweep();
|
|
324
352
|
}
|
|
353
|
+
await options.onOpen?.(pair[1], principal);
|
|
325
354
|
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
326
355
|
};
|
|
327
356
|
const act = async (action) => {
|
|
@@ -353,8 +382,10 @@ function createDurableObjectHub(ctx, options) {
|
|
|
353
382
|
if (request.headers.get("upgrade") === "websocket") return accept(request);
|
|
354
383
|
return act(await request.json());
|
|
355
384
|
},
|
|
356
|
-
webSocketMessage(socket, message) {
|
|
357
|
-
if (typeof message
|
|
385
|
+
async webSocketMessage(socket, message) {
|
|
386
|
+
if (typeof message !== "string") return;
|
|
387
|
+
const frame = hub.message(socket, message);
|
|
388
|
+
if (frame && options.onMessage) await options.onMessage(socket, principalOf(socket), frame);
|
|
358
389
|
},
|
|
359
390
|
async webSocketClose(socket, code) {
|
|
360
391
|
try {
|
|
@@ -370,6 +401,16 @@ function createDurableObjectHub(ctx, options) {
|
|
|
370
401
|
}
|
|
371
402
|
await closed(socket);
|
|
372
403
|
},
|
|
404
|
+
/** Every accepted socket, including one whose close is being handled. */
|
|
405
|
+
sockets() {
|
|
406
|
+
return ctx.getWebSockets();
|
|
407
|
+
},
|
|
408
|
+
/** The principal a socket authenticated as. */
|
|
409
|
+
principalOf,
|
|
410
|
+
/** Application data stored with a socket by `setSocketData`. */
|
|
411
|
+
socketData: (socket) => hub.socketData(socket),
|
|
412
|
+
/** Stores application data with a socket; it survives hibernation and shares the 2 KB limit. */
|
|
413
|
+
setSocketData: (socket, data) => hub.setSocketData(socket, data),
|
|
373
414
|
/** Whether `principal` holds a socket that has pinged within the presence timeout. */
|
|
374
415
|
present(principal) {
|
|
375
416
|
return liveSockets(principal).length > 0;
|
|
@@ -380,14 +421,18 @@ function createDurableObjectHub(ctx, options) {
|
|
|
380
421
|
*/
|
|
381
422
|
async alarm() {
|
|
382
423
|
const now = Date.now();
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
424
|
+
const principals = /* @__PURE__ */ new Set();
|
|
425
|
+
for (const socket of ctx.getWebSockets(PRESENCE_TAG)) {
|
|
426
|
+
const principal = principalOf(socket);
|
|
427
|
+
principals.add(principal);
|
|
428
|
+
if (now - lastSeen(socket) <= timeoutMs) continue;
|
|
429
|
+
try {
|
|
430
|
+
socket.close(HEARTBEAT_TIMEOUT_CLOSE_CODE, "Heartbeat timeout");
|
|
431
|
+
} catch {
|
|
390
432
|
}
|
|
433
|
+
await options.onClose?.(socket, principal);
|
|
434
|
+
}
|
|
435
|
+
for (const principal of principals) {
|
|
391
436
|
await reconcile(principal);
|
|
392
437
|
}
|
|
393
438
|
await scheduleSweep();
|
|
@@ -408,24 +453,84 @@ function hubClient(stub) {
|
|
|
408
453
|
publish: async (path, patches) => ok(await post({ action: "publish", path, patches }))
|
|
409
454
|
};
|
|
410
455
|
}
|
|
456
|
+
function sameSecret(given, expected) {
|
|
457
|
+
const left = new TextEncoder().encode(given);
|
|
458
|
+
const right = new TextEncoder().encode(expected);
|
|
459
|
+
let difference = left.length ^ right.length;
|
|
460
|
+
for (let index = 0; index < right.length; index++) {
|
|
461
|
+
difference |= (left[index] ?? 0) ^ right[index];
|
|
462
|
+
}
|
|
463
|
+
return difference === 0;
|
|
464
|
+
}
|
|
465
|
+
function withCors(response, origin) {
|
|
466
|
+
if (!origin) return response;
|
|
467
|
+
const headers = new Headers(response.headers);
|
|
468
|
+
headers.set("access-control-allow-origin", origin);
|
|
469
|
+
headers.append("vary", "origin");
|
|
470
|
+
return new Response(response.body, {
|
|
471
|
+
status: response.status,
|
|
472
|
+
statusText: response.statusText,
|
|
473
|
+
headers
|
|
474
|
+
});
|
|
475
|
+
}
|
|
411
476
|
async function handleReactiveRoutes(request, options) {
|
|
412
477
|
const url = new URL(request.url);
|
|
413
478
|
const { routes, secret } = options;
|
|
414
|
-
|
|
415
|
-
if (!
|
|
416
|
-
|
|
479
|
+
const known = [routes.ticket, routes.socket, routes.reactive, routes.notify];
|
|
480
|
+
if (!known.includes(url.pathname)) return void 0;
|
|
481
|
+
const requestOrigin = request.headers.get("origin");
|
|
482
|
+
const corsOrigin = requestOrigin && options.allowedOrigins?.includes(requestOrigin) ? requestOrigin : null;
|
|
483
|
+
if (request.method === "OPTIONS" && url.pathname !== routes.socket) {
|
|
484
|
+
if (!corsOrigin) return new Response(null, { status: 403 });
|
|
485
|
+
return new Response(null, {
|
|
486
|
+
status: 204,
|
|
487
|
+
headers: {
|
|
488
|
+
"access-control-allow-origin": corsOrigin,
|
|
489
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
490
|
+
"access-control-allow-headers": "authorization, content-type",
|
|
491
|
+
"access-control-max-age": "86400",
|
|
492
|
+
vary: "origin"
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (url.pathname === routes.notify) {
|
|
417
497
|
if (request.method !== "POST") return error("Method not allowed", 405);
|
|
498
|
+
if (!options.notifySecret) return error("Hub notify is not configured", 503);
|
|
499
|
+
if (!sameSecret(bearerToken(request), options.notifySecret)) {
|
|
500
|
+
return error("Unauthorized", 401);
|
|
501
|
+
}
|
|
502
|
+
let body2;
|
|
503
|
+
try {
|
|
504
|
+
body2 = await request.json();
|
|
505
|
+
} catch {
|
|
506
|
+
return error("Invalid JSON", 400);
|
|
507
|
+
}
|
|
508
|
+
const { path, patches } = body2 ?? {};
|
|
509
|
+
if (typeof path !== "string" || patches !== void 0 && !Array.isArray(patches)) {
|
|
510
|
+
return error("Expected { path, patches? }", 400);
|
|
511
|
+
}
|
|
512
|
+
const client = hubClient(options.hub());
|
|
513
|
+
if (patches) await client.publish(path, patches);
|
|
514
|
+
else await client.notify(path);
|
|
515
|
+
return json({ type: "ok" });
|
|
516
|
+
}
|
|
517
|
+
if (!secret) return withCors(error("Reactive hub is not configured", 503), corsOrigin);
|
|
518
|
+
if (url.pathname === routes.ticket) {
|
|
519
|
+
if (request.method !== "POST") return withCors(error("Method not allowed", 405), corsOrigin);
|
|
418
520
|
const principal2 = await options.authenticate(request);
|
|
419
|
-
if (!principal2) return error("Unauthorized", 401);
|
|
521
|
+
if (!principal2) return withCors(error("Unauthorized", 401), corsOrigin);
|
|
420
522
|
const socketUrl = new URL(routes.socket, url);
|
|
421
523
|
socketUrl.protocol = "wss:";
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
524
|
+
return withCors(
|
|
525
|
+
json(
|
|
526
|
+
await createSignedClusterTicket({
|
|
527
|
+
secret,
|
|
528
|
+
principal: principal2,
|
|
529
|
+
url: socketUrl.toString(),
|
|
530
|
+
...options.ticketLifetimeSeconds === void 0 ? {} : { lifetimeSeconds: options.ticketLifetimeSeconds }
|
|
531
|
+
})
|
|
532
|
+
),
|
|
533
|
+
corsOrigin
|
|
429
534
|
);
|
|
430
535
|
}
|
|
431
536
|
if (url.pathname === routes.socket) {
|
|
@@ -438,17 +543,17 @@ async function handleReactiveRoutes(request, options) {
|
|
|
438
543
|
forwarded.headers.set(HUB_PRINCIPAL_HEADER, principal2);
|
|
439
544
|
return options.hub().fetch(forwarded);
|
|
440
545
|
}
|
|
441
|
-
if (request.method !== "POST") return error("Method not allowed", 405);
|
|
546
|
+
if (request.method !== "POST") return withCors(error("Method not allowed", 405), corsOrigin);
|
|
442
547
|
const principal = await verifySignedClusterTicket(bearerToken(request), secret);
|
|
443
|
-
if (!principal) return error("Unauthorized", 401);
|
|
548
|
+
if (!principal) return withCors(error("Unauthorized", 401), corsOrigin);
|
|
444
549
|
let body;
|
|
445
550
|
try {
|
|
446
551
|
body = await request.json();
|
|
447
552
|
} catch {
|
|
448
|
-
return error("Invalid JSON", 400);
|
|
553
|
+
return withCors(error("Invalid JSON", 400), corsOrigin);
|
|
449
554
|
}
|
|
450
|
-
if (!isHubRequest(body)) return error("Unsupported reactive request", 400);
|
|
451
|
-
return hubClient(options.hub()).request(body, principal);
|
|
555
|
+
if (!isHubRequest(body)) return withCors(error("Unsupported reactive request", 400), corsOrigin);
|
|
556
|
+
return withCors(await hubClient(options.hub()).request(body, principal), corsOrigin);
|
|
452
557
|
}
|
|
453
558
|
|
|
454
559
|
export { HUB_PRINCIPAL_HEADER, createDurableObjectHub, handleReactiveRoutes, hubClient };
|