@livestore/sync-cf 0.3.2-dev.9 → 0.4.0-dev.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/dist/.tsbuildinfo +1 -1
- package/dist/cf-worker/cf-types.d.ts +2 -0
- package/dist/cf-worker/cf-types.d.ts.map +1 -0
- package/dist/cf-worker/cf-types.js +2 -0
- package/dist/cf-worker/cf-types.js.map +1 -0
- package/dist/cf-worker/durable-object.d.ts +41 -3
- package/dist/cf-worker/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/durable-object.js +160 -127
- package/dist/cf-worker/durable-object.js.map +1 -1
- package/dist/cf-worker/mod.d.ts +1 -0
- package/dist/cf-worker/mod.d.ts.map +1 -1
- package/dist/cf-worker/mod.js +1 -0
- package/dist/cf-worker/mod.js.map +1 -1
- package/dist/cf-worker/worker.d.ts +28 -22
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +3 -2
- package/dist/cf-worker/worker.js.map +1 -1
- package/package.json +5 -6
- package/src/cf-worker/cf-types.ts +12 -0
- package/src/cf-worker/durable-object.ts +203 -153
- package/src/cf-worker/mod.ts +1 -0
- package/src/cf-worker/worker.ts +42 -35
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import type * as CfWorker from '@cloudflare/workers-types';
|
|
2
2
|
import type { Schema } from '@livestore/utils/effect';
|
|
3
3
|
import type { Env } from './durable-object.ts';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
4
|
+
export declare namespace HelperTypes {
|
|
5
|
+
type AnyDON = CfWorker.DurableObjectNamespace<undefined>;
|
|
6
|
+
type DOKeys<T> = {
|
|
7
|
+
[K in keyof T]-?: T[K] extends AnyDON ? K : never;
|
|
8
|
+
}[keyof T];
|
|
9
|
+
type NonBuiltins<T> = Omit<T, keyof Env>;
|
|
10
|
+
/**
|
|
11
|
+
* Helper type to extract DurableObject keys from Env to give consumer type safety.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* type PlatformEnv = {
|
|
16
|
+
* DB: D1Database
|
|
17
|
+
* ADMIN_TOKEN: string
|
|
18
|
+
* WEBSOCKET_SERVER: DurableObjectNamespace<WebSocketServer>
|
|
19
|
+
* }
|
|
20
|
+
* export default makeWorker<PlatformEnv>({
|
|
21
|
+
* durableObject: { name: "WEBSOCKET_SERVER" },
|
|
22
|
+
* // ^ (property) name?: "WEBSOCKET_SERVER" | undefined
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
export type ExtractDurableObjectKeys<TEnv = Env> = DOKeys<NonBuiltins<TEnv>> extends never ? string : DOKeys<NonBuiltins<TEnv>>;
|
|
26
|
+
export {};
|
|
27
|
+
}
|
|
22
28
|
export type CFWorker<TEnv extends Env = Env, _T extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined> = {
|
|
23
29
|
fetch: <CFHostMetada = unknown>(request: CfWorker.Request<CFHostMetada>, env: TEnv, ctx: CfWorker.ExecutionContext) => Promise<CfWorker.Response>;
|
|
24
30
|
};
|
|
@@ -39,7 +45,7 @@ export type MakeWorkerOptions<TEnv extends Env = Env> = {
|
|
|
39
45
|
*
|
|
40
46
|
* @default 'WEBSOCKET_SERVER'
|
|
41
47
|
*/
|
|
42
|
-
name?: ExtractDurableObjectKeys<TEnv>;
|
|
48
|
+
name?: HelperTypes.ExtractDurableObjectKeys<TEnv>;
|
|
43
49
|
};
|
|
44
50
|
};
|
|
45
51
|
export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined>(options?: MakeWorkerOptions<TEnv>) => CFWorker<TEnv, TDurableObjectRpc>;
|
|
@@ -61,19 +67,19 @@ export declare const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc exte
|
|
|
61
67
|
* return handleWebSocket(request, env, ctx, { headers: {}, validatePayload })
|
|
62
68
|
* }
|
|
63
69
|
*
|
|
64
|
-
* return new Response('Invalid path', { status: 400
|
|
70
|
+
* return new Response('Invalid path', { status: 400 })
|
|
71
|
+
* return new Response('Invalid path', { status: 400 })
|
|
65
72
|
* }
|
|
66
73
|
* }
|
|
67
74
|
* ```
|
|
68
75
|
*
|
|
69
76
|
* @throws {UnexpectedError} If the payload is invalid
|
|
70
77
|
*/
|
|
71
|
-
export declare const handleWebSocket: <TEnv extends Env = Env, TDurableObjectRpc extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown>(request: CfWorker.Request<CFHostMetada>, env: TEnv, _ctx: CfWorker.ExecutionContext, options
|
|
78
|
+
export declare const handleWebSocket: <TEnv extends Env = Env, TDurableObjectRpc extends CfWorker.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown>(request: CfWorker.Request<CFHostMetada>, env: TEnv, _ctx: CfWorker.ExecutionContext, options?: {
|
|
72
79
|
headers?: CfWorker.HeadersInit;
|
|
73
80
|
durableObject?: MakeWorkerOptions<TEnv>["durableObject"];
|
|
74
81
|
validatePayload?: (payload: Schema.JsonValue | undefined, context: {
|
|
75
82
|
storeId: string;
|
|
76
83
|
}) => void | Promise<void>;
|
|
77
84
|
}) => Promise<CfWorker.Response>;
|
|
78
|
-
export {};
|
|
79
85
|
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,2BAA2B,CAAA;AAE1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"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"}
|
package/dist/cf-worker/worker.js
CHANGED
|
@@ -62,14 +62,15 @@ export const makeWorker = (options = {}) => {
|
|
|
62
62
|
* return handleWebSocket(request, env, ctx, { headers: {}, validatePayload })
|
|
63
63
|
* }
|
|
64
64
|
*
|
|
65
|
-
* return new Response('Invalid path', { status: 400
|
|
65
|
+
* return new Response('Invalid path', { status: 400 })
|
|
66
|
+
* return new Response('Invalid path', { status: 400 })
|
|
66
67
|
* }
|
|
67
68
|
* }
|
|
68
69
|
* ```
|
|
69
70
|
*
|
|
70
71
|
* @throws {UnexpectedError} If the payload is invalid
|
|
71
72
|
*/
|
|
72
|
-
export const handleWebSocket = (request, env, _ctx, options) => Effect.gen(function* () {
|
|
73
|
+
export const handleWebSocket = (request, env, _ctx, options = {}) => Effect.gen(function* () {
|
|
73
74
|
const url = new URL(request.url);
|
|
74
75
|
const urlParams = UrlParams.fromInput(url.searchParams);
|
|
75
76
|
const paramsResult = yield* UrlParams.schemaStruct(SearchParamsSchema)(urlParams).pipe(Effect.either);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAE3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/cf-worker/worker.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/sync-cf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-dev.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./dist/sync-impl/mod.js",
|
|
8
|
+
"./common": "./dist/common/mod.js",
|
|
8
9
|
"./cf-worker": "./dist/cf-worker/mod.js",
|
|
9
10
|
"./cf-worker/durable-object": "./dist/cf-worker/durable-object.js",
|
|
10
11
|
"./cf-worker/worker": "./dist/cf-worker/worker.js"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"@
|
|
14
|
-
"@livestore/utils": "0.
|
|
15
|
-
|
|
16
|
-
"devDependencies": {
|
|
17
|
-
"@cloudflare/workers-types": "^4.20250702.0"
|
|
14
|
+
"@cloudflare/workers-types": "4.20250807.0",
|
|
15
|
+
"@livestore/utils": "0.4.0-dev.0",
|
|
16
|
+
"@livestore/common": "0.4.0-dev.0"
|
|
18
17
|
},
|
|
19
18
|
"files": [
|
|
20
19
|
"dist",
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { makeColumnSpec, UnexpectedError } from '@livestore/common'
|
|
1
|
+
import { UnexpectedError } from '@livestore/common'
|
|
3
2
|
import { EventSequenceNumber, type LiveStoreEvent, State } from '@livestore/common/schema'
|
|
4
3
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
5
4
|
import { Effect, Logger, LogLevel, Option, Schema, UrlParams } from '@livestore/utils/effect'
|
|
6
|
-
|
|
7
5
|
import { SearchParamsSchema, WSMessage } from '../common/mod.ts'
|
|
8
6
|
import type { SyncMetadata } from '../common/ws-message-types.ts'
|
|
7
|
+
import type * as CfWorker from './cf-types.ts'
|
|
8
|
+
|
|
9
|
+
// NOTE We need to redeclare runtime types here to avoid type conflicts with the lib.dom Response type.
|
|
10
|
+
declare class Response extends CfWorker.Response {}
|
|
11
|
+
declare class WebSocketPair extends CfWorker.WebSocketPair {}
|
|
12
|
+
declare class WebSocketRequestResponsePair extends CfWorker.WebSocketRequestResponsePair {}
|
|
9
13
|
|
|
10
14
|
export interface Env {
|
|
11
|
-
DB: D1Database
|
|
15
|
+
DB: CfWorker.D1Database
|
|
12
16
|
ADMIN_SECRET: string
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
type WebSocketClient = WebSocket
|
|
16
|
-
|
|
17
19
|
const encodeOutgoingMessage = Schema.encodeSync(Schema.parseJson(WSMessage.BackendToClientMessage))
|
|
18
20
|
const encodeIncomingMessage = Schema.encodeSync(Schema.parseJson(WSMessage.ClientToBackendMessage))
|
|
19
|
-
const decodeIncomingMessage = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.ClientToBackendMessage))
|
|
20
21
|
|
|
21
22
|
export const eventlogTable = State.SQLite.table({
|
|
22
23
|
// NOTE actual table name is determined at runtime
|
|
@@ -63,17 +64,50 @@ export type MakeDurableObjectClassOptions = {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export type MakeDurableObjectClass = (options?: MakeDurableObjectClassOptions) => {
|
|
66
|
-
new (ctx: DurableObjectState, env: Env): DurableObject
|
|
67
|
+
new (ctx: CfWorker.DurableObjectState, env: Env): CfWorker.DurableObject
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Creates a Durable Object class for handling WebSocket-based sync.
|
|
72
|
+
*
|
|
73
|
+
* Example:
|
|
74
|
+
* ```ts
|
|
75
|
+
* // In your Cloudflare Worker file
|
|
76
|
+
* import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
|
|
77
|
+
*
|
|
78
|
+
* export class WebSocketServer extends makeDurableObject({
|
|
79
|
+
* onPush: async (message) => {
|
|
80
|
+
* console.log('onPush', message.batch)
|
|
81
|
+
* },
|
|
82
|
+
* onPull: async (message) => {
|
|
83
|
+
* console.log('onPull', message)
|
|
84
|
+
* },
|
|
85
|
+
* }) {}
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* ```toml
|
|
89
|
+
* # wrangler.toml
|
|
90
|
+
* [new_classes]
|
|
91
|
+
* WebSocketServer = "src/websocket-server.ts"
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
69
94
|
export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
70
|
-
return class WebSocketServerBase
|
|
95
|
+
return class WebSocketServerBase implements CfWorker.DurableObject, CfWorker.Rpc.DurableObjectBranded {
|
|
96
|
+
__DURABLE_OBJECT_BRAND = 'WebSocketServerBase' as never
|
|
97
|
+
ctx: CfWorker.DurableObjectState
|
|
98
|
+
env: Env
|
|
99
|
+
|
|
100
|
+
constructor(ctx: CfWorker.DurableObjectState, env: Env) {
|
|
101
|
+
this.ctx = ctx
|
|
102
|
+
this.env = env
|
|
103
|
+
}
|
|
104
|
+
|
|
71
105
|
/** Needed to prevent concurrent pushes */
|
|
72
106
|
private pushSemaphore = Effect.makeSemaphore(1).pipe(Effect.runSync)
|
|
73
107
|
|
|
74
108
|
private currentHead: EventSequenceNumber.GlobalEventSequenceNumber | 'uninitialized' = 'uninitialized'
|
|
75
109
|
|
|
76
|
-
fetch = async (request: Request) =>
|
|
110
|
+
fetch = async (request: CfWorker.Request): Promise<CfWorker.Response> =>
|
|
77
111
|
Effect.sync(() => {
|
|
78
112
|
const { storeId, payload } = getRequestSearchParams(request)
|
|
79
113
|
const storage = makeStorage(this.ctx, this.env, storeId)
|
|
@@ -94,7 +128,7 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
94
128
|
),
|
|
95
129
|
)
|
|
96
130
|
|
|
97
|
-
const colSpec = makeColumnSpec(eventlogTable.sqliteDef.ast)
|
|
131
|
+
const colSpec = State.SQLite.makeColumnSpec(eventlogTable.sqliteDef.ast)
|
|
98
132
|
this.env.DB.exec(`CREATE TABLE IF NOT EXISTS ${storage.dbName} (${colSpec}) strict`)
|
|
99
133
|
|
|
100
134
|
return new Response(null, {
|
|
@@ -103,190 +137,201 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
103
137
|
})
|
|
104
138
|
}).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
|
|
105
139
|
|
|
106
|
-
webSocketMessage = (ws:
|
|
107
|
-
|
|
108
|
-
const decodedMessageRes = decodeIncomingMessage(message)
|
|
140
|
+
webSocketMessage = (ws: CfWorker.WebSocket, message: ArrayBuffer | string): Promise<void> | undefined => {
|
|
141
|
+
const decodedMessageRes = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.ClientToBackendMessage))(message)
|
|
109
142
|
|
|
110
143
|
if (decodedMessageRes._tag === 'Left') {
|
|
111
|
-
|
|
144
|
+
Effect.logError('Invalid message received', { message }).pipe(
|
|
145
|
+
Effect.provide(Logger.prettyWithThread('durable-object')),
|
|
146
|
+
Effect.runSync,
|
|
147
|
+
)
|
|
112
148
|
return
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
const decodedMessage = decodedMessageRes.right
|
|
152
|
+
|
|
116
153
|
const requestId = decodedMessage.requestId
|
|
117
154
|
|
|
118
155
|
return Effect.gen(this, function* () {
|
|
119
156
|
const { storeId, payload } = yield* Schema.decode(WebSocketAttachmentSchema)(ws.deserializeAttachment())
|
|
120
157
|
const storage = makeStorage(this.ctx, this.env, storeId)
|
|
121
158
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
159
|
+
switch (decodedMessage._tag) {
|
|
160
|
+
// TODO allow pulling concurrently to not block incoming push requests
|
|
161
|
+
case 'WSMessage.PullReq': {
|
|
162
|
+
if (options?.onPull) {
|
|
163
|
+
yield* Effect.tryAll(() => options.onPull!(decodedMessage, { storeId, payload }))
|
|
164
|
+
}
|
|
129
165
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
ws.send(encodeOutgoingMessage(message))
|
|
136
|
-
})
|
|
166
|
+
const respond = (message: WSMessage.PullRes) =>
|
|
167
|
+
Effect.gen(function* () {
|
|
168
|
+
if (options?.onPullRes) {
|
|
169
|
+
yield* Effect.tryAll(() => options.onPullRes!(message))
|
|
170
|
+
}
|
|
137
171
|
|
|
138
|
-
|
|
172
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
173
|
+
yield* Effect.logWarning('WebSocket not open, skipping send', {
|
|
174
|
+
readyState: ws.readyState,
|
|
175
|
+
message,
|
|
176
|
+
})
|
|
177
|
+
return
|
|
178
|
+
}
|
|
139
179
|
|
|
140
|
-
|
|
141
|
-
|
|
180
|
+
yield* Effect.try({
|
|
181
|
+
try: () => ws.send(encodeOutgoingMessage(message)),
|
|
182
|
+
catch: (cause) =>
|
|
183
|
+
new UnexpectedError({ cause, note: 'Failed to send pull response', payload: { message } }),
|
|
184
|
+
})
|
|
185
|
+
})
|
|
142
186
|
|
|
143
|
-
|
|
144
|
-
const batches =
|
|
145
|
-
remainingEvents.length === 0
|
|
146
|
-
? [[]]
|
|
147
|
-
: Array.from({ length: Math.ceil(remainingEvents.length / PULL_CHUNK_SIZE) }, (_, i) =>
|
|
148
|
-
remainingEvents.slice(i * PULL_CHUNK_SIZE, (i + 1) * PULL_CHUNK_SIZE),
|
|
149
|
-
)
|
|
187
|
+
const cursor = decodedMessage.cursor
|
|
150
188
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
yield* respond(WSMessage.PullRes.make({ batch, remaining, requestId: { context: 'pull', requestId } }))
|
|
154
|
-
}
|
|
189
|
+
// TODO use streaming
|
|
190
|
+
const remainingEvents = yield* storage.getEvents(cursor)
|
|
155
191
|
|
|
156
|
-
|
|
192
|
+
// Send at least one response, even if there are no events
|
|
193
|
+
const batches =
|
|
194
|
+
remainingEvents.length === 0
|
|
195
|
+
? [[]]
|
|
196
|
+
: Array.from({ length: Math.ceil(remainingEvents.length / PULL_CHUNK_SIZE) }, (_, i) =>
|
|
197
|
+
remainingEvents.slice(i * PULL_CHUNK_SIZE, (i + 1) * PULL_CHUNK_SIZE),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
for (const [index, batch] of batches.entries()) {
|
|
201
|
+
const remaining = Math.max(0, remainingEvents.length - (index + 1) * PULL_CHUNK_SIZE)
|
|
202
|
+
yield* respond(WSMessage.PullRes.make({ batch, remaining, requestId: { context: 'pull', requestId } }))
|
|
157
203
|
}
|
|
158
|
-
case 'WSMessage.PushReq': {
|
|
159
|
-
const respond = (message: WSMessage.PushAck | WSMessage.Error) =>
|
|
160
|
-
Effect.gen(function* () {
|
|
161
|
-
if (options?.onPushRes) {
|
|
162
|
-
yield* Effect.tryAll(() => options.onPushRes!(message))
|
|
163
|
-
}
|
|
164
|
-
ws.send(encodeOutgoingMessage(message))
|
|
165
|
-
})
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'WSMessage.PushReq': {
|
|
208
|
+
const respond = (message: WSMessage.PushAck | WSMessage.Error) =>
|
|
209
|
+
Effect.gen(function* () {
|
|
210
|
+
if (options?.onPushRes) {
|
|
211
|
+
yield* Effect.tryAll(() => options.onPushRes!(message))
|
|
212
|
+
}
|
|
213
|
+
ws.send(encodeOutgoingMessage(message))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (decodedMessage.batch.length === 0) {
|
|
217
|
+
yield* respond(WSMessage.PushAck.make({ requestId }))
|
|
218
|
+
return
|
|
219
|
+
}
|
|
171
220
|
|
|
172
|
-
|
|
221
|
+
yield* this.pushSemaphore.take(1)
|
|
173
222
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
223
|
+
if (options?.onPush) {
|
|
224
|
+
yield* Effect.tryAll(() => options.onPush!(decodedMessage, { storeId, payload }))
|
|
225
|
+
}
|
|
177
226
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
currentHead = EventSequenceNumber.ROOT.global
|
|
190
|
-
} else {
|
|
191
|
-
currentHead = currentHeadFromStorage as EventSequenceNumber.GlobalEventSequenceNumber
|
|
192
|
-
}
|
|
227
|
+
// TODO check whether we could use the Durable Object storage for this to speed up the lookup
|
|
228
|
+
// const expectedParentNum = yield* storage.getHead
|
|
229
|
+
let currentHead: EventSequenceNumber.GlobalEventSequenceNumber
|
|
230
|
+
if (this.currentHead === 'uninitialized') {
|
|
231
|
+
const currentHeadFromStorage = yield* Effect.promise(() => this.ctx.storage.get('currentHead'))
|
|
232
|
+
// console.log('currentHeadFromStorage', currentHeadFromStorage)
|
|
233
|
+
if (currentHeadFromStorage === undefined) {
|
|
234
|
+
// console.log('currentHeadFromStorage is null, getting from D1')
|
|
235
|
+
// currentHead = yield* storage.getHead
|
|
236
|
+
// console.log('currentHeadFromStorage is null, using root')
|
|
237
|
+
currentHead = EventSequenceNumber.ROOT.global
|
|
193
238
|
} else {
|
|
194
|
-
|
|
195
|
-
currentHead = this.currentHead
|
|
239
|
+
currentHead = currentHeadFromStorage as EventSequenceNumber.GlobalEventSequenceNumber
|
|
196
240
|
}
|
|
241
|
+
} else {
|
|
242
|
+
// console.log('currentHead is already initialized', this.currentHead)
|
|
243
|
+
currentHead = this.currentHead
|
|
244
|
+
}
|
|
197
245
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
246
|
+
// TODO handle clientId unique conflict
|
|
247
|
+
// Validate the batch
|
|
248
|
+
const firstEvent = decodedMessage.batch[0]!
|
|
249
|
+
if (firstEvent.parentSeqNum !== currentHead) {
|
|
250
|
+
const err = WSMessage.Error.make({
|
|
251
|
+
message: `Invalid parent event number. Received e${firstEvent.parentSeqNum} but expected e${currentHead}`,
|
|
252
|
+
requestId,
|
|
253
|
+
})
|
|
206
254
|
|
|
207
|
-
|
|
255
|
+
yield* Effect.logError(err)
|
|
208
256
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
257
|
+
yield* respond(err)
|
|
258
|
+
yield* this.pushSemaphore.release(1)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
213
261
|
|
|
214
|
-
|
|
262
|
+
yield* respond(WSMessage.PushAck.make({ requestId }))
|
|
215
263
|
|
|
216
|
-
|
|
264
|
+
const createdAt = new Date().toISOString()
|
|
217
265
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
266
|
+
// NOTE we're not waiting for this to complete yet to allow the broadcast to happen right away
|
|
267
|
+
// while letting the async storage write happen in the background
|
|
268
|
+
const storeFiber = yield* storage.appendEvents(decodedMessage.batch, createdAt).pipe(Effect.fork)
|
|
221
269
|
|
|
222
|
-
|
|
223
|
-
|
|
270
|
+
this.currentHead = decodedMessage.batch.at(-1)!.seqNum
|
|
271
|
+
yield* Effect.promise(() => this.ctx.storage.put('currentHead', this.currentHead))
|
|
224
272
|
|
|
225
|
-
|
|
273
|
+
yield* this.pushSemaphore.release(1)
|
|
226
274
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// console.debug(`Broadcasting push batch to ${this.subscribedWebSockets.size} clients`)
|
|
230
|
-
if (connectedClients.length > 0) {
|
|
231
|
-
// TODO refactor to batch api
|
|
232
|
-
const pullRes = WSMessage.PullRes.make({
|
|
233
|
-
batch: decodedMessage.batch.map((eventEncoded) => ({
|
|
234
|
-
eventEncoded,
|
|
235
|
-
metadata: Option.some({ createdAt }),
|
|
236
|
-
})),
|
|
237
|
-
remaining: 0,
|
|
238
|
-
requestId: { context: 'push', requestId },
|
|
239
|
-
})
|
|
240
|
-
const pullResEnc = encodeOutgoingMessage(pullRes)
|
|
275
|
+
const connectedClients = this.ctx.getWebSockets()
|
|
241
276
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
277
|
+
// console.debug(`Broadcasting push batch to ${this.subscribedWebSockets.size} clients`)
|
|
278
|
+
if (connectedClients.length > 0) {
|
|
279
|
+
// TODO refactor to batch api
|
|
280
|
+
const pullRes = WSMessage.PullRes.make({
|
|
281
|
+
batch: decodedMessage.batch.map((eventEncoded) => ({
|
|
282
|
+
eventEncoded,
|
|
283
|
+
metadata: Option.some({ createdAt }),
|
|
284
|
+
})),
|
|
285
|
+
remaining: 0,
|
|
286
|
+
requestId: { context: 'push', requestId },
|
|
287
|
+
})
|
|
288
|
+
const pullResEnc = encodeOutgoingMessage(pullRes)
|
|
246
289
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
290
|
+
// Only calling once for now.
|
|
291
|
+
if (options?.onPullRes) {
|
|
292
|
+
yield* Effect.tryAll(() => options.onPullRes!(pullRes))
|
|
251
293
|
}
|
|
252
294
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
break
|
|
257
|
-
}
|
|
258
|
-
case 'WSMessage.AdminResetRoomReq': {
|
|
259
|
-
if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
|
|
260
|
-
ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
|
|
261
|
-
return
|
|
295
|
+
// NOTE we're also sending the pullRes to the pushing ws client as a confirmation
|
|
296
|
+
for (const conn of connectedClients) {
|
|
297
|
+
conn.send(pullResEnc)
|
|
262
298
|
}
|
|
299
|
+
}
|
|
263
300
|
|
|
264
|
-
|
|
265
|
-
|
|
301
|
+
// Wait for the storage write to complete before finishing this request
|
|
302
|
+
yield* storeFiber
|
|
266
303
|
|
|
267
|
-
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
case 'WSMessage.AdminResetRoomReq': {
|
|
307
|
+
if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
|
|
308
|
+
ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
|
|
309
|
+
return
|
|
268
310
|
}
|
|
269
|
-
case 'WSMessage.AdminInfoReq': {
|
|
270
|
-
if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
|
|
271
|
-
ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
311
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
|
|
278
|
-
),
|
|
279
|
-
)
|
|
312
|
+
yield* storage.resetStore
|
|
313
|
+
ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
|
|
280
314
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
case 'WSMessage.AdminInfoReq': {
|
|
318
|
+
if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
|
|
319
|
+
ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
|
|
320
|
+
return
|
|
286
321
|
}
|
|
322
|
+
|
|
323
|
+
ws.send(
|
|
324
|
+
encodeOutgoingMessage(
|
|
325
|
+
WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
default: {
|
|
332
|
+
console.error('unsupported message', decodedMessage)
|
|
333
|
+
return shouldNeverHappen()
|
|
287
334
|
}
|
|
288
|
-
} catch (error: any) {
|
|
289
|
-
ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: error.message, requestId })))
|
|
290
335
|
}
|
|
291
336
|
}).pipe(
|
|
292
337
|
Effect.withSpan(`@livestore/sync-cf:durable-object:webSocketMessage:${decodedMessage._tag}`, {
|
|
@@ -304,7 +349,12 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
304
349
|
)
|
|
305
350
|
}
|
|
306
351
|
|
|
307
|
-
webSocketClose = async (
|
|
352
|
+
webSocketClose = async (
|
|
353
|
+
ws: CfWorker.WebSocket,
|
|
354
|
+
code: number,
|
|
355
|
+
_reason: string,
|
|
356
|
+
_wasClean: boolean,
|
|
357
|
+
): Promise<void> => {
|
|
308
358
|
// If the client closes the connection, the runtime will invoke the webSocketClose() handler.
|
|
309
359
|
ws.close(code, 'Durable Object is closing WebSocket')
|
|
310
360
|
}
|
|
@@ -327,10 +377,10 @@ type SyncStorage = {
|
|
|
327
377
|
resetStore: Effect.Effect<void, UnexpectedError>
|
|
328
378
|
}
|
|
329
379
|
|
|
330
|
-
const makeStorage = (ctx:
|
|
380
|
+
const makeStorage = (ctx: any, env: Env, storeId: string): SyncStorage => {
|
|
331
381
|
const dbName = `eventlog_${PERSISTENCE_FORMAT_VERSION}_${toValidTableName(storeId)}`
|
|
332
382
|
|
|
333
|
-
const execDb = <T>(cb: (db: D1Database) => Promise<D1Result<T>>) =>
|
|
383
|
+
const execDb = <T>(cb: (db: CfWorker.D1Database) => Promise<CfWorker.D1Result<T>>) =>
|
|
334
384
|
Effect.tryPromise({
|
|
335
385
|
try: () => cb(env.DB),
|
|
336
386
|
catch: (error) => new UnexpectedError({ cause: error, payload: { dbName } }),
|
|
@@ -418,7 +468,7 @@ const makeStorage = (ctx: DurableObjectState, env: Env, storeId: string): SyncSt
|
|
|
418
468
|
}
|
|
419
469
|
}
|
|
420
470
|
|
|
421
|
-
const getRequestSearchParams = (request: Request) => {
|
|
471
|
+
const getRequestSearchParams = (request: CfWorker.Request) => {
|
|
422
472
|
const url = new URL(request.url)
|
|
423
473
|
const urlParams = UrlParams.fromInput(url.searchParams)
|
|
424
474
|
const paramsResult = UrlParams.schemaStruct(SearchParamsSchema)(urlParams).pipe(Effect.runSync)
|
package/src/cf-worker/mod.ts
CHANGED