@livestore/sync-cf 0.4.0-dev.8 → 0.4.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 +7 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/cf-worker/do/durable-object.d.ts +1 -1
- package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
- package/dist/cf-worker/do/durable-object.js +15 -14
- package/dist/cf-worker/do/durable-object.js.map +1 -1
- package/dist/cf-worker/do/layer.d.ts +6 -6
- package/dist/cf-worker/do/layer.d.ts.map +1 -1
- package/dist/cf-worker/do/layer.js +32 -9
- package/dist/cf-worker/do/layer.js.map +1 -1
- package/dist/cf-worker/do/pull.d.ts +8 -3
- package/dist/cf-worker/do/pull.d.ts.map +1 -1
- package/dist/cf-worker/do/pull.js +22 -10
- package/dist/cf-worker/do/pull.js.map +1 -1
- package/dist/cf-worker/do/push.d.ts +5 -4
- package/dist/cf-worker/do/push.d.ts.map +1 -1
- package/dist/cf-worker/do/push.js +80 -41
- package/dist/cf-worker/do/push.js.map +1 -1
- package/dist/cf-worker/do/sqlite.d.ts +10 -1
- package/dist/cf-worker/do/sqlite.d.ts.map +1 -1
- package/dist/cf-worker/do/sqlite.js +13 -4
- package/dist/cf-worker/do/sqlite.js.map +1 -1
- package/dist/cf-worker/do/sync-storage.d.ts +14 -9
- package/dist/cf-worker/do/sync-storage.d.ts.map +1 -1
- package/dist/cf-worker/do/sync-storage.js +92 -18
- package/dist/cf-worker/do/sync-storage.js.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts +2 -1
- package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/do-rpc-server.js +13 -7
- package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts +3 -1
- package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/http-rpc-server.js +24 -15
- package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +2 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
- package/dist/cf-worker/do/transport/ws-rpc-server.js +30 -8
- package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
- package/dist/cf-worker/shared.d.ts +123 -30
- package/dist/cf-worker/shared.d.ts.map +1 -1
- package/dist/cf-worker/shared.js +50 -6
- package/dist/cf-worker/shared.js.map +1 -1
- package/dist/cf-worker/worker.d.ts +64 -71
- package/dist/cf-worker/worker.d.ts.map +1 -1
- package/dist/cf-worker/worker.js +70 -48
- package/dist/cf-worker/worker.js.map +1 -1
- package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/do-rpc-client.js +27 -10
- package/dist/client/transport/do-rpc-client.js.map +1 -1
- package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/http-rpc-client.js +29 -9
- package/dist/client/transport/http-rpc-client.js.map +1 -1
- package/dist/client/transport/ws-rpc-client.d.ts +2 -1
- package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
- package/dist/client/transport/ws-rpc-client.js +31 -17
- package/dist/client/transport/ws-rpc-client.js.map +1 -1
- package/dist/common/constants.d.ts +7 -0
- package/dist/common/constants.d.ts.map +1 -0
- package/dist/common/constants.js +17 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/do-rpc-schema.d.ts +6 -6
- package/dist/common/do-rpc-schema.d.ts.map +1 -1
- package/dist/common/do-rpc-schema.js +4 -4
- package/dist/common/do-rpc-schema.js.map +1 -1
- package/dist/common/http-rpc-schema.d.ts +4 -4
- package/dist/common/http-rpc-schema.d.ts.map +1 -1
- package/dist/common/http-rpc-schema.js +4 -4
- package/dist/common/http-rpc-schema.js.map +1 -1
- package/dist/common/mod.d.ts +4 -1
- package/dist/common/mod.d.ts.map +1 -1
- package/dist/common/mod.js +4 -1
- package/dist/common/mod.js.map +1 -1
- package/dist/common/sync-message-types.d.ts +2 -2
- package/dist/common/sync-message-types.js +3 -3
- package/dist/common/sync-message-types.js.map +1 -1
- package/dist/common/ws-rpc-schema.d.ts +3 -3
- package/dist/common/ws-rpc-schema.d.ts.map +1 -1
- package/dist/common/ws-rpc-schema.js +3 -3
- package/dist/common/ws-rpc-schema.js.map +1 -1
- package/package.json +72 -14
- package/src/cf-worker/do/durable-object.ts +23 -18
- package/src/cf-worker/do/layer.ts +35 -13
- package/src/cf-worker/do/pull.ts +43 -14
- package/src/cf-worker/do/push.ts +107 -46
- package/src/cf-worker/do/sqlite.ts +14 -4
- package/src/cf-worker/do/sync-storage.ts +151 -31
- package/src/cf-worker/do/transport/do-rpc-server.ts +22 -9
- package/src/cf-worker/do/transport/http-rpc-server.ts +33 -13
- package/src/cf-worker/do/transport/ws-rpc-server.ts +40 -12
- package/src/cf-worker/shared.ts +149 -25
- package/src/cf-worker/worker.ts +138 -108
- package/src/client/transport/do-rpc-client.ts +41 -17
- package/src/client/transport/http-rpc-client.ts +43 -17
- package/src/client/transport/ws-rpc-client.ts +42 -19
- package/src/common/constants.ts +18 -0
- package/src/common/do-rpc-schema.ts +5 -4
- package/src/common/http-rpc-schema.ts +5 -4
- package/src/common/mod.ts +4 -2
- package/src/common/sync-message-types.ts +3 -3
- package/src/common/ws-rpc-schema.ts +4 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EventSequenceNumber, State } from '@livestore/common/schema'
|
|
1
|
+
import { UnknownError } from '@livestore/common'
|
|
3
2
|
import type { CfTypes } from '@livestore/common-cf'
|
|
3
|
+
import { EventSequenceNumber, State } from '@livestore/common/schema'
|
|
4
4
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
5
5
|
import { Effect, Predicate } from '@livestore/utils/effect'
|
|
6
6
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
7
|
+
|
|
7
8
|
import type { Env, MakeDurableObjectClassOptions, RpcSubscription } from '../shared.ts'
|
|
8
9
|
import { contextTable, eventlogTable } from './sqlite.ts'
|
|
9
10
|
import { makeStorage } from './sync-storage.ts'
|
|
@@ -27,7 +28,7 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const getStoreId = (from: CfTypes.Request | { storeId: string }) => {
|
|
30
|
-
if (Predicate.hasProperty(from, 'url')) {
|
|
31
|
+
if (Predicate.hasProperty(from, 'url') === true) {
|
|
31
32
|
const url = new URL(from.url)
|
|
32
33
|
return (
|
|
33
34
|
url.searchParams.get('storeId') ?? shouldNeverHappen(`No storeId provided in request URL search params`)
|
|
@@ -37,15 +38,36 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const storeId = getStoreId(from)
|
|
40
|
-
|
|
41
|
+
// Resolve storage engine
|
|
42
|
+
const makeEngine = Effect.gen(function* () {
|
|
43
|
+
const opt = doOptions?.storage
|
|
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`) })
|
|
48
|
+
}
|
|
49
|
+
return { _tag: 'd1' as const, db }
|
|
50
|
+
} else if (opt?._tag === 'do-sqlite' || opt === undefined) {
|
|
51
|
+
return { _tag: 'do-sqlite' as const }
|
|
52
|
+
} else return shouldNeverHappen(`Invalid storage engine`, opt)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const engine = yield* makeEngine
|
|
56
|
+
|
|
57
|
+
const storage = makeStorage(doSelf.ctx, storeId, engine)
|
|
41
58
|
|
|
42
59
|
// Initialize database tables
|
|
43
60
|
{
|
|
44
61
|
const colSpec = State.SQLite.makeColumnSpec(eventlogTable.sqliteDef.ast)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
71
|
}
|
|
50
72
|
{
|
|
51
73
|
const colSpec = State.SQLite.makeColumnSpec(contextTable.sqliteDef.ast)
|
|
@@ -56,14 +78,14 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
|
|
|
56
78
|
.exec(`SELECT * FROM "${contextTable.sqliteDef.name}" WHERE storeId = ?`, storeId)
|
|
57
79
|
.toArray()[0] as typeof contextTable.rowSchema.Type | undefined
|
|
58
80
|
|
|
59
|
-
const currentHeadRef = { current: storageRow?.currentHead ?? EventSequenceNumber.ROOT.global }
|
|
81
|
+
const currentHeadRef = { current: storageRow?.currentHead ?? EventSequenceNumber.Client.ROOT.global }
|
|
60
82
|
|
|
61
83
|
// TODO do concistency check with eventlog table to make sure the head is consistent
|
|
62
84
|
|
|
63
|
-
// Should be the same backendId for lifetime of the
|
|
85
|
+
// Should be the same backendId for lifetime of the Durable Object
|
|
64
86
|
const backendId = storageRow?.backendId ?? nanoid()
|
|
65
87
|
|
|
66
|
-
const updateCurrentHead = (currentHead: EventSequenceNumber.
|
|
88
|
+
const updateCurrentHead = (currentHead: EventSequenceNumber.Global.Type) => {
|
|
67
89
|
doSelf.ctx.storage.sql.exec(
|
|
68
90
|
`INSERT OR REPLACE INTO "${contextTable.sqliteDef.name}" (storeId, currentHead, backendId) VALUES (?, ?, ?)`,
|
|
69
91
|
storeId,
|
|
@@ -96,12 +118,12 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
|
|
|
96
118
|
|
|
97
119
|
// Set initial current head to root
|
|
98
120
|
if (storageRow === undefined) {
|
|
99
|
-
updateCurrentHead(EventSequenceNumber.ROOT.global)
|
|
121
|
+
updateCurrentHead(EventSequenceNumber.Client.ROOT.global)
|
|
100
122
|
}
|
|
101
123
|
|
|
102
124
|
return storageCache
|
|
103
125
|
},
|
|
104
|
-
|
|
126
|
+
UnknownError.mapToUnknownError,
|
|
105
127
|
Effect.withSpan('@livestore/sync-cf:durable-object:makeDoCtx'),
|
|
106
128
|
),
|
|
107
129
|
}) {}
|
package/src/cf-worker/do/pull.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { BackendIdMismatchError,
|
|
2
|
-
import {
|
|
1
|
+
import { BackendIdMismatchError, SyncBackend, UnknownError } from '@livestore/common'
|
|
2
|
+
import { splitChunkBySize } from '@livestore/common/sync'
|
|
3
|
+
import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
|
|
4
|
+
|
|
5
|
+
import { MAX_PULL_EVENTS_PER_MESSAGE, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
|
|
3
6
|
import { SyncMessage } from '../../common/mod.ts'
|
|
4
|
-
import {
|
|
7
|
+
import type { ForwardedHeaders } from '../shared.ts'
|
|
5
8
|
import { DoCtx } from './layer.ts'
|
|
6
9
|
|
|
10
|
+
const encodePullResponse = Schema.encodeSync(SyncMessage.PullResponse)
|
|
11
|
+
|
|
7
12
|
// Notes on stream handling:
|
|
8
13
|
// We're intentionally closing the stream once we've read all existing events
|
|
9
14
|
//
|
|
@@ -13,16 +18,27 @@ import { DoCtx } from './layer.ts'
|
|
|
13
18
|
// DO RPC:
|
|
14
19
|
// - Further chunks will be emitted manually in `push.ts`
|
|
15
20
|
// - If the client sends a `Interrupt` RPC message, TODO
|
|
16
|
-
export const makeEndingPullStream = (
|
|
17
|
-
req
|
|
18
|
-
payload
|
|
19
|
-
|
|
21
|
+
export const makeEndingPullStream = ({
|
|
22
|
+
req,
|
|
23
|
+
payload,
|
|
24
|
+
headers,
|
|
25
|
+
}: {
|
|
26
|
+
req: SyncMessage.PullRequest
|
|
27
|
+
payload: Schema.JsonValue | undefined
|
|
28
|
+
headers: ForwardedHeaders | undefined
|
|
29
|
+
}): Stream.Stream<SyncMessage.PullResponse, UnknownError | BackendIdMismatchError, DoCtx> =>
|
|
20
30
|
Effect.gen(function* () {
|
|
21
31
|
const { doOptions, backendId, storeId, storage } = yield* DoCtx
|
|
22
32
|
|
|
23
|
-
if (doOptions?.onPull) {
|
|
24
|
-
yield* Effect.tryAll(() =>
|
|
25
|
-
|
|
33
|
+
if (doOptions?.onPull !== undefined) {
|
|
34
|
+
yield* Effect.tryAll(() =>
|
|
35
|
+
doOptions.onPull!(req, {
|
|
36
|
+
storeId,
|
|
37
|
+
...(payload !== undefined ? { payload } : {}),
|
|
38
|
+
...(headers !== undefined ? { headers } : {}),
|
|
39
|
+
}),
|
|
40
|
+
).pipe(
|
|
41
|
+
UnknownError.mapToUnknownError,
|
|
26
42
|
)
|
|
27
43
|
}
|
|
28
44
|
|
|
@@ -35,7 +51,16 @@ export const makeEndingPullStream = (
|
|
|
35
51
|
)
|
|
36
52
|
|
|
37
53
|
return storedEvents.pipe(
|
|
38
|
-
Stream.
|
|
54
|
+
Stream.mapChunksEffect(
|
|
55
|
+
splitChunkBySize({
|
|
56
|
+
maxItems: MAX_PULL_EVENTS_PER_MESSAGE,
|
|
57
|
+
maxBytes: MAX_WS_MESSAGE_BYTES,
|
|
58
|
+
encode: (batch) =>
|
|
59
|
+
encodePullResponse(
|
|
60
|
+
SyncMessage.PullResponse.make({ batch, pageInfo: SyncBackend.pageInfoNoMore, backendId }),
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
39
64
|
Stream.mapAccum(total, (remaining, chunk) => {
|
|
40
65
|
const asArray = Chunk.toReadonlyArray(chunk)
|
|
41
66
|
const nextRemaining = Math.max(0, remaining - asArray.length)
|
|
@@ -51,8 +76,8 @@ export const makeEndingPullStream = (
|
|
|
51
76
|
}),
|
|
52
77
|
Stream.tap(
|
|
53
78
|
Effect.fn(function* (res) {
|
|
54
|
-
if (doOptions?.onPullRes) {
|
|
55
|
-
yield* Effect.tryAll(() => doOptions.onPullRes!(res)).pipe(
|
|
79
|
+
if (doOptions?.onPullRes !== undefined) {
|
|
80
|
+
yield* Effect.tryAll(() => doOptions.onPullRes!(res)).pipe(UnknownError.mapToUnknownError)
|
|
56
81
|
}
|
|
57
82
|
}),
|
|
58
83
|
),
|
|
@@ -60,6 +85,10 @@ export const makeEndingPullStream = (
|
|
|
60
85
|
)
|
|
61
86
|
}).pipe(
|
|
62
87
|
Stream.unwrap,
|
|
63
|
-
Stream.mapError((cause) =>
|
|
88
|
+
Stream.mapError((cause) =>
|
|
89
|
+
cause._tag === 'BackendIdMismatchError' || cause._tag === 'UnknownError'
|
|
90
|
+
? cause
|
|
91
|
+
: new UnknownError({ cause }),
|
|
92
|
+
),
|
|
64
93
|
Stream.withSpan('cloudflare-provider:pull'),
|
|
65
94
|
)
|
package/src/cf-worker/do/push.ts
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BackendIdMismatchError,
|
|
3
|
-
InvalidPushError,
|
|
4
3
|
ServerAheadError,
|
|
5
4
|
SyncBackend,
|
|
6
|
-
|
|
5
|
+
UnknownError,
|
|
7
6
|
} from '@livestore/common'
|
|
8
7
|
import { type CfTypes, emitStreamResponse } from '@livestore/common-cf'
|
|
9
|
-
import {
|
|
8
|
+
import { splitChunkBySize } from '@livestore/common/sync'
|
|
9
|
+
import { Chunk, Effect, Option, type RpcMessage, Schema } from '@livestore/utils/effect'
|
|
10
|
+
|
|
11
|
+
import { MAX_PUSH_EVENTS_PER_REQUEST, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
|
|
10
12
|
import { SyncMessage } from '../../common/mod.ts'
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type Env,
|
|
15
|
+
type ForwardedHeaders,
|
|
16
|
+
type MakeDurableObjectClassOptions,
|
|
17
|
+
type StoreId,
|
|
18
|
+
WebSocketAttachmentSchema,
|
|
19
|
+
} from '../shared.ts'
|
|
12
20
|
import { DoCtx } from './layer.ts'
|
|
13
21
|
|
|
22
|
+
const encodePullResponse = Schema.encodeSync(SyncMessage.PullResponse)
|
|
23
|
+
const jsonStringify = Schema.encodeSync(Schema.parseJson())
|
|
24
|
+
type PullBatchItem = SyncMessage.PullResponse['batch'][number]
|
|
25
|
+
|
|
14
26
|
export const makePush =
|
|
15
27
|
({
|
|
16
28
|
payload,
|
|
29
|
+
headers,
|
|
17
30
|
options,
|
|
18
31
|
storeId,
|
|
19
32
|
ctx,
|
|
20
33
|
env,
|
|
21
34
|
}: {
|
|
22
35
|
payload: Schema.JsonValue | undefined
|
|
36
|
+
headers: ForwardedHeaders | undefined
|
|
23
37
|
options: MakeDurableObjectClassOptions | undefined
|
|
24
38
|
storeId: StoreId
|
|
25
39
|
ctx: CfTypes.DurableObjectState
|
|
@@ -34,9 +48,15 @@ export const makePush =
|
|
|
34
48
|
return SyncMessage.PushAck.make({})
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
if (options?.onPush) {
|
|
38
|
-
yield* Effect.tryAll(() =>
|
|
39
|
-
|
|
51
|
+
if (options?.onPush !== undefined) {
|
|
52
|
+
yield* Effect.tryAll(() =>
|
|
53
|
+
options.onPush!(pushRequest, {
|
|
54
|
+
storeId,
|
|
55
|
+
...(payload !== undefined ? { payload } : {}),
|
|
56
|
+
...(headers !== undefined ? { headers } : {}),
|
|
57
|
+
}),
|
|
58
|
+
).pipe(
|
|
59
|
+
UnknownError.mapToUnknownError,
|
|
40
60
|
)
|
|
41
61
|
}
|
|
42
62
|
|
|
@@ -51,6 +71,13 @@ export const makePush =
|
|
|
51
71
|
// Validate the batch
|
|
52
72
|
const firstEventParent = pushRequest.batch[0]!.parentSeqNum
|
|
53
73
|
if (firstEventParent !== currentHead) {
|
|
74
|
+
// yield* Effect.logDebug('ServerAheadError: backend head mismatch', {
|
|
75
|
+
// expectedHead: currentHead,
|
|
76
|
+
// providedHead: firstEventParent,
|
|
77
|
+
// batchSize: pushRequest.batch.length,
|
|
78
|
+
// backendId,
|
|
79
|
+
// })
|
|
80
|
+
|
|
54
81
|
return yield* new ServerAheadError({ minimumExpectedNum: currentHead, providedNum: firstEventParent })
|
|
55
82
|
}
|
|
56
83
|
|
|
@@ -68,40 +95,71 @@ export const makePush =
|
|
|
68
95
|
yield* Effect.gen(function* () {
|
|
69
96
|
const connectedClients = ctx.getWebSockets()
|
|
70
97
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
// Preparing chunks of responses to make sure we don't exceed the WS message size limit.
|
|
99
|
+
const responses = yield* Chunk.fromIterable(pushRequest.batch).pipe(
|
|
100
|
+
splitChunkBySize({
|
|
101
|
+
maxItems: MAX_PUSH_EVENTS_PER_REQUEST,
|
|
102
|
+
maxBytes: MAX_WS_MESSAGE_BYTES,
|
|
103
|
+
encode: (items) =>
|
|
104
|
+
encodePullResponse(
|
|
105
|
+
SyncMessage.PullResponse.make({
|
|
106
|
+
batch: items.map(
|
|
107
|
+
(eventEncoded): PullBatchItem => ({
|
|
108
|
+
eventEncoded,
|
|
109
|
+
metadata: Option.some(SyncMessage.SyncMetadata.make({ createdAt })),
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
pageInfo: SyncBackend.pageInfoNoMore,
|
|
113
|
+
backendId,
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
}),
|
|
117
|
+
Effect.map(
|
|
118
|
+
Chunk.map((eventsChunk) => {
|
|
119
|
+
const batchWithMetadata = Chunk.toReadonlyArray(eventsChunk).map((eventEncoded) => ({
|
|
120
|
+
eventEncoded,
|
|
121
|
+
metadata: Option.some(SyncMessage.SyncMetadata.make({ createdAt })),
|
|
122
|
+
}))
|
|
123
|
+
|
|
124
|
+
const response = SyncMessage.PullResponse.make({
|
|
125
|
+
batch: batchWithMetadata,
|
|
126
|
+
pageInfo: SyncBackend.pageInfoNoMore,
|
|
127
|
+
backendId,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
response,
|
|
132
|
+
encoded: Schema.encodeSync(SyncMessage.PullResponse)(response),
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
80
137
|
|
|
81
|
-
|
|
138
|
+
// Dual broadcasting: WebSocket + RPC clients
|
|
82
139
|
|
|
83
140
|
// Broadcast to WebSocket clients
|
|
84
141
|
if (connectedClients.length > 0) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
142
|
+
for (const { response, encoded } of responses) {
|
|
143
|
+
// Only calling once for now.
|
|
144
|
+
if (options?.onPullRes !== undefined) {
|
|
145
|
+
yield* Effect.tryAll(() => options.onPullRes!(response)).pipe(UnknownError.mapToUnknownError)
|
|
146
|
+
}
|
|
89
147
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
148
|
+
// NOTE we're also sending the pullRes chunk to the pushing ws client as confirmation
|
|
149
|
+
for (const conn of connectedClients) {
|
|
150
|
+
const attachment = yield* Schema.decode(WebSocketAttachmentSchema)(conn.deserializeAttachment())
|
|
151
|
+
|
|
152
|
+
// We're doing something a bit "advanced" here as we're directly emitting Effect RPC-compatible
|
|
153
|
+
// response messsages on the Effect RPC-managed websocket connection to the WS client.
|
|
154
|
+
// For this we need to get the RPC `requestId` from the WebSocket attachment.
|
|
155
|
+
for (const requestId of attachment.pullRequestIds) {
|
|
156
|
+
const res: RpcMessage.ResponseChunkEncoded = {
|
|
157
|
+
_tag: 'Chunk',
|
|
158
|
+
requestId,
|
|
159
|
+
values: [encoded],
|
|
160
|
+
}
|
|
161
|
+
conn.send(jsonStringify(res))
|
|
103
162
|
}
|
|
104
|
-
conn.send(JSON.stringify(res))
|
|
105
163
|
}
|
|
106
164
|
}
|
|
107
165
|
|
|
@@ -110,17 +168,16 @@ export const makePush =
|
|
|
110
168
|
|
|
111
169
|
// RPC broadcasting would require reconstructing client stubs from clientIds
|
|
112
170
|
if (rpcSubscriptions.size > 0) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
emitStreamResponse({
|
|
171
|
+
for (const subscription of rpcSubscriptions.values()) {
|
|
172
|
+
for (const { encoded } of responses) {
|
|
173
|
+
yield* emitStreamResponse({
|
|
117
174
|
callerContext: subscription.callerContext,
|
|
118
175
|
env,
|
|
119
176
|
requestId: subscription.requestId,
|
|
120
|
-
values: [
|
|
121
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.exit)
|
|
122
|
-
|
|
123
|
-
|
|
177
|
+
values: [encoded],
|
|
178
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.exit)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
124
181
|
|
|
125
182
|
yield* Effect.logDebug(`Broadcasted to ${rpcSubscriptions.size} RPC clients`)
|
|
126
183
|
}
|
|
@@ -138,12 +195,16 @@ export const makePush =
|
|
|
138
195
|
}).pipe(
|
|
139
196
|
Effect.tap(
|
|
140
197
|
Effect.fn(function* (message) {
|
|
141
|
-
if (options?.onPushRes) {
|
|
142
|
-
yield* Effect.tryAll(() => options.onPushRes!(message)).pipe(
|
|
198
|
+
if (options?.onPushRes !== undefined) {
|
|
199
|
+
yield* Effect.tryAll(() => options.onPushRes!(message)).pipe(UnknownError.mapToUnknownError)
|
|
143
200
|
}
|
|
144
201
|
}),
|
|
145
202
|
),
|
|
146
|
-
Effect.mapError((cause) =>
|
|
203
|
+
Effect.mapError((cause) =>
|
|
204
|
+
cause._tag === 'BackendIdMismatchError' || cause._tag === 'ServerAheadError' || cause._tag === 'UnknownError'
|
|
205
|
+
? cause
|
|
206
|
+
: new UnknownError({ cause }),
|
|
207
|
+
),
|
|
147
208
|
Effect.withSpan('sync-cf:do:push', { attributes: { storeId, batchSize: pushRequest.batch.length } }),
|
|
148
209
|
)
|
|
149
210
|
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { EventSequenceNumber, State } from '@livestore/common/schema'
|
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
|
3
|
+
|
|
3
4
|
import { PERSISTENCE_FORMAT_VERSION } from '../shared.ts'
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Main event log table storing all LiveStore events.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ IMPORTANT: Any changes to this schema require bumping PERSISTENCE_FORMAT_VERSION in shared.ts
|
|
10
|
+
*/
|
|
5
11
|
export const eventlogTable = State.SQLite.table({
|
|
6
12
|
// NOTE actual table name is determined at runtime to use proper storeId
|
|
7
13
|
name: `eventlog_${PERSISTENCE_FORMAT_VERSION}_$storeId`,
|
|
8
14
|
columns: {
|
|
9
|
-
seqNum: State.SQLite.integer({ primaryKey: true, schema: EventSequenceNumber.
|
|
10
|
-
parentSeqNum: State.SQLite.integer({ schema: EventSequenceNumber.
|
|
15
|
+
seqNum: State.SQLite.integer({ primaryKey: true, schema: EventSequenceNumber.Global.Schema }),
|
|
16
|
+
parentSeqNum: State.SQLite.integer({ schema: EventSequenceNumber.Global.Schema }),
|
|
11
17
|
name: State.SQLite.text({}),
|
|
12
18
|
args: State.SQLite.text({ schema: Schema.parseJson(Schema.Any), nullable: true }),
|
|
13
19
|
/** ISO date format. Currently only used for debugging purposes. */
|
|
@@ -17,12 +23,16 @@ export const eventlogTable = State.SQLite.table({
|
|
|
17
23
|
},
|
|
18
24
|
})
|
|
19
25
|
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Context metadata table - one row per Durable Object.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ IMPORTANT: Any changes to this schema require bumping PERSISTENCE_FORMAT_VERSION in shared.ts
|
|
30
|
+
*/
|
|
21
31
|
export const contextTable = State.SQLite.table({
|
|
22
32
|
name: `context_${PERSISTENCE_FORMAT_VERSION}`,
|
|
23
33
|
columns: {
|
|
24
34
|
storeId: State.SQLite.text({ primaryKey: true }),
|
|
25
|
-
currentHead: State.SQLite.integer({ schema: EventSequenceNumber.
|
|
35
|
+
currentHead: State.SQLite.integer({ schema: EventSequenceNumber.Global.Schema }),
|
|
26
36
|
backendId: State.SQLite.text({}),
|
|
27
37
|
},
|
|
28
38
|
})
|