@livestore/sync-cf 0.4.0 → 0.5.0-dev.1
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 +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/do/durable-object.js +53 -36
- package/dist/cf-worker/do/durable-object.js.map +1 -1
- package/dist/cf-worker/do/layer.d.ts +22 -18
- package/dist/cf-worker/do/layer.d.ts.map +1 -1
- package/dist/cf-worker/do/layer.js +45 -35
- package/dist/cf-worker/do/layer.js.map +1 -1
- package/dist/cf-worker/do/observability.d.ts +23 -0
- package/dist/cf-worker/do/observability.d.ts.map +1 -0
- package/dist/cf-worker/do/observability.js +52 -0
- package/dist/cf-worker/do/observability.js.map +1 -0
- package/dist/cf-worker/do/observability.test.d.ts +2 -0
- package/dist/cf-worker/do/observability.test.d.ts.map +1 -0
- package/dist/cf-worker/do/observability.test.js +204 -0
- package/dist/cf-worker/do/observability.test.js.map +1 -0
- package/dist/cf-worker/do/pull.d.ts +3 -3
- package/dist/cf-worker/do/pull.d.ts.map +1 -1
- package/dist/cf-worker/do/pull.js +19 -20
- package/dist/cf-worker/do/pull.js.map +1 -1
- package/dist/cf-worker/do/push.d.ts +16 -3
- package/dist/cf-worker/do/push.d.ts.map +1 -1
- package/dist/cf-worker/do/push.js +74 -53
- package/dist/cf-worker/do/push.js.map +1 -1
- package/dist/cf-worker/do/push.test.d.ts +2 -0
- package/dist/cf-worker/do/push.test.d.ts.map +1 -0
- package/dist/cf-worker/do/push.test.js +30 -0
- package/dist/cf-worker/do/push.test.js.map +1 -0
- package/dist/cf-worker/do/sqlite.d.ts +34 -46
- package/dist/cf-worker/do/sqlite.d.ts.map +1 -1
- package/dist/cf-worker/do/sqlite.js +2 -2
- package/dist/cf-worker/do/sqlite.js.map +1 -1
- package/dist/cf-worker/do/sync-storage.d.ts +7 -6
- package/dist/cf-worker/do/sync-storage.d.ts.map +1 -1
- package/dist/cf-worker/do/sync-storage.js +29 -17
- package/dist/cf-worker/do/sync-storage.js.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts +17 -2
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.js +41 -23
- package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.test.d.ts +2 -0
- package/dist/cf-worker/do/transport/do-rpc-server.test.d.ts.map +1 -0
- package/dist/cf-worker/do/transport/do-rpc-server.test.js +28 -0
- package/dist/cf-worker/do/transport/do-rpc-server.test.js.map +1 -0
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts +2 -2
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.js +12 -12
- package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +9 -2
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.js +37 -29
- package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
- package/dist/cf-worker/mod.js +3 -3
- package/dist/cf-worker/shared.d.ts +37 -29
- package/dist/cf-worker/shared.d.ts.map +1 -1
- package/dist/cf-worker/shared.js +14 -12
- package/dist/cf-worker/shared.js.map +1 -1
- package/dist/cf-worker/worker.d.ts +9 -9
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +17 -15
- package/dist/cf-worker/worker.js.map +1 -1
- package/dist/client/mod.js +3 -3
- package/dist/client/transport/do-rpc-client.d.ts +18 -6
- package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/do-rpc-client.js +112 -38
- package/dist/client/transport/do-rpc-client.js.map +1 -1
- package/dist/client/transport/do-rpc-client.test.d.ts +2 -0
- package/dist/client/transport/do-rpc-client.test.d.ts.map +1 -0
- package/dist/client/transport/do-rpc-client.test.js +268 -0
- package/dist/client/transport/do-rpc-client.test.js.map +1 -0
- package/dist/client/transport/http-rpc-client.d.ts +3 -3
- package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/http-rpc-client.js +48 -29
- package/dist/client/transport/http-rpc-client.js.map +1 -1
- package/dist/client/transport/rpc-client-error.d.ts +5 -0
- package/dist/client/transport/rpc-client-error.d.ts.map +1 -0
- package/dist/client/transport/rpc-client-error.js +8 -0
- package/dist/client/transport/rpc-client-error.js.map +1 -0
- package/dist/client/transport/rpc-client-error.test.d.ts +2 -0
- package/dist/client/transport/rpc-client-error.test.d.ts.map +1 -0
- package/dist/client/transport/rpc-client-error.test.js +33 -0
- package/dist/client/transport/rpc-client-error.test.js.map +1 -0
- package/dist/client/transport/ws-rpc-client.d.ts +3 -3
- package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/ws-rpc-client.js +33 -23
- package/dist/client/transport/ws-rpc-client.js.map +1 -1
- package/dist/common/do-rpc-schema.d.ts +53 -41
- package/dist/common/do-rpc-schema.d.ts.map +1 -1
- package/dist/common/do-rpc-schema.js +13 -4
- package/dist/common/do-rpc-schema.js.map +1 -1
- package/dist/common/http-rpc-schema.d.ts +38 -39
- package/dist/common/http-rpc-schema.d.ts.map +1 -1
- package/dist/common/http-rpc-schema.js +6 -6
- package/dist/common/http-rpc-schema.js.map +1 -1
- package/dist/common/mod.d.ts +5 -5
- package/dist/common/mod.d.ts.map +1 -1
- package/dist/common/mod.js +7 -7
- package/dist/common/mod.js.map +1 -1
- package/dist/common/sync-message-types.d.ts +116 -133
- package/dist/common/sync-message-types.d.ts.map +1 -1
- package/dist/common/sync-message-types.js +26 -14
- package/dist/common/sync-message-types.js.map +1 -1
- package/dist/common/ws-rpc-schema.d.ts +33 -34
- package/dist/common/ws-rpc-schema.d.ts.map +1 -1
- package/dist/common/ws-rpc-schema.js +5 -5
- package/dist/common/ws-rpc-schema.js.map +1 -1
- package/package.json +42 -58
- package/src/cf-worker/do/durable-object.ts +70 -64
- package/src/cf-worker/do/layer.ts +142 -99
- package/src/cf-worker/do/observability.test.ts +337 -0
- package/src/cf-worker/do/observability.ts +121 -0
- package/src/cf-worker/do/pull.ts +28 -28
- package/src/cf-worker/do/push.test.ts +40 -0
- package/src/cf-worker/do/push.ts +139 -115
- package/src/cf-worker/do/sqlite.ts +1 -1
- package/src/cf-worker/do/sync-storage.ts +34 -21
- package/src/cf-worker/do/transport/do-rpc-server.test.ts +32 -0
- package/src/cf-worker/do/transport/do-rpc-server.ts +46 -35
- package/src/cf-worker/do/transport/http-rpc-server.ts +7 -10
- package/src/cf-worker/do/transport/ws-rpc-server.ts +52 -28
- package/src/cf-worker/shared.ts +29 -17
- package/src/cf-worker/worker.ts +21 -22
- package/src/client/transport/do-rpc-client.test.ts +411 -0
- package/src/client/transport/do-rpc-client.ts +175 -51
- package/src/client/transport/http-rpc-client.ts +66 -45
- package/src/client/transport/rpc-client-error.test.ts +41 -0
- package/src/client/transport/rpc-client-error.ts +9 -0
- package/src/client/transport/ws-rpc-client.ts +40 -32
- package/src/common/do-rpc-schema.ts +13 -3
- package/src/common/http-rpc-schema.ts +5 -5
- package/src/common/mod.ts +4 -4
- package/src/common/sync-message-types.ts +16 -16
- package/src/common/ws-rpc-schema.ts +4 -4
package/package.json
CHANGED
|
@@ -1,88 +1,72 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$genie": {
|
|
3
|
+
"source": "package.json.genie.ts",
|
|
4
|
+
"warning": "DO NOT EDIT - changes will be overwritten",
|
|
5
|
+
"workspaceClosureDirs": [
|
|
6
|
+
"packages/@livestore/common",
|
|
7
|
+
"packages/@livestore/common-cf",
|
|
8
|
+
"packages/@livestore/sync-cf",
|
|
9
|
+
"packages/@livestore/utils",
|
|
10
|
+
"packages/@livestore/utils-dev",
|
|
11
|
+
"packages/@livestore/webmesh"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
2
14
|
"name": "@livestore/sync-cf",
|
|
3
|
-
"version": "0.
|
|
15
|
+
"version": "0.5.0-dev.1",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
4
18
|
"license": "Apache-2.0",
|
|
5
19
|
"repository": {
|
|
6
20
|
"type": "git",
|
|
7
21
|
"url": "git+https://github.com/livestorejs/livestore.git"
|
|
8
22
|
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./client": "./dist/client/mod.js",
|
|
25
|
+
"./common": "./dist/common/mod.js",
|
|
26
|
+
"./cf-worker": "./dist/cf-worker/mod.js"
|
|
27
|
+
},
|
|
9
28
|
"files": [
|
|
10
29
|
"dist",
|
|
11
30
|
"package.json",
|
|
12
31
|
"src",
|
|
13
32
|
"README.md"
|
|
14
33
|
],
|
|
15
|
-
"type": "module",
|
|
16
|
-
"sideEffects": false,
|
|
17
|
-
"exports": {
|
|
18
|
-
"./client": "./dist/client/mod.js",
|
|
19
|
-
"./common": "./dist/common/mod.js",
|
|
20
|
-
"./cf-worker": "./dist/cf-worker/mod.js"
|
|
21
|
-
},
|
|
22
|
-
"publishConfig": {
|
|
23
|
-
"access": "public"
|
|
24
|
-
},
|
|
25
34
|
"dependencies": {
|
|
26
35
|
"@cloudflare/workers-types": "4.20251118.0",
|
|
27
|
-
"@livestore/common": "^0.
|
|
28
|
-
"@livestore/common-cf": "^0.
|
|
29
|
-
"@livestore/utils": "^0.
|
|
36
|
+
"@livestore/common": "^0.5.0-dev.1",
|
|
37
|
+
"@livestore/common-cf": "^0.5.0-dev.1",
|
|
38
|
+
"@livestore/utils": "^0.5.0-dev.1"
|
|
30
39
|
},
|
|
31
40
|
"devDependencies": {
|
|
32
|
-
"@effect/
|
|
33
|
-
"@effect/
|
|
34
|
-
"@effect/
|
|
35
|
-
"@effect/
|
|
36
|
-
"@effect/
|
|
37
|
-
"@effect/
|
|
38
|
-
"@
|
|
39
|
-
"@effect/platform-bun": "0.89.0",
|
|
40
|
-
"@effect/platform-node": "0.106.0",
|
|
41
|
-
"@effect/printer": "0.49.0",
|
|
42
|
-
"@effect/printer-ansi": "0.49.0",
|
|
43
|
-
"@effect/rpc": "0.75.1",
|
|
44
|
-
"@effect/sql": "0.51.1",
|
|
45
|
-
"@effect/typeclass": "0.40.0",
|
|
46
|
-
"@effect/vitest": "0.29.0",
|
|
41
|
+
"@effect/opentelemetry": "4.0.3",
|
|
42
|
+
"@effect/platform-browser": "4.0.3",
|
|
43
|
+
"@effect/platform-bun": "4.0.3",
|
|
44
|
+
"@effect/platform-node": "4.0.3",
|
|
45
|
+
"@effect/platform-node-shared": "4.0.3",
|
|
46
|
+
"@effect/vitest": "4.0.3",
|
|
47
|
+
"@livestore/utils-dev": "^0.5.0-dev.1",
|
|
47
48
|
"@opentelemetry/api": "1.9.0",
|
|
48
49
|
"@opentelemetry/resources": "2.2.0",
|
|
49
50
|
"@standard-schema/spec": "1.1.0",
|
|
50
|
-
"effect": "
|
|
51
|
+
"effect": "4.0.3",
|
|
52
|
+
"vitest": "4.1.9"
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
|
-
"@effect/
|
|
54
|
-
"@effect/
|
|
55
|
-
"@effect/
|
|
56
|
-
"@effect/
|
|
57
|
-
"@effect/
|
|
58
|
-
"@effect/
|
|
59
|
-
"@effect/platform-browser": "^0.76.0",
|
|
60
|
-
"@effect/platform-bun": "^0.89.0",
|
|
61
|
-
"@effect/platform-node": "^0.106.0",
|
|
62
|
-
"@effect/printer": "^0.49.0",
|
|
63
|
-
"@effect/printer-ansi": "^0.49.0",
|
|
64
|
-
"@effect/rpc": "^0.75.1",
|
|
65
|
-
"@effect/sql": "^0.51.1",
|
|
66
|
-
"@effect/typeclass": "^0.40.0",
|
|
67
|
-
"@effect/vitest": "^0.29.0",
|
|
55
|
+
"@effect/opentelemetry": "^4.0.3",
|
|
56
|
+
"@effect/platform-browser": "^4.0.3",
|
|
57
|
+
"@effect/platform-bun": "^4.0.3",
|
|
58
|
+
"@effect/platform-node": "^4.0.3",
|
|
59
|
+
"@effect/platform-node-shared": "^4.0.3",
|
|
60
|
+
"@effect/vitest": "^4.0.3",
|
|
68
61
|
"@opentelemetry/api": "^1.9.0",
|
|
69
62
|
"@opentelemetry/resources": "^2.2.0",
|
|
70
63
|
"@standard-schema/spec": "^1.1.0",
|
|
71
|
-
"effect": "^
|
|
64
|
+
"effect": "^4.0.3"
|
|
72
65
|
},
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"warning": "DO NOT EDIT - changes will be overwritten",
|
|
76
|
-
"workspaceClosureDirs": [
|
|
77
|
-
"packages/@livestore/common",
|
|
78
|
-
"packages/@livestore/common-cf",
|
|
79
|
-
"packages/@livestore/sync-cf",
|
|
80
|
-
"packages/@livestore/utils",
|
|
81
|
-
"packages/@livestore/utils-dev",
|
|
82
|
-
"packages/@livestore/webmesh"
|
|
83
|
-
]
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
84
68
|
},
|
|
85
69
|
"scripts": {
|
|
86
|
-
"test": "
|
|
70
|
+
"test": "vitest run"
|
|
87
71
|
}
|
|
88
72
|
}
|
|
@@ -4,17 +4,7 @@ import { DurableObject } from 'cloudflare:workers'
|
|
|
4
4
|
|
|
5
5
|
import { type CfTypes, setupDurableObjectWebSocketRpc } from '@livestore/common-cf'
|
|
6
6
|
import { CfDeclare } from '@livestore/common-cf/declare'
|
|
7
|
-
import {
|
|
8
|
-
Effect,
|
|
9
|
-
FetchHttpClient,
|
|
10
|
-
Layer,
|
|
11
|
-
Logger,
|
|
12
|
-
LogLevel,
|
|
13
|
-
Otlp,
|
|
14
|
-
RpcMessage,
|
|
15
|
-
Schema,
|
|
16
|
-
type Scope,
|
|
17
|
-
} from '@livestore/utils/effect'
|
|
7
|
+
import { Effect, Layer, Logger, References, RpcMessage, Schema, type Scope } from '@livestore/utils/effect'
|
|
18
8
|
|
|
19
9
|
import {
|
|
20
10
|
type Env,
|
|
@@ -24,17 +14,18 @@ import {
|
|
|
24
14
|
type SyncBackendRpcInterface,
|
|
25
15
|
WebSocketAttachmentSchema,
|
|
26
16
|
} from '../shared.ts'
|
|
27
|
-
import
|
|
17
|
+
import * as DoCtx from './layer.ts'
|
|
18
|
+
import { makeObservability } from './observability.ts'
|
|
28
19
|
import { createDoRpcHandler } from './transport/do-rpc-server.ts'
|
|
29
20
|
import { createHttpRpcHandler } from './transport/http-rpc-server.ts'
|
|
30
21
|
import { makeRpcServer } from './transport/ws-rpc-server.ts'
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const Response = CfDeclare.Response
|
|
24
|
+
const WebSocketPair = CfDeclare.WebSocketPair
|
|
25
|
+
const WebSocketRequestResponsePair = CfDeclare.WebSocketRequestResponsePair
|
|
26
|
+
|
|
27
|
+
/** Module-scoped JSON encoder; keeping the sync codec out of Effect generators avoids `schemaSyncInEffect`. */
|
|
28
|
+
const jsonStringify = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown))
|
|
38
29
|
|
|
39
30
|
const DurableObjectBase = DurableObject as any as new (
|
|
40
31
|
state: CfTypes.DurableObjectState,
|
|
@@ -52,14 +43,14 @@ export type MakeDurableObjectClass = (options?: MakeDurableObjectClassOptions) =
|
|
|
52
43
|
/**
|
|
53
44
|
* Creates a Durable Object class for handling WebSocket-based sync.
|
|
54
45
|
* A sync Durable Object is uniquely scoped to a specific `storeId`.
|
|
55
|
-
*
|
|
46
|
+
*
|
|
56
47
|
* The sync DO supports 3 transport modes:
|
|
57
48
|
* - HTTP JSON-RPC
|
|
58
49
|
* - WebSocket
|
|
59
50
|
* - Durable Object RPC calls (only works in combination with `@livestore/adapter-cf`)
|
|
60
51
|
*
|
|
61
52
|
* Example:
|
|
62
|
-
*
|
|
53
|
+
*
|
|
63
54
|
* ```ts
|
|
64
55
|
* // In your Cloudflare Worker file
|
|
65
56
|
* import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
|
|
@@ -88,25 +79,18 @@ export type MakeDurableObjectClass = (options?: MakeDurableObjectClassOptions) =
|
|
|
88
79
|
export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
89
80
|
const enabledTransports = options?.enabledTransports ?? new Set(['http', 'ws', 'do-rpc'])
|
|
90
81
|
|
|
91
|
-
const Logging = Logger.consoleWithThread('SyncDo')
|
|
92
|
-
|
|
93
|
-
const Observability: Layer.Layer<never> = options?.otel?.baseUrl !== undefined
|
|
94
|
-
? (Otlp.layer({
|
|
95
|
-
baseUrl: options.otel.baseUrl,
|
|
96
|
-
tracerExportInterval: 50,
|
|
97
|
-
resource: {
|
|
98
|
-
serviceName: options.otel.serviceName ?? 'sync-cf-do',
|
|
99
|
-
},
|
|
100
|
-
}).pipe(Layer.provide(FetchHttpClient.layer)) as Layer.Layer<never>)
|
|
101
|
-
: Layer.empty
|
|
102
|
-
|
|
103
82
|
return class SyncBackendDOBase extends DurableObjectBase implements SyncBackendRpcInterface {
|
|
104
83
|
__DURABLE_OBJECT_BRAND = 'SyncBackendDOBase' as never
|
|
84
|
+
private readonly observability = makeObservability(options?.otel)
|
|
105
85
|
|
|
106
86
|
constructor(ctx: CfTypes.DurableObjectState, env: Env) {
|
|
107
87
|
super(ctx, env)
|
|
108
88
|
|
|
109
|
-
const WebSocketRpcServerLive = makeRpcServer({
|
|
89
|
+
const WebSocketRpcServerLive = makeRpcServer({
|
|
90
|
+
doSelf: this,
|
|
91
|
+
doOptions: options,
|
|
92
|
+
observability: this.observability,
|
|
93
|
+
})
|
|
110
94
|
|
|
111
95
|
// This registers the `webSocketMessage` and `webSocketClose` handlers
|
|
112
96
|
if (enabledTransports.has('ws') === true) {
|
|
@@ -118,35 +102,24 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
118
102
|
// in combination with DO hibernation
|
|
119
103
|
onMessage: (request, ws) => {
|
|
120
104
|
if (request._tag === 'Request' && request.tag === 'SyncWsRpc.Pull') {
|
|
121
|
-
// Is Pull request: add requestId to pullRequestIds
|
|
122
105
|
const attachment = ws.deserializeAttachment()
|
|
123
|
-
const { pullRequestIds, ...rest } = Schema.
|
|
106
|
+
const { pullRequestIds, ...rest } = Schema.decodeUnknownSync(WebSocketAttachmentSchema)(attachment)
|
|
124
107
|
ws.serializeAttachment(
|
|
125
108
|
Schema.encodeSync(WebSocketAttachmentSchema)({
|
|
126
109
|
...rest,
|
|
127
110
|
pullRequestIds: [...pullRequestIds, request.id],
|
|
128
111
|
}),
|
|
129
112
|
)
|
|
130
|
-
} else if (request._tag === 'Interrupt') {
|
|
131
|
-
// Is Interrupt request: remove requestId from pullRequestIds
|
|
132
|
-
const attachment = ws.deserializeAttachment()
|
|
133
|
-
const { pullRequestIds, ...rest } = Schema.decodeSync(WebSocketAttachmentSchema)(attachment)
|
|
134
|
-
ws.serializeAttachment(
|
|
135
|
-
Schema.encodeSync(WebSocketAttachmentSchema)({
|
|
136
|
-
...rest,
|
|
137
|
-
pullRequestIds: pullRequestIds.filter((id) => id !== request.requestId),
|
|
138
|
-
}),
|
|
139
|
-
)
|
|
140
|
-
// TODO also emit `Exit` stream RPC message
|
|
141
113
|
}
|
|
142
114
|
},
|
|
143
|
-
|
|
115
|
+
onRequestExit: (requestId, ws) => removePullRequestId(ws, requestId),
|
|
116
|
+
onProtocolDefect: clearPullRequestIds,
|
|
144
117
|
})
|
|
145
118
|
}
|
|
146
119
|
}
|
|
147
120
|
|
|
148
|
-
override fetch = async (request: Request): Promise<Response> =>
|
|
149
|
-
Effect.gen(this, function* () {
|
|
121
|
+
override fetch = async (request: CfTypes.Request): Promise<CfTypes.Response> =>
|
|
122
|
+
Effect.gen({ self: this }, function* () {
|
|
150
123
|
const searchParams = matchSyncRequest(request)
|
|
151
124
|
if (searchParams === undefined) {
|
|
152
125
|
throw new Error('No search params found in request URL')
|
|
@@ -170,9 +143,16 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
170
143
|
|
|
171
144
|
// Since we're using websocket hibernation, we need to remember the storeId for subsequent `webSocketMessage` calls
|
|
172
145
|
// Also store forwarded headers so they're available after hibernation resume
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
146
|
+
// Encoding known-valid domain data: an encode failure is an invariant violation (a defect),
|
|
147
|
+
// so `Effect.orDie` is the correct modeling. serializeAttachment takes the value synchronously,
|
|
148
|
+
// so resolve the encoded value first, then pass it in.
|
|
149
|
+
const attachment = yield* Schema.encodeEffect(WebSocketAttachmentSchema)({
|
|
150
|
+
storeId,
|
|
151
|
+
payload,
|
|
152
|
+
pullRequestIds: [],
|
|
153
|
+
headers,
|
|
154
|
+
}).pipe(Effect.orDie)
|
|
155
|
+
server.serializeAttachment(attachment)
|
|
176
156
|
|
|
177
157
|
// See https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server
|
|
178
158
|
|
|
@@ -180,10 +160,7 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
180
160
|
|
|
181
161
|
// Ping requests are sent by Effect RPC internally
|
|
182
162
|
this.ctx.setWebSocketAutoResponse(
|
|
183
|
-
new WebSocketRequestResponsePair(
|
|
184
|
-
JSON.stringify(RpcMessage.constPing),
|
|
185
|
-
JSON.stringify(RpcMessage.constPong),
|
|
186
|
-
),
|
|
163
|
+
new WebSocketRequestResponsePair(jsonStringify(RpcMessage.constPing), jsonStringify(RpcMessage.constPong)),
|
|
187
164
|
)
|
|
188
165
|
|
|
189
166
|
return new Response(null, {
|
|
@@ -200,11 +177,11 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
200
177
|
})
|
|
201
178
|
}).pipe(
|
|
202
179
|
Effect.tapCauseLogPretty, // Also log errors to console before catching them
|
|
203
|
-
Effect.
|
|
180
|
+
Effect.catchCause((cause) =>
|
|
204
181
|
Effect.succeed(new Response('Error', { status: 500, statusText: cause.toString() })),
|
|
205
182
|
),
|
|
206
183
|
Effect.withSpan('@livestore/sync-cf:durable-object:fetch'),
|
|
207
|
-
Effect.provide(DoCtx.
|
|
184
|
+
Effect.provide(DoCtx.layer({ doSelf: this, doOptions: options, from: request })),
|
|
208
185
|
this.runEffectAsPromise,
|
|
209
186
|
)
|
|
210
187
|
|
|
@@ -216,10 +193,11 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
216
193
|
throw new Error('Do RPC transport is not enabled (based on `options.enabledTransports`)')
|
|
217
194
|
}
|
|
218
195
|
|
|
219
|
-
return createDoRpcHandler({
|
|
220
|
-
|
|
221
|
-
this
|
|
222
|
-
|
|
196
|
+
return createDoRpcHandler({
|
|
197
|
+
payload,
|
|
198
|
+
input: { doSelf: this, doOptions: options },
|
|
199
|
+
observability: this.observability,
|
|
200
|
+
}).pipe(Effect.withSpan('@livestore/sync-cf:durable-object:rpc'), this.runEffectAsPromise)
|
|
223
201
|
}
|
|
224
202
|
|
|
225
203
|
/**
|
|
@@ -237,10 +215,38 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
|
|
|
237
215
|
private runEffectAsPromise = <T, E = never>(effect: Effect.Effect<T, E, Scope.Scope>): Promise<T> =>
|
|
238
216
|
effect.pipe(
|
|
239
217
|
Effect.tapCauseLogPretty,
|
|
240
|
-
|
|
241
|
-
Effect.provide(
|
|
218
|
+
Effect.annotateLogs({ thread: 'SyncDo' }),
|
|
219
|
+
Effect.provide(
|
|
220
|
+
Layer.mergeAll(Logger.layer([Logger.consoleStructured]), Layer.succeed(References.MinimumLogLevel, 'Debug')),
|
|
221
|
+
),
|
|
242
222
|
Effect.scoped,
|
|
223
|
+
this.observability.effect,
|
|
243
224
|
Effect.runPromise,
|
|
244
225
|
)
|
|
245
226
|
}
|
|
246
227
|
}
|
|
228
|
+
|
|
229
|
+
const removePullRequestId = (ws: CfTypes.WebSocket, requestId: string | number) =>
|
|
230
|
+
updatePullRequestIds(ws, (pullRequestIds) =>
|
|
231
|
+
pullRequestIds.includes(requestId) === true ? pullRequestIds.filter((id) => id !== requestId) : pullRequestIds,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const clearPullRequestIds = (ws: CfTypes.WebSocket) =>
|
|
235
|
+
updatePullRequestIds(ws, (pullRequestIds) => (pullRequestIds.length === 0 ? pullRequestIds : []))
|
|
236
|
+
|
|
237
|
+
const updatePullRequestIds = (
|
|
238
|
+
ws: CfTypes.WebSocket,
|
|
239
|
+
update: (pullRequestIds: ReadonlyArray<string | number>) => ReadonlyArray<string | number>,
|
|
240
|
+
) => {
|
|
241
|
+
const attachment = ws.deserializeAttachment()
|
|
242
|
+
const { pullRequestIds, ...rest } = Schema.decodeUnknownSync(WebSocketAttachmentSchema)(attachment)
|
|
243
|
+
const nextPullRequestIds = update(pullRequestIds)
|
|
244
|
+
if (nextPullRequestIds === pullRequestIds) return
|
|
245
|
+
|
|
246
|
+
ws.serializeAttachment(
|
|
247
|
+
Schema.encodeSync(WebSocketAttachmentSchema)({
|
|
248
|
+
...rest,
|
|
249
|
+
pullRequestIds: nextPullRequestIds,
|
|
250
|
+
}),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
@@ -2,16 +2,17 @@ import { UnknownError } from '@livestore/common'
|
|
|
2
2
|
import type { CfTypes } from '@livestore/common-cf'
|
|
3
3
|
import { EventSequenceNumber, State } from '@livestore/common/schema'
|
|
4
4
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
5
|
-
import { Effect, Predicate } from '@livestore/utils/effect'
|
|
5
|
+
import { Context, Effect, Layer, Predicate, Semaphore } from '@livestore/utils/effect'
|
|
6
6
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
7
7
|
|
|
8
|
-
import type { Env, MakeDurableObjectClassOptions
|
|
8
|
+
import type { Env, MakeDurableObjectClassOptions } from '../shared.ts'
|
|
9
9
|
import { contextTable, eventlogTable } from './sqlite.ts'
|
|
10
|
-
import { makeStorage } from './sync-storage.ts'
|
|
10
|
+
import { makeStorage, type StorageEngine, type SyncStorage } from './sync-storage.ts'
|
|
11
11
|
|
|
12
12
|
const CacheSymbol = Symbol('Cache')
|
|
13
|
+
const InitializationSemaphoreSymbol = Symbol('InitializationSemaphore')
|
|
13
14
|
|
|
14
|
-
export interface
|
|
15
|
+
export interface Options {
|
|
15
16
|
doSelf: CfTypes.DurableObject & {
|
|
16
17
|
ctx: CfTypes.DurableObjectState
|
|
17
18
|
env: Env
|
|
@@ -20,110 +21,152 @@ export interface DoCtxInput {
|
|
|
20
21
|
from: CfTypes.Request | { storeId: string }
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
export type DoCtxInput = Options
|
|
25
|
+
|
|
26
|
+
export interface Service {
|
|
27
|
+
readonly storeId: string
|
|
28
|
+
readonly backendId: string
|
|
29
|
+
readonly currentHeadRef: { current: EventSequenceNumber.Global.Type }
|
|
30
|
+
readonly updateCurrentHead: (currentHead: EventSequenceNumber.Global.Type) => void
|
|
31
|
+
/** Serializes push admission across concurrent RPC fibers for this Durable Object instance. */
|
|
32
|
+
readonly pushSemaphore: Semaphore.Semaphore
|
|
33
|
+
readonly storage: SyncStorage
|
|
34
|
+
readonly doOptions: MakeDurableObjectClassOptions | undefined
|
|
35
|
+
readonly env: Env
|
|
36
|
+
readonly ctx: CfTypes.DurableObjectState
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DoCtx extends Context.Service<DoCtx, Service>()('@livestore/sync-cf/DoCtx') {}
|
|
40
|
+
|
|
41
|
+
type DurableObjectCacheHost = Options['doSelf'] & {
|
|
42
|
+
[CacheSymbol]?: Service
|
|
43
|
+
[InitializationSemaphoreSymbol]?: Semaphore.Semaphore
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const make = Effect.fn(
|
|
47
|
+
function* ({ doSelf, doOptions, from }: Options) {
|
|
48
|
+
const cacheHost = doSelf as DurableObjectCacheHost
|
|
49
|
+
if (cacheHost[CacheSymbol] !== undefined) {
|
|
50
|
+
return DoCtx.of(cacheHost[CacheSymbol])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Request layers can be constructed concurrently during the first RPCs to a DO.
|
|
54
|
+
// Install this lock synchronously so every contender shares the same initializer.
|
|
55
|
+
const initializationSemaphore = cacheHost[InitializationSemaphoreSymbol] ?? Semaphore.makeUnsafe(1)
|
|
56
|
+
cacheHost[InitializationSemaphoreSymbol] = initializationSemaphore
|
|
57
|
+
|
|
58
|
+
return yield* initializationSemaphore.withPermits(1)(
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
if (cacheHost[CacheSymbol] !== undefined) {
|
|
61
|
+
return DoCtx.of(cacheHost[CacheSymbol])
|
|
36
62
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (opt?._tag === 'd1') {
|
|
45
|
-
const db = (doSelf.env as any)[opt.binding]
|
|
46
|
-
if (db == null) {
|
|
47
|
-
return yield* UnknownError.make({ cause: new Error(`D1 binding '${opt.binding}' not found on env`) })
|
|
63
|
+
|
|
64
|
+
const getStoreId = (from: CfTypes.Request | { storeId: string }) => {
|
|
65
|
+
if (Predicate.hasProperty(from, 'url') === true) {
|
|
66
|
+
const url = new URL(from.url)
|
|
67
|
+
return (
|
|
68
|
+
url.searchParams.get('storeId') ?? shouldNeverHappen(`No storeId provided in request URL search params`)
|
|
69
|
+
)
|
|
48
70
|
}
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
return { _tag: 'do-sqlite' as const }
|
|
52
|
-
} else return shouldNeverHappen(`Invalid storage engine`, opt)
|
|
53
|
-
})
|
|
71
|
+
return from.storeId
|
|
72
|
+
}
|
|
54
73
|
|
|
55
|
-
|
|
74
|
+
const storeId = getStoreId(from)
|
|
75
|
+
const engine = yield* resolveStorageEngine(doOptions?.storage, doSelf.env)
|
|
76
|
+
|
|
77
|
+
const storage = makeStorage(doSelf.ctx, storeId, engine)
|
|
78
|
+
|
|
79
|
+
// Initialize database tables
|
|
80
|
+
{
|
|
81
|
+
const colSpec = State.SQLite.makeColumnSpec(eventlogTable.sqliteDef.ast)
|
|
82
|
+
if (engine._tag === 'd1') {
|
|
83
|
+
// D1 database is async, so we need to use a promise
|
|
84
|
+
yield* Effect.promise(() =>
|
|
85
|
+
engine.db.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`),
|
|
86
|
+
)
|
|
87
|
+
} else {
|
|
88
|
+
// DO SQLite table lives in Durable Object storage
|
|
89
|
+
doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
{
|
|
93
|
+
const colSpec = State.SQLite.makeColumnSpec(contextTable.sqliteDef.ast)
|
|
94
|
+
doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${contextTable.sqliteDef.name}" (${colSpec}) strict`)
|
|
95
|
+
}
|
|
56
96
|
|
|
57
|
-
|
|
97
|
+
const storageRow = doSelf.ctx.storage.sql
|
|
98
|
+
.exec(`SELECT * FROM "${contextTable.sqliteDef.name}" WHERE storeId = ?`, storeId)
|
|
99
|
+
.toArray()[0] as typeof contextTable.rowSchema.Type | undefined
|
|
58
100
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const colSpec = State.SQLite.makeColumnSpec(eventlogTable.sqliteDef.ast)
|
|
62
|
-
if (engine._tag === 'd1') {
|
|
63
|
-
// D1 database is async, so we need to use a promise
|
|
64
|
-
yield* Effect.promise(() =>
|
|
65
|
-
engine.db.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`),
|
|
66
|
-
)
|
|
67
|
-
} else {
|
|
68
|
-
// DO SQLite table lives in Durable Object storage
|
|
69
|
-
doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
{
|
|
73
|
-
const colSpec = State.SQLite.makeColumnSpec(contextTable.sqliteDef.ast)
|
|
74
|
-
doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${contextTable.sqliteDef.name}" (${colSpec}) strict`)
|
|
75
|
-
}
|
|
101
|
+
const currentHeadRef = { current: storageRow?.currentHead ?? EventSequenceNumber.Client.ROOT.global }
|
|
102
|
+
const pushSemaphore = yield* Semaphore.make(1)
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
// TODO: Reconcile this persisted head with the event log's highest sequence number.
|
|
105
|
+
// D1 event batches and this DO-local context row cannot be updated atomically, so a
|
|
106
|
+
// failure between those writes can leave push admission using a stale head after restart.
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
// Should be the same backendId for lifetime of the Durable Object
|
|
109
|
+
const backendId = storageRow?.backendId ?? nanoid()
|
|
82
110
|
|
|
83
|
-
|
|
111
|
+
const updateCurrentHead = (currentHead: EventSequenceNumber.Global.Type) => {
|
|
112
|
+
doSelf.ctx.storage.sql.exec(
|
|
113
|
+
`INSERT OR REPLACE INTO "${contextTable.sqliteDef.name}" (storeId, currentHead, backendId) VALUES (?, ?, ?)`,
|
|
114
|
+
storeId,
|
|
115
|
+
currentHead,
|
|
116
|
+
backendId,
|
|
117
|
+
)
|
|
84
118
|
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
currentHeadRef.current = currentHead
|
|
120
|
+
}
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
doSelf.ctx.storage.sql.exec(
|
|
90
|
-
`INSERT OR REPLACE INTO "${contextTable.sqliteDef.name}" (storeId, currentHead, backendId) VALUES (?, ?, ?)`,
|
|
122
|
+
const storageCache = {
|
|
91
123
|
storeId,
|
|
92
|
-
currentHead,
|
|
93
124
|
backendId,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
currentHeadRef,
|
|
126
|
+
updateCurrentHead,
|
|
127
|
+
pushSemaphore,
|
|
128
|
+
storage,
|
|
129
|
+
doOptions,
|
|
130
|
+
env: doSelf.env,
|
|
131
|
+
ctx: doSelf.ctx,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
cacheHost[CacheSymbol] = storageCache
|
|
135
|
+
|
|
136
|
+
// Set initial current head to root
|
|
137
|
+
if (storageRow === undefined) {
|
|
138
|
+
updateCurrentHead(EventSequenceNumber.Client.ROOT.global)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return DoCtx.of(storageCache)
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
},
|
|
145
|
+
UnknownError.mapToUnknownError,
|
|
146
|
+
Effect.withSpan('@livestore/sync-cf:durable-object:makeDoCtx'),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
export const layer = (options: Options) => Layer.effect(DoCtx, make(options))
|
|
150
|
+
|
|
151
|
+
/** Resolves storage configuration into the concrete database handle used at runtime. */
|
|
152
|
+
const resolveStorageEngine = Effect.fn(function* (
|
|
153
|
+
storageOption: MakeDurableObjectClassOptions['storage'],
|
|
154
|
+
env: Env,
|
|
155
|
+
): Effect.fn.Return<StorageEngine, UnknownError> {
|
|
156
|
+
if (storageOption === undefined || storageOption._tag === 'do-sqlite') {
|
|
157
|
+
return { _tag: 'do-sqlite' }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (storageOption._tag === 'd1') {
|
|
161
|
+
const db = (env as Record<string, CfTypes.D1Database | undefined>)[storageOption.binding]
|
|
162
|
+
if (db === undefined) {
|
|
163
|
+
return yield* UnknownError.make({
|
|
164
|
+
cause: new Error(`D1 binding '${storageOption.binding}' not found on env`),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { _tag: 'd1', db }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return shouldNeverHappen(`Invalid storage engine`, storageOption)
|
|
172
|
+
})
|