@livestore/common 0.3.0-dev.4 → 0.3.0-dev.6
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/adapter-types.d.ts +26 -23
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/derived-mutations.d.ts +4 -4
- package/dist/derived-mutations.d.ts.map +1 -1
- package/dist/derived-mutations.test.js.map +1 -1
- package/dist/devtools/devtools-bridge.d.ts +2 -1
- package/dist/devtools/devtools-bridge.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.d.ts +90 -102
- package/dist/devtools/devtools-messages.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.js +9 -6
- package/dist/devtools/devtools-messages.js.map +1 -1
- package/dist/leader-thread/apply-mutation.d.ts +5 -2
- package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
- package/dist/leader-thread/apply-mutation.js +37 -25
- package/dist/leader-thread/apply-mutation.js.map +1 -1
- package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -1
- package/dist/leader-thread/leader-sync-processor.js +20 -12
- package/dist/leader-thread/leader-sync-processor.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +22 -66
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +4 -2
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +5 -2
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/mutationlog.d.ts +4 -17
- package/dist/leader-thread/mutationlog.d.ts.map +1 -1
- package/dist/leader-thread/mutationlog.js +2 -1
- package/dist/leader-thread/mutationlog.js.map +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +9 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +7 -5
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/mutation.d.ts +9 -2
- package/dist/mutation.d.ts.map +1 -1
- package/dist/mutation.js +5 -5
- package/dist/mutation.js.map +1 -1
- package/dist/query-builder/impl.d.ts +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts +2 -2
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +13 -19
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/EventId.d.ts +16 -14
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +15 -7
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/MutationEvent.d.ts +49 -74
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +29 -13
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/system-tables.d.ts +26 -26
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +19 -11
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema-management/migrations.js +6 -6
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sync/client-session-sync-processor.d.ts +4 -4
- package/dist/sync/client-session-sync-processor.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +1 -4
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +1 -1
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +3 -3
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +3 -2
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/mutation-fixtures.js +3 -9
- package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
- package/dist/sync/sync.d.ts +6 -6
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +10 -10
- package/dist/sync/syncstate.test.js +2 -6
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/validate-push-payload.d.ts +2 -2
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +2 -2
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
- package/src/adapter-types.ts +22 -24
- package/src/derived-mutations.test.ts +1 -1
- package/src/derived-mutations.ts +9 -5
- package/src/devtools/devtools-bridge.ts +2 -1
- package/src/devtools/devtools-messages.ts +9 -6
- package/src/leader-thread/apply-mutation.ts +48 -30
- package/src/leader-thread/leader-sync-processor.ts +26 -15
- package/src/leader-thread/leader-worker-devtools.ts +30 -109
- package/src/leader-thread/make-leader-thread-layer.ts +15 -5
- package/src/leader-thread/mutationlog.ts +9 -5
- package/src/leader-thread/recreate-db.ts +9 -5
- package/src/leader-thread/types.ts +7 -8
- package/src/mutation.ts +17 -7
- package/src/rehydrate-from-mutationlog.ts +15 -23
- package/src/schema/EventId.ts +23 -9
- package/src/schema/MutationEvent.ts +40 -20
- package/src/schema/system-tables.ts +19 -11
- package/src/schema-management/migrations.ts +6 -6
- package/src/sync/client-session-sync-processor.ts +4 -4
- package/src/sync/next/history-dag-common.ts +1 -1
- package/src/sync/next/rebase-events.ts +7 -7
- package/src/sync/next/test/mutation-fixtures.ts +3 -10
- package/src/sync/sync.ts +4 -2
- package/src/sync/syncstate.test.ts +4 -4
- package/src/sync/validate-push-payload.ts +7 -4
- package/src/version.ts +1 -1
@@ -1,8 +1,9 @@
|
|
1
|
-
import type { HttpClient, Scope
|
2
|
-
import { Deferred, Effect,
|
1
|
+
import type { HttpClient, Scope } from '@livestore/utils/effect'
|
2
|
+
import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
|
3
3
|
|
4
4
|
import type { BootStatus, MakeSynchronousDatabase, SqliteError, SynchronousDatabase } from '../adapter-types.js'
|
5
5
|
import { UnexpectedError } from '../adapter-types.js'
|
6
|
+
import type * as Devtools from '../devtools/index.js'
|
6
7
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
7
8
|
import { EventId, MutationEvent, mutationLogMetaTable, SYNC_STATUS_TABLE, syncStatusTable } from '../schema/mod.js'
|
8
9
|
import { migrateTable } from '../schema-management/migrations.js'
|
@@ -13,22 +14,24 @@ import { makeLeaderSyncProcessor } from './leader-sync-processor.js'
|
|
13
14
|
import { bootDevtools } from './leader-worker-devtools.js'
|
14
15
|
import { makePullQueueSet } from './pull-queue-set.js'
|
15
16
|
import { recreateDb } from './recreate-db.js'
|
17
|
+
import type { ShutdownChannel } from './shutdown-channel.js'
|
16
18
|
import type { DevtoolsOptions, InitialBlockingSyncContext, InitialSyncOptions, ShutdownState } from './types.js'
|
17
19
|
import { LeaderThreadCtx } from './types.js'
|
18
20
|
|
19
21
|
export const makeLeaderThreadLayer = ({
|
20
22
|
schema,
|
21
23
|
storeId,
|
22
|
-
|
24
|
+
clientId,
|
23
25
|
makeSyncDb,
|
24
26
|
makeSyncBackend,
|
25
27
|
db,
|
26
28
|
dbLog,
|
27
29
|
devtoolsOptions,
|
28
30
|
initialSyncOptions = { _tag: 'Skip' },
|
31
|
+
shutdownChannel,
|
29
32
|
}: {
|
30
33
|
storeId: string
|
31
|
-
|
34
|
+
clientId: string
|
32
35
|
schema: LiveStoreSchema
|
33
36
|
makeSyncDb: MakeSynchronousDatabase
|
34
37
|
makeSyncBackend: Effect.Effect<SyncBackend, UnexpectedError, Scope.Scope> | undefined
|
@@ -36,6 +39,7 @@ export const makeLeaderThreadLayer = ({
|
|
36
39
|
dbLog: SynchronousDatabase
|
37
40
|
devtoolsOptions: DevtoolsOptions
|
38
41
|
initialSyncOptions: InitialSyncOptions | undefined
|
42
|
+
shutdownChannel: ShutdownChannel
|
39
43
|
}): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
|
40
44
|
Effect.gen(function* () {
|
41
45
|
const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
|
@@ -50,19 +54,25 @@ export const makeLeaderThreadLayer = ({
|
|
50
54
|
|
51
55
|
const syncProcessor = yield* makeLeaderSyncProcessor({ schema, dbMissing, dbLog, initialBlockingSyncContext })
|
52
56
|
|
57
|
+
const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.MessageToAppLeader>().pipe(
|
58
|
+
Effect.acquireRelease(Queue.shutdown),
|
59
|
+
)
|
60
|
+
|
53
61
|
const ctx = {
|
54
62
|
schema,
|
55
63
|
bootStatusQueue,
|
56
64
|
storeId,
|
57
|
-
|
65
|
+
clientId,
|
58
66
|
db,
|
59
67
|
dbLog,
|
60
68
|
makeSyncDb,
|
61
69
|
mutationEventSchema: MutationEvent.makeMutationEventSchema(schema),
|
62
70
|
shutdownStateSubRef: yield* SubscriptionRef.make<ShutdownState>('running'),
|
71
|
+
shutdownChannel,
|
63
72
|
syncBackend,
|
64
73
|
syncProcessor,
|
65
74
|
connectedClientSessionPullQueues: yield* makePullQueueSet,
|
75
|
+
extraIncomingMessagesQueue,
|
66
76
|
} satisfies typeof LeaderThreadCtx.Service
|
67
77
|
|
68
78
|
// @ts-expect-error For debugging purposes
|
@@ -2,11 +2,14 @@ import { Effect, Schema } from '@livestore/utils/effect'
|
|
2
2
|
|
3
3
|
import type { SynchronousDatabase } from '../adapter-types.js'
|
4
4
|
import * as EventId from '../schema/EventId.js'
|
5
|
+
import type * as MutationEvent from '../schema/MutationEvent.js'
|
5
6
|
import { MUTATION_LOG_META_TABLE, mutationLogMetaTable, SYNC_STATUS_TABLE } from '../schema/system-tables.js'
|
6
7
|
import { prepareBindValues, sql } from '../util.js'
|
7
8
|
import { LeaderThreadCtx } from './types.js'
|
8
9
|
|
9
|
-
export const getMutationEventsSince = (
|
10
|
+
export const getMutationEventsSince = (
|
11
|
+
since: EventId.EventId,
|
12
|
+
): Effect.Effect<ReadonlyArray<MutationEvent.AnyEncoded>, never, LeaderThreadCtx> =>
|
10
13
|
Effect.gen(function* () {
|
11
14
|
const { dbLog } = yield* LeaderThreadCtx
|
12
15
|
|
@@ -26,16 +29,17 @@ export const getMutationEventsSince = (since: EventId.EventId) =>
|
|
26
29
|
.filter((_) => EventId.compare(_.id, since) > 0)
|
27
30
|
})
|
28
31
|
|
29
|
-
export const getLocalHeadFromDb = (dbLog: SynchronousDatabase) => {
|
30
|
-
const res = dbLog.select<{ idGlobal:
|
32
|
+
export const getLocalHeadFromDb = (dbLog: SynchronousDatabase): EventId.EventId => {
|
33
|
+
const res = dbLog.select<{ idGlobal: EventId.GlobalEventId; idLocal: EventId.LocalEventId }>(
|
31
34
|
sql`select idGlobal, idLocal from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idLocal DESC limit 1`,
|
32
35
|
)[0]
|
33
36
|
|
34
37
|
return res ? { global: res.idGlobal, local: res.idLocal } : EventId.ROOT
|
35
38
|
}
|
36
39
|
|
37
|
-
export const getBackendHeadFromDb = (dbLog: SynchronousDatabase) =>
|
38
|
-
dbLog.select<{ head:
|
40
|
+
export const getBackendHeadFromDb = (dbLog: SynchronousDatabase): EventId.GlobalEventId =>
|
41
|
+
dbLog.select<{ head: EventId.GlobalEventId }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ??
|
42
|
+
EventId.ROOT.global
|
39
43
|
|
40
44
|
// TODO use prepared statements
|
41
45
|
export const updateBackendHead = (dbLog: SynchronousDatabase, head: EventId.EventId) =>
|
@@ -24,7 +24,9 @@ export const recreateDb: Effect.Effect<
|
|
24
24
|
|
25
25
|
// NOTE to speed up the operations below, we're creating a temporary in-memory database
|
26
26
|
// and later we'll overwrite the persisted database with the new data
|
27
|
-
|
27
|
+
// TODO bring back this optimization
|
28
|
+
// const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
|
29
|
+
const tmpSyncDb = db
|
28
30
|
yield* configureConnection(tmpSyncDb, { fkEnabled: true })
|
29
31
|
|
30
32
|
const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
|
@@ -91,17 +93,19 @@ export const recreateDb: Effect.Effect<
|
|
91
93
|
}
|
92
94
|
}
|
93
95
|
|
96
|
+
// TODO bring back
|
94
97
|
// Import the temporary in-memory database into the persistent database
|
95
|
-
yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
|
96
|
-
|
97
|
-
)
|
98
|
+
// yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
|
99
|
+
// Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
|
100
|
+
// )
|
98
101
|
|
99
102
|
// TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
|
100
103
|
// We've disabled this for now as it made the code too complex, as we often run syncing right after
|
101
104
|
// so the snapshot is no longer up to date
|
102
105
|
// const snapshotFromTmpDb = tmpSyncDb.export()
|
103
106
|
|
104
|
-
|
107
|
+
// TODO bring back
|
108
|
+
// tmpSyncDb.close()
|
105
109
|
}).pipe(
|
106
110
|
Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
|
107
111
|
Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
|
@@ -22,7 +22,7 @@ import type {
|
|
22
22
|
UnexpectedError,
|
23
23
|
} from '../index.js'
|
24
24
|
import type { EventId, LiveStoreSchema, MutationEvent } from '../schema/mod.js'
|
25
|
-
import type
|
25
|
+
import type * as SyncState from '../sync/syncstate.js'
|
26
26
|
import type { ShutdownChannel } from './shutdown-channel.js'
|
27
27
|
|
28
28
|
export type ShutdownState = 'running' | 'shutting-down'
|
@@ -67,7 +67,6 @@ export type DevtoolsOptions =
|
|
67
67
|
makeContext: Effect.Effect<
|
68
68
|
{
|
69
69
|
devtoolsWebChannel: WebChannel.WebChannel<Devtools.MessageToAppLeader, Devtools.MessageFromAppLeader>
|
70
|
-
shutdownChannel: ShutdownChannel
|
71
70
|
persistenceInfo: PersistenceInfoPair
|
72
71
|
},
|
73
72
|
UnexpectedError,
|
@@ -80,18 +79,21 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
|
|
80
79
|
{
|
81
80
|
schema: LiveStoreSchema
|
82
81
|
storeId: string
|
83
|
-
|
82
|
+
clientId: string
|
84
83
|
makeSyncDb: MakeSynchronousDatabase
|
85
84
|
db: LeaderDatabase
|
86
85
|
dbLog: LeaderDatabase
|
87
86
|
bootStatusQueue: Queue.Queue<BootStatus>
|
88
87
|
// TODO we should find a more elegant way to handle cases which need this ref for their implementation
|
89
88
|
shutdownStateSubRef: SubscriptionRef.SubscriptionRef<ShutdownState>
|
89
|
+
shutdownChannel: ShutdownChannel
|
90
90
|
mutationEventSchema: MutationEvent.ForMutationDefRecord<any>
|
91
91
|
// devtools: DevtoolsContext
|
92
92
|
syncBackend: SyncBackend | undefined
|
93
93
|
syncProcessor: SyncProcessor
|
94
94
|
connectedClientSessionPullQueues: PullQueueSet
|
95
|
+
/** e.g. used for `store.__dev` APIs */
|
96
|
+
extraIncomingMessagesQueue: Queue.Queue<Devtools.MessageToAppLeader>
|
95
97
|
}
|
96
98
|
>() {}
|
97
99
|
|
@@ -101,10 +103,7 @@ export type InitialBlockingSyncContext = {
|
|
101
103
|
}
|
102
104
|
|
103
105
|
export type PullQueueItem = {
|
104
|
-
|
105
|
-
// backendHead: number
|
106
|
-
payload: PayloadUpstream
|
107
|
-
// TODO move `remaining` into `PayloadUpstream`
|
106
|
+
payload: SyncState.PayloadUpstream
|
108
107
|
remaining: number
|
109
108
|
}
|
110
109
|
|
@@ -118,7 +117,7 @@ export interface SyncProcessor {
|
|
118
117
|
boot: (args: {
|
119
118
|
dbReady: Deferred.Deferred<void>
|
120
119
|
}) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient>
|
121
|
-
syncState: Effect.Effect<SyncState, UnexpectedError>
|
120
|
+
syncState: Effect.Effect<SyncState.SyncState, UnexpectedError>
|
122
121
|
}
|
123
122
|
|
124
123
|
export interface PullQueueSet {
|
package/src/mutation.ts
CHANGED
@@ -8,10 +8,19 @@ import { prepareBindValues } from './util.js'
|
|
8
8
|
|
9
9
|
export const getExecArgsFromMutation = ({
|
10
10
|
mutationDef,
|
11
|
-
|
11
|
+
mutationEvent,
|
12
12
|
}: {
|
13
13
|
mutationDef: MutationDef.Any
|
14
|
-
|
14
|
+
/** Both encoded and decoded mutation events are supported to reduce the number of times we need to decode/encode */
|
15
|
+
mutationEvent:
|
16
|
+
| {
|
17
|
+
decoded: MutationEvent.AnyDecoded | MutationEvent.PartialAnyDecoded
|
18
|
+
encoded: undefined
|
19
|
+
}
|
20
|
+
| {
|
21
|
+
decoded: undefined
|
22
|
+
encoded: MutationEvent.AnyEncoded | MutationEvent.PartialAnyEncoded
|
23
|
+
}
|
15
24
|
}): ReadonlyArray<{
|
16
25
|
statementSql: string
|
17
26
|
bindValues: PreparedBindValues
|
@@ -23,7 +32,9 @@ export const getExecArgsFromMutation = ({
|
|
23
32
|
|
24
33
|
switch (typeof mutationDef.sql) {
|
25
34
|
case 'function': {
|
26
|
-
const
|
35
|
+
const mutationArgsDecoded =
|
36
|
+
mutationEvent.decoded?.args ?? Schema.decodeUnknownSync(mutationDef.schema)(mutationEvent.encoded!.args)
|
37
|
+
const res = mutationDef.sql(mutationArgsDecoded)
|
27
38
|
statementRes = Array.isArray(res) ? res : [res]
|
28
39
|
break
|
29
40
|
}
|
@@ -40,10 +51,9 @@ export const getExecArgsFromMutation = ({
|
|
40
51
|
return statementRes.map((statementRes) => {
|
41
52
|
const statementSql = typeof statementRes === 'string' ? statementRes : statementRes.sql
|
42
53
|
|
43
|
-
const
|
44
|
-
|
45
|
-
|
46
|
-
: statementRes.bindValues
|
54
|
+
const mutationArgsEncoded =
|
55
|
+
mutationEvent.encoded?.args ?? Schema.encodeUnknownSync(mutationDef.schema)(mutationEvent.decoded!.args)
|
56
|
+
const bindValues = typeof statementRes === 'string' ? mutationArgsEncoded : statementRes.bindValues
|
47
57
|
|
48
58
|
const writeTables = typeof statementRes === 'string' ? undefined : statementRes.writeTables
|
49
59
|
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
2
2
|
import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
|
3
3
|
|
4
4
|
import { type MigrationOptionsFromMutationLog, type SynchronousDatabase, UnexpectedError } from './adapter-types.js'
|
5
|
-
import {
|
5
|
+
import { makeApplyMutation } from './leader-thread/apply-mutation.js'
|
6
6
|
import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/mod.js'
|
7
7
|
import { EventId, MUTATION_LOG_META_TABLE } from './schema/mod.js'
|
8
8
|
import type { PreparedBindValues } from './util.js'
|
@@ -10,7 +10,8 @@ import { sql } from './util.js'
|
|
10
10
|
|
11
11
|
export const rehydrateFromMutationLog = ({
|
12
12
|
logDb,
|
13
|
-
db
|
13
|
+
// TODO re-use this db when bringing back the boot in-memory db implementation
|
14
|
+
// db,
|
14
15
|
schema,
|
15
16
|
migrationOptions,
|
16
17
|
onProgress,
|
@@ -28,6 +29,8 @@ export const rehydrateFromMutationLog = ({
|
|
28
29
|
|
29
30
|
const hashMutation = memoizeByRef((mutation: MutationDef.Any) => Schema.hash(mutation.schema))
|
30
31
|
|
32
|
+
const applyMutation = yield* makeApplyMutation
|
33
|
+
|
31
34
|
const processMutation = (row: MutationLogMetaRow) =>
|
32
35
|
Effect.gen(function* () {
|
33
36
|
const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
|
@@ -40,7 +43,10 @@ export const rehydrateFromMutationLog = ({
|
|
40
43
|
)
|
41
44
|
}
|
42
45
|
|
43
|
-
const
|
46
|
+
const args = JSON.parse(row.argsJson)
|
47
|
+
|
48
|
+
// Checking whether the schema has changed in an incompatible way
|
49
|
+
yield* Schema.decodeUnknown(mutationDef.schema)(args).pipe(
|
44
50
|
Effect.mapError((cause) =>
|
45
51
|
UnexpectedError.make({
|
46
52
|
cause,
|
@@ -53,28 +59,14 @@ This likely means the schema has changed in an incompatible way.
|
|
53
59
|
),
|
54
60
|
)
|
55
61
|
|
56
|
-
const
|
62
|
+
const mutationEventEncoded = {
|
57
63
|
id: { global: row.idGlobal, local: row.idLocal },
|
58
64
|
parentId: { global: row.parentIdGlobal, local: row.parentIdLocal },
|
59
65
|
mutation: row.mutation,
|
60
|
-
args
|
61
|
-
} satisfies MutationEvent.
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
const makeExecuteOptions = (statementSql: string, bindValues: any) => ({
|
66
|
-
onRowsChanged: (rowsChanged: number) => {
|
67
|
-
if (rowsChanged === 0 && migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true) {
|
68
|
-
console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
|
69
|
-
}
|
70
|
-
},
|
71
|
-
})
|
72
|
-
|
73
|
-
for (const { statementSql, bindValues } of execArgsArr) {
|
74
|
-
// TODO cache prepared statements for mutations
|
75
|
-
db.execute(statementSql, bindValues, isDevEnv() ? makeExecuteOptions(statementSql, bindValues) : undefined)
|
76
|
-
// console.log(`Re-executed mutation ${mutationSql}`, bindValues)
|
77
|
-
}
|
66
|
+
args,
|
67
|
+
} satisfies MutationEvent.AnyEncoded
|
68
|
+
|
69
|
+
yield* applyMutation(mutationEventEncoded, { skipMutationLog: true })
|
78
70
|
}).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
|
79
71
|
|
80
72
|
const CHUNK_SIZE = 100
|
package/src/schema/EventId.ts
CHANGED
@@ -1,4 +1,14 @@
|
|
1
|
-
import { Schema } from '@livestore/utils/effect'
|
1
|
+
import { Brand, Schema } from '@livestore/utils/effect'
|
2
|
+
|
3
|
+
export type LocalEventId = Brand.Branded<number, 'LocalEventId'>
|
4
|
+
export const localEventId = Brand.nominal<LocalEventId>()
|
5
|
+
export const LocalEventId = Schema.fromBrand(localEventId)(Schema.Int)
|
6
|
+
|
7
|
+
export type GlobalEventId = Brand.Branded<number, 'GlobalEventId'>
|
8
|
+
export const globalEventId = Brand.nominal<GlobalEventId>()
|
9
|
+
export const GlobalEventId = Schema.fromBrand(globalEventId)(Schema.Int)
|
10
|
+
|
11
|
+
export const localDefault = 0 as any as LocalEventId
|
2
12
|
|
3
13
|
/**
|
4
14
|
* LiveStore event id value consisting of a globally unique event sequence number
|
@@ -6,11 +16,11 @@ import { Schema } from '@livestore/utils/effect'
|
|
6
16
|
*
|
7
17
|
* The local sequence number is only used for localOnly mutations and starts from 0 for each global sequence number.
|
8
18
|
*/
|
9
|
-
export type EventId = { global:
|
19
|
+
export type EventId = { global: GlobalEventId; local: LocalEventId }
|
10
20
|
|
11
21
|
export const EventId = Schema.Struct({
|
12
|
-
global:
|
13
|
-
local:
|
22
|
+
global: GlobalEventId,
|
23
|
+
local: LocalEventId,
|
14
24
|
}).annotations({ title: 'LiveStore.EventId' })
|
15
25
|
|
16
26
|
/**
|
@@ -27,20 +37,24 @@ export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.lo
|
|
27
37
|
|
28
38
|
export type EventIdPair = { id: EventId; parentId: EventId }
|
29
39
|
|
30
|
-
export const ROOT = { global: -1, local:
|
40
|
+
export const ROOT = { global: -1 as any as GlobalEventId, local: localDefault } satisfies EventId
|
31
41
|
|
32
42
|
export const isGreaterThan = (a: EventId, b: EventId) => {
|
33
43
|
return a.global > b.global || (a.global === b.global && a.local > b.local)
|
34
44
|
}
|
35
45
|
|
36
|
-
export const
|
46
|
+
export const make = (id: EventId | typeof EventId.Encoded): EventId => {
|
47
|
+
return Schema.is(EventId)(id) ? id : Schema.decodeSync(EventId)(id)
|
48
|
+
}
|
49
|
+
|
50
|
+
export const nextPair = (id: EventId, isLocal: boolean): EventIdPair => {
|
37
51
|
if (isLocal) {
|
38
|
-
return { id: { global: id.global, local: id.local + 1 }, parentId: id }
|
52
|
+
return { id: { global: id.global, local: (id.local + 1) as any as LocalEventId }, parentId: id }
|
39
53
|
}
|
40
54
|
|
41
55
|
return {
|
42
|
-
id: { global: id.global + 1, local:
|
56
|
+
id: { global: (id.global + 1) as any as GlobalEventId, local: localDefault },
|
43
57
|
// NOTE we always point to `local: 0` for non-localOnly mutations
|
44
|
-
parentId: { global: id.global, local:
|
58
|
+
parentId: { global: id.global, local: localDefault },
|
45
59
|
}
|
46
60
|
}
|
@@ -30,10 +30,31 @@ export type MutationEventEncoded<TMutationsDef extends MutationDef.Any> = {
|
|
30
30
|
parentId: EventId.EventId
|
31
31
|
}
|
32
32
|
|
33
|
-
export type
|
33
|
+
export type AnyDecoded = MutationEvent<MutationDef.Any>
|
34
|
+
export const AnyDecoded = Schema.Struct({
|
35
|
+
mutation: Schema.String,
|
36
|
+
args: Schema.Any,
|
37
|
+
id: EventId.EventId,
|
38
|
+
parentId: EventId.EventId,
|
39
|
+
}).annotations({ title: 'MutationEvent.AnyDecoded' })
|
40
|
+
|
34
41
|
export type AnyEncoded = MutationEventEncoded<MutationDef.Any>
|
42
|
+
export const AnyEncoded = Schema.Struct({
|
43
|
+
mutation: Schema.String,
|
44
|
+
args: Schema.Any,
|
45
|
+
id: EventId.EventId,
|
46
|
+
parentId: EventId.EventId,
|
47
|
+
}).annotations({ title: 'MutationEvent.AnyEncoded' })
|
48
|
+
|
49
|
+
export const AnyEncodedGlobal = Schema.Struct({
|
50
|
+
mutation: Schema.String,
|
51
|
+
args: Schema.Any,
|
52
|
+
id: EventId.GlobalEventId,
|
53
|
+
parentId: EventId.GlobalEventId,
|
54
|
+
}).annotations({ title: 'MutationEvent.AnyEncodedGlobal' })
|
55
|
+
export type AnyEncodedGlobal = typeof AnyEncodedGlobal.Type
|
35
56
|
|
36
|
-
export type
|
57
|
+
export type PartialAnyDecoded = MutationEventPartial<MutationDef.Any>
|
37
58
|
export type PartialAnyEncoded = MutationEventPartialEncoded<MutationDef.Any>
|
38
59
|
|
39
60
|
export type PartialForSchema<TSchema extends LiveStoreSchema> = {
|
@@ -44,8 +65,9 @@ export type ForSchema<TSchema extends LiveStoreSchema> = {
|
|
44
65
|
[K in keyof TSchema['_MutationDefMapType']]: MutationEvent<TSchema['_MutationDefMapType'][K]>
|
45
66
|
}[keyof TSchema['_MutationDefMapType']]
|
46
67
|
|
47
|
-
export const isPartialMutationEvent = (
|
48
|
-
|
68
|
+
export const isPartialMutationEvent = (
|
69
|
+
mutationEvent: AnyDecoded | PartialAnyDecoded,
|
70
|
+
): mutationEvent is PartialAnyDecoded => 'id' in mutationEvent === false && 'parentId' in mutationEvent === false
|
49
71
|
|
50
72
|
export type ForMutationDefRecord<TMutationsDefRecord extends MutationDefRecord> = Schema.Schema<
|
51
73
|
{
|
@@ -109,22 +131,7 @@ export const makeMutationEventPartialSchema = <TSchema extends LiveStoreSchema>(
|
|
109
131
|
|
110
132
|
export const makeMutationEventSchemaMemo = memoizeByRef(makeMutationEventSchema)
|
111
133
|
|
112
|
-
|
113
|
-
mutation: Schema.String,
|
114
|
-
args: Schema.Any,
|
115
|
-
id: EventId.EventId,
|
116
|
-
parentId: EventId.EventId,
|
117
|
-
}).annotations({ title: 'MutationEvent.Any' })
|
118
|
-
|
119
|
-
export const DecodedAny = Schema.typeSchema(Any).annotations({
|
120
|
-
title: 'MutationEvent.DecodedAny',
|
121
|
-
})
|
122
|
-
|
123
|
-
export const EncodedAny = Schema.encodedSchema(Any).annotations({
|
124
|
-
title: 'MutationEvent.EncodedAny',
|
125
|
-
})
|
126
|
-
|
127
|
-
/** Equivalent to EncodedAny but with a meta field and some convenience methods */
|
134
|
+
/** Equivalent to AnyEncoded but with a meta field and some convenience methods */
|
128
135
|
export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEvent.EncodedWithMeta')({
|
129
136
|
mutation: Schema.String,
|
130
137
|
args: Schema.Any,
|
@@ -151,6 +158,19 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
151
158
|
...this,
|
152
159
|
...EventId.nextPair(this.id, isLocal),
|
153
160
|
})
|
161
|
+
|
162
|
+
static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
|
163
|
+
new EncodedWithMeta({
|
164
|
+
...mutationEvent,
|
165
|
+
id: { global: mutationEvent.id, local: EventId.localDefault },
|
166
|
+
parentId: { global: mutationEvent.parentId, local: EventId.localDefault },
|
167
|
+
})
|
168
|
+
|
169
|
+
toGlobal = (): AnyEncodedGlobal => ({
|
170
|
+
...this,
|
171
|
+
id: this.id.global,
|
172
|
+
parentId: this.parentId.global,
|
173
|
+
})
|
154
174
|
}
|
155
175
|
|
156
176
|
export const isEqualEncoded = (a: AnyEncoded, b: AnyEncoded) =>
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { type SqliteAst as __SqliteAst, SqliteDsl } from '@livestore/db-schema'
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
+
import * as EventId from './EventId.js'
|
4
5
|
import type { FromTable } from './table-def.js'
|
5
6
|
import { table } from './table-def.js'
|
6
7
|
|
@@ -46,14 +47,15 @@ export const sessionChangesetMetaTable = table(
|
|
46
47
|
SESSION_CHANGESET_META_TABLE,
|
47
48
|
{
|
48
49
|
// TODO bring back primary key
|
49
|
-
idGlobal: SqliteDsl.integer({}),
|
50
|
-
idLocal: SqliteDsl.integer({}),
|
51
|
-
|
52
|
-
// idLocal: SqliteDsl.integer({ primaryKey: true }),
|
53
|
-
changeset: SqliteDsl.blob({}),
|
50
|
+
idGlobal: SqliteDsl.integer({ schema: EventId.GlobalEventId }),
|
51
|
+
idLocal: SqliteDsl.integer({ schema: EventId.LocalEventId }),
|
52
|
+
changeset: SqliteDsl.blob({ nullable: true }),
|
54
53
|
debug: SqliteDsl.json({ nullable: true }),
|
55
54
|
},
|
56
|
-
{
|
55
|
+
{
|
56
|
+
disableAutomaticIdColumn: true,
|
57
|
+
indexes: [{ columns: ['idGlobal', 'idLocal'], name: 'idx_session_changeset_id' }],
|
58
|
+
},
|
57
59
|
)
|
58
60
|
|
59
61
|
export type SessionChangesetMetaRow = FromTable.RowDecoded<typeof sessionChangesetMetaTable>
|
@@ -70,16 +72,22 @@ export const MUTATION_LOG_META_TABLE = 'mutation_log'
|
|
70
72
|
export const mutationLogMetaTable = table(
|
71
73
|
MUTATION_LOG_META_TABLE,
|
72
74
|
{
|
73
|
-
idGlobal: SqliteDsl.integer({ primaryKey: true }),
|
74
|
-
idLocal: SqliteDsl.integer({ primaryKey: true }),
|
75
|
-
parentIdGlobal: SqliteDsl.integer({}),
|
76
|
-
parentIdLocal: SqliteDsl.integer({}),
|
75
|
+
idGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventId.GlobalEventId }),
|
76
|
+
idLocal: SqliteDsl.integer({ primaryKey: true, schema: EventId.LocalEventId }),
|
77
|
+
parentIdGlobal: SqliteDsl.integer({ schema: EventId.GlobalEventId }),
|
78
|
+
parentIdLocal: SqliteDsl.integer({ schema: EventId.LocalEventId }),
|
77
79
|
mutation: SqliteDsl.text({}),
|
78
80
|
argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
|
79
81
|
schemaHash: SqliteDsl.integer({}),
|
80
82
|
syncMetadataJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Option(Schema.JsonValue)) }),
|
81
83
|
},
|
82
|
-
{
|
84
|
+
{
|
85
|
+
disableAutomaticIdColumn: true,
|
86
|
+
indexes: [
|
87
|
+
{ columns: ['idGlobal'], name: 'idx_idGlobal' },
|
88
|
+
{ columns: ['idGlobal', 'idLocal'], name: 'idx_mutationlog_id' },
|
89
|
+
],
|
90
|
+
},
|
83
91
|
)
|
84
92
|
|
85
93
|
export type MutationLogMetaRow = FromTable.RowDecoded<typeof mutationLogMetaTable>
|
@@ -129,10 +129,10 @@ export const migrateTable = ({
|
|
129
129
|
|
130
130
|
if (behaviour === 'drop-and-recreate') {
|
131
131
|
// TODO need to possibly handle cascading deletes due to foreign keys
|
132
|
-
dbExecute(db, sql`drop table if exists ${tableName}`)
|
133
|
-
dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
|
132
|
+
dbExecute(db, sql`drop table if exists '${tableName}'`)
|
133
|
+
dbExecute(db, sql`create table if not exists '${tableName}' (${columnSpec}) strict`)
|
134
134
|
} else if (behaviour === 'create-if-not-exists') {
|
135
|
-
dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
|
135
|
+
dbExecute(db, sql`create table if not exists '${tableName}' (${columnSpec}) strict`)
|
136
136
|
}
|
137
137
|
|
138
138
|
for (const index of tableAst.indexes) {
|
@@ -162,11 +162,11 @@ export const migrateTable = ({
|
|
162
162
|
|
163
163
|
const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
|
164
164
|
const uniqueStr = index.unique ? 'UNIQUE' : ''
|
165
|
-
return sql`create ${uniqueStr} index if not exists ${index.name} on ${tableName} (${index.columns.join(', ')})`
|
165
|
+
return sql`create ${uniqueStr} index if not exists '${index.name}' on '${tableName}' (${index.columns.join(', ')})`
|
166
166
|
}
|
167
167
|
|
168
168
|
export const makeColumnSpec = (tableAst: SqliteAst.Table) => {
|
169
|
-
const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => _.name)
|
169
|
+
const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => `'${_.name}'`)
|
170
170
|
const columnDefStrs = tableAst.columns.map(toSqliteColumnSpec)
|
171
171
|
if (primaryKeys.length > 0) {
|
172
172
|
columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
|
@@ -191,5 +191,5 @@ const toSqliteColumnSpec = (column: SqliteAst.Column) => {
|
|
191
191
|
return `default ${encodedDefaultValue}`
|
192
192
|
})()
|
193
193
|
|
194
|
-
return
|
194
|
+
return `'${column.name}' ${columnTypeStr} ${nullableStr} ${defaultValueStr}`
|
195
195
|
}
|
@@ -3,7 +3,7 @@ import type { Scope } from '@livestore/utils/effect'
|
|
3
3
|
import { Effect, Schema, Stream } from '@livestore/utils/effect'
|
4
4
|
import * as otel from '@opentelemetry/api'
|
5
5
|
|
6
|
-
import type {
|
6
|
+
import type { ClientSessionLeaderThreadProxy, UnexpectedError } from '../adapter-types.js'
|
7
7
|
import * as EventId from '../schema/EventId.js'
|
8
8
|
import { type LiveStoreSchema } from '../schema/mod.js'
|
9
9
|
import * as MutationEvent from '../schema/MutationEvent.js'
|
@@ -32,9 +32,9 @@ export const makeClientSessionSyncProcessor = ({
|
|
32
32
|
schema: LiveStoreSchema
|
33
33
|
initialLeaderHead: EventId.EventId
|
34
34
|
pushToLeader: (batch: ReadonlyArray<MutationEvent.AnyEncoded>) => void
|
35
|
-
pullFromLeader:
|
35
|
+
pullFromLeader: ClientSessionLeaderThreadProxy['mutations']['pull']
|
36
36
|
applyMutation: (
|
37
|
-
mutationEventDecoded: MutationEvent.
|
37
|
+
mutationEventDecoded: MutationEvent.PartialAnyDecoded,
|
38
38
|
options: { otelContext: otel.Context; withChangeset: boolean },
|
39
39
|
) => {
|
40
40
|
writeTables: Set<string>
|
@@ -196,7 +196,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
196
196
|
|
197
197
|
export interface ClientSessionSyncProcessor {
|
198
198
|
push: (
|
199
|
-
batch: ReadonlyArray<MutationEvent.
|
199
|
+
batch: ReadonlyArray<MutationEvent.PartialAnyDecoded>,
|
200
200
|
options: { otelContext: otel.Context },
|
201
201
|
) => {
|
202
202
|
writeTables: Set<string>
|
@@ -20,7 +20,7 @@ export const emptyHistoryDag = (): HistoryDag =>
|
|
20
20
|
})
|
21
21
|
|
22
22
|
// TODO consider making `ROOT_ID` parent to itself
|
23
|
-
export const rootParentId = { global: EventId.ROOT.global - 1, local:
|
23
|
+
export const rootParentId = EventId.make({ global: EventId.ROOT.global - 1, local: EventId.localDefault })
|
24
24
|
|
25
25
|
export type HistoryDagNode = {
|
26
26
|
id: EventId.EventId
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import
|
1
|
+
import * as EventId from '../../schema/EventId.js'
|
2
2
|
import type * as MutationEvent from '../../schema/MutationEvent.js'
|
3
3
|
import type { MutationDef, MutationEventFactsSnapshot } from '../../schema/mutations.js'
|
4
4
|
import {
|
@@ -19,13 +19,13 @@ export type RebaseInput = {
|
|
19
19
|
newRemoteEvents: RebaseEventWithConflict[]
|
20
20
|
pendingLocalEvents: RebaseEventWithConflict[]
|
21
21
|
validate: (args: {
|
22
|
-
rebasedLocalEvents: MutationEvent.
|
22
|
+
rebasedLocalEvents: MutationEvent.PartialAnyDecoded[]
|
23
23
|
mutationDefs: Record<string, MutationDef.Any>
|
24
24
|
}) => FactValidationResult
|
25
25
|
}
|
26
26
|
|
27
27
|
export type RebaseOutput = {
|
28
|
-
rebasedLocalEvents: MutationEvent.
|
28
|
+
rebasedLocalEvents: MutationEvent.PartialAnyDecoded[]
|
29
29
|
}
|
30
30
|
|
31
31
|
export type RebaseFn = (input: RebaseInput) => RebaseOutput
|
@@ -48,7 +48,7 @@ export const rebaseEvents = ({
|
|
48
48
|
newRemoteEvents: HistoryDagNode[]
|
49
49
|
rebaseFn: RebaseFn
|
50
50
|
currentFactsSnapshot: MutationEventFactsSnapshot
|
51
|
-
}): MutationEvent.
|
51
|
+
}): ReadonlyArray<MutationEvent.AnyDecoded> => {
|
52
52
|
const initialSnapshot = new Map(currentFactsSnapshot)
|
53
53
|
applyFactGroups(
|
54
54
|
newRemoteEvents.map((event) => event.factsGroup),
|
@@ -89,10 +89,10 @@ export const rebaseEvents = ({
|
|
89
89
|
return rebasedLocalEvents.map(
|
90
90
|
(event, index) =>
|
91
91
|
({
|
92
|
-
id: { global: headGlobalId + index + 1, local:
|
93
|
-
parentId: { global: headGlobalId + index, local:
|
92
|
+
id: EventId.make({ global: headGlobalId + index + 1, local: EventId.localDefault }),
|
93
|
+
parentId: EventId.make({ global: headGlobalId + index, local: EventId.localDefault }),
|
94
94
|
mutation: event.mutation,
|
95
95
|
args: event.args,
|
96
|
-
}) satisfies MutationEvent.
|
96
|
+
}) satisfies MutationEvent.AnyDecoded,
|
97
97
|
)
|
98
98
|
}
|