@livestore/common 0.2.0-dev.2 → 0.3.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/fixture.d.ts +163 -1
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +3 -1
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/adapter-types.d.ts +53 -38
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +5 -7
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts +2 -2
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/debug-info.d.ts +13 -13
- package/dist/derived-mutations.d.ts +1 -1
- package/dist/derived-mutations.d.ts.map +1 -1
- package/dist/devtools/devtools-bridge.d.ts +2 -2
- package/dist/devtools/devtools-bridge.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.d.ts +84 -196
- package/dist/devtools/devtools-messages.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.js +55 -61
- package/dist/devtools/devtools-messages.js.map +1 -1
- package/dist/devtools/index.d.ts.map +1 -1
- package/dist/devtools/index.js +1 -2
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/init-singleton-tables.d.ts +1 -1
- package/dist/init-singleton-tables.d.ts.map +1 -1
- package/dist/leader-thread/apply-mutation.d.ts +8 -0
- package/dist/leader-thread/apply-mutation.d.ts.map +1 -0
- package/dist/leader-thread/apply-mutation.js +95 -0
- package/dist/leader-thread/apply-mutation.js.map +1 -0
- package/dist/leader-thread/connection.d.ts +11 -0
- package/dist/leader-thread/connection.d.ts.map +1 -0
- package/dist/leader-thread/connection.js +44 -0
- package/dist/leader-thread/connection.js.map +1 -0
- package/dist/leader-thread/leader-sync-processor.d.ts +47 -0
- package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -0
- package/dist/leader-thread/leader-sync-processor.js +422 -0
- package/dist/leader-thread/leader-sync-processor.js.map +1 -0
- package/dist/leader-thread/leader-worker-devtools.d.ts +6 -0
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -0
- package/dist/leader-thread/leader-worker-devtools.js +216 -0
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -0
- package/dist/leader-thread/make-leader-thread-layer.d.ts +20 -0
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -0
- package/dist/leader-thread/make-leader-thread-layer.js +106 -0
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -0
- package/dist/leader-thread/mod.d.ts +7 -0
- package/dist/leader-thread/mod.d.ts.map +1 -0
- package/dist/leader-thread/mod.js +7 -0
- package/dist/leader-thread/mod.js.map +1 -0
- package/dist/leader-thread/mutationlog.d.ts +23 -0
- package/dist/leader-thread/mutationlog.d.ts.map +1 -0
- package/dist/leader-thread/mutationlog.js +27 -0
- package/dist/leader-thread/mutationlog.js.map +1 -0
- package/dist/leader-thread/pull-queue-set.d.ts +7 -0
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -0
- package/dist/leader-thread/pull-queue-set.js +39 -0
- package/dist/leader-thread/pull-queue-set.js.map +1 -0
- package/dist/leader-thread/recreate-db.d.ts +7 -0
- package/dist/leader-thread/recreate-db.d.ts.map +1 -0
- package/dist/leader-thread/recreate-db.js +69 -0
- package/dist/leader-thread/recreate-db.js.map +1 -0
- package/dist/leader-thread/shutdown-channel.d.ts +15 -0
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -0
- package/dist/leader-thread/shutdown-channel.js +7 -0
- package/dist/leader-thread/shutdown-channel.js.map +1 -0
- package/dist/leader-thread/types.d.ts +87 -0
- package/dist/leader-thread/types.d.ts.map +1 -0
- package/dist/leader-thread/types.js +11 -0
- package/dist/leader-thread/types.js.map +1 -0
- package/dist/mutation.d.ts +3 -4
- package/dist/mutation.d.ts.map +1 -1
- package/dist/mutation.js +0 -14
- package/dist/mutation.js.map +1 -1
- package/dist/otel.d.ts +7 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +11 -0
- package/dist/otel.js.map +1 -0
- package/dist/query-builder/api.d.ts +2 -2
- package/dist/query-builder/api.d.ts.map +1 -1
- package/dist/query-builder/api.js.map +1 -1
- package/dist/query-builder/impl.d.ts +1 -1
- package/dist/query-builder/impl.d.ts.map +1 -1
- package/dist/query-builder/impl.js +23 -5
- package/dist/query-builder/impl.js.map +1 -1
- package/dist/query-builder/impl.test.js +30 -1
- package/dist/query-builder/impl.test.js.map +1 -1
- package/dist/query-info.d.ts +1 -1
- package/dist/query-info.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +6 -6
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/EventId.d.ts +37 -0
- package/dist/schema/EventId.d.ts.map +1 -0
- package/dist/schema/EventId.js +30 -0
- package/dist/schema/EventId.js.map +1 -0
- package/dist/schema/MutationEvent.d.ts +191 -0
- package/dist/schema/MutationEvent.d.ts.map +1 -0
- package/dist/schema/MutationEvent.js +56 -0
- package/dist/schema/MutationEvent.js.map +1 -0
- package/dist/schema/mod.d.ts +8 -0
- package/dist/schema/mod.d.ts.map +1 -0
- package/dist/schema/mod.js +8 -0
- package/dist/schema/mod.js.map +1 -0
- package/dist/schema/mutations.d.ts +3 -123
- package/dist/schema/mutations.d.ts.map +1 -1
- package/dist/schema/mutations.js +0 -26
- package/dist/schema/mutations.js.map +1 -1
- package/dist/schema/{index.d.ts → schema.d.ts} +1 -5
- package/dist/schema/schema.d.ts.map +1 -0
- package/dist/schema/{index.js → schema.js} +1 -5
- package/dist/schema/schema.js.map +1 -0
- package/dist/schema/system-tables.d.ts +55 -29
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +10 -5
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema-management/migrations.d.ts +1 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +6 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-mutation-defs.d.ts +1 -1
- package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
- package/dist/sync/client-session-sync-processor.d.ts +45 -0
- package/dist/sync/client-session-sync-processor.d.ts.map +1 -0
- package/dist/sync/client-session-sync-processor.js +131 -0
- package/dist/sync/client-session-sync-processor.js.map +1 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +2 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/next/compact-events.d.ts +1 -1
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +2 -1
- package/dist/sync/next/compact-events.js.map +1 -1
- package/dist/sync/next/facts.d.ts +5 -5
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +1 -1
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +30 -0
- package/dist/sync/next/history-dag-common.d.ts.map +1 -0
- package/dist/sync/next/history-dag-common.js +20 -0
- package/dist/sync/next/history-dag-common.js.map +1 -0
- package/dist/sync/next/history-dag.d.ts +4 -27
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +1 -19
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/next/mod.d.ts +1 -0
- package/dist/sync/next/mod.d.ts.map +1 -1
- package/dist/sync/next/mod.js +1 -0
- package/dist/sync/next/mod.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +3 -2
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.d.ts.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +2 -1
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts +1 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/mutation-fixtures.js +4 -3
- package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
- package/dist/sync/sync.d.ts +33 -12
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +10 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +123 -0
- package/dist/sync/syncstate.d.ts.map +1 -0
- package/dist/sync/syncstate.js +248 -0
- package/dist/sync/syncstate.js.map +1 -0
- package/dist/sync/syncstate.test.d.ts +2 -0
- package/dist/sync/syncstate.test.d.ts.map +1 -0
- package/dist/sync/syncstate.test.js +399 -0
- package/dist/sync/syncstate.test.js.map +1 -0
- package/dist/sync/validate-push-payload.d.ts +5 -0
- package/dist/sync/validate-push-payload.d.ts.map +1 -0
- package/dist/sync/validate-push-payload.js +15 -0
- package/dist/sync/validate-push-payload.js.map +1 -0
- package/dist/util.d.ts +2 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +13 -6
- package/src/__tests__/fixture.ts +5 -1
- package/src/adapter-types.ts +60 -34
- package/src/derived-mutations.test.ts +1 -1
- package/src/derived-mutations.ts +1 -1
- package/src/devtools/devtools-bridge.ts +2 -2
- package/src/devtools/devtools-messages.ts +70 -74
- package/src/devtools/index.ts +1 -2
- package/src/index.ts +2 -1
- package/src/init-singleton-tables.ts +1 -1
- package/src/leader-thread/apply-mutation.ts +143 -0
- package/src/leader-thread/connection.ts +67 -0
- package/src/leader-thread/leader-sync-processor.ts +666 -0
- package/src/leader-thread/leader-worker-devtools.ts +358 -0
- package/src/leader-thread/make-leader-thread-layer.ts +192 -0
- package/src/leader-thread/mod.ts +6 -0
- package/src/leader-thread/mutationlog.ts +42 -0
- package/src/leader-thread/pull-queue-set.ts +58 -0
- package/src/leader-thread/recreate-db.ts +109 -0
- package/src/leader-thread/shutdown-channel.ts +13 -0
- package/src/leader-thread/types.ts +129 -0
- package/src/mutation.ts +3 -21
- package/src/otel.ts +20 -0
- package/src/query-builder/api.ts +3 -2
- package/src/query-builder/impl.test.ts +35 -1
- package/src/query-builder/impl.ts +23 -6
- package/src/query-info.ts +1 -1
- package/src/rehydrate-from-mutationlog.ts +7 -11
- package/src/schema/EventId.ts +46 -0
- package/src/schema/MutationEvent.ts +161 -0
- package/src/schema/mod.ts +7 -0
- package/src/schema/mutations.ts +5 -126
- package/src/schema/{index.ts → schema.ts} +0 -5
- package/src/schema/system-tables.ts +18 -5
- package/src/schema-management/migrations.ts +9 -2
- package/src/schema-management/validate-mutation-defs.ts +1 -1
- package/src/sync/client-session-sync-processor.ts +207 -0
- package/src/sync/index.ts +2 -0
- package/src/sync/next/compact-events.ts +3 -2
- package/src/sync/next/facts.ts +11 -5
- package/src/sync/next/history-dag-common.ts +44 -0
- package/src/sync/next/history-dag.ts +3 -45
- package/src/sync/next/mod.ts +1 -0
- package/src/sync/next/rebase-events.ts +6 -5
- package/src/sync/next/test/compact-events.test.ts +3 -2
- package/src/sync/next/test/mutation-fixtures.ts +7 -6
- package/src/sync/sync.ts +32 -12
- package/src/sync/syncstate.test.ts +464 -0
- package/src/sync/syncstate.ts +385 -0
- package/src/sync/validate-push-payload.ts +18 -0
- package/src/version.ts +2 -2
- package/dist/schema/index.d.ts.map +0 -1
- package/dist/schema/index.js.map +0 -1
- package/dist/sync/next-mutation-event-id-pair.d.ts +0 -14
- package/dist/sync/next-mutation-event-id-pair.d.ts.map +0 -1
- package/dist/sync/next-mutation-event-id-pair.js +0 -13
- package/dist/sync/next-mutation-event-id-pair.js.map +0 -1
- package/src/sync/next-mutation-event-id-pair.ts +0 -20
@@ -0,0 +1,109 @@
|
|
1
|
+
import { casesHandled } from '@livestore/utils'
|
2
|
+
import type { HttpClient } from '@livestore/utils/effect'
|
3
|
+
import { Effect, Queue } from '@livestore/utils/effect'
|
4
|
+
|
5
|
+
import type { InvalidPullError, IsOfflineError, MigrationHooks, SqliteError } from '../index.js'
|
6
|
+
import { initializeSingletonTables, migrateDb, rehydrateFromMutationLog, UnexpectedError } from '../index.js'
|
7
|
+
import { configureConnection } from './connection.js'
|
8
|
+
import { LeaderThreadCtx } from './types.js'
|
9
|
+
|
10
|
+
export const recreateDb: Effect.Effect<
|
11
|
+
void,
|
12
|
+
UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
|
13
|
+
LeaderThreadCtx | HttpClient.HttpClient
|
14
|
+
> = Effect.gen(function* () {
|
15
|
+
const { db, dbLog, makeSyncDb, schema, bootStatusQueue } = yield* LeaderThreadCtx
|
16
|
+
|
17
|
+
const migrationOptions = schema.migrationOptions
|
18
|
+
|
19
|
+
yield* Effect.addFinalizer(
|
20
|
+
Effect.fn('recreateDb:finalizer')(function* (ex) {
|
21
|
+
if (ex._tag === 'Failure') db.destroy()
|
22
|
+
}),
|
23
|
+
)
|
24
|
+
|
25
|
+
// NOTE to speed up the operations below, we're creating a temporary in-memory database
|
26
|
+
// and later we'll overwrite the persisted database with the new data
|
27
|
+
const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
|
28
|
+
yield* configureConnection(tmpSyncDb, { fkEnabled: true })
|
29
|
+
|
30
|
+
const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
|
31
|
+
Effect.gen(function* () {
|
32
|
+
yield* Effect.tryAll(() => hooks?.init?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
33
|
+
|
34
|
+
yield* migrateDb({
|
35
|
+
db: tmpSyncDb,
|
36
|
+
schema,
|
37
|
+
onProgress: ({ done, total }) =>
|
38
|
+
Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
|
39
|
+
})
|
40
|
+
|
41
|
+
initializeSingletonTables(schema, tmpSyncDb)
|
42
|
+
|
43
|
+
yield* Effect.tryAll(() => hooks?.pre?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
44
|
+
|
45
|
+
return tmpSyncDb
|
46
|
+
})
|
47
|
+
|
48
|
+
switch (migrationOptions.strategy) {
|
49
|
+
case 'from-mutation-log': {
|
50
|
+
const hooks = migrationOptions.hooks
|
51
|
+
const tmpSyncDb = yield* initDb(hooks)
|
52
|
+
|
53
|
+
yield* rehydrateFromMutationLog({
|
54
|
+
db: tmpSyncDb,
|
55
|
+
logDb: dbLog,
|
56
|
+
schema,
|
57
|
+
migrationOptions,
|
58
|
+
onProgress: ({ done, total }) =>
|
59
|
+
Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
|
60
|
+
})
|
61
|
+
|
62
|
+
yield* Effect.tryAll(() => hooks?.post?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
63
|
+
|
64
|
+
break
|
65
|
+
}
|
66
|
+
case 'hard-reset': {
|
67
|
+
const hooks = migrationOptions.hooks
|
68
|
+
const tmpInMemoryDb = yield* initDb(hooks)
|
69
|
+
|
70
|
+
// The database is migrated but empty now, so nothing else to do
|
71
|
+
|
72
|
+
yield* Effect.tryAll(() => hooks?.post?.(tmpInMemoryDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
73
|
+
|
74
|
+
break
|
75
|
+
}
|
76
|
+
case 'manual': {
|
77
|
+
const oldDbData = db.export()
|
78
|
+
|
79
|
+
const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
|
80
|
+
UnexpectedError.mapToUnexpectedError,
|
81
|
+
)
|
82
|
+
|
83
|
+
tmpSyncDb.import(newDbData)
|
84
|
+
|
85
|
+
// TODO validate schema
|
86
|
+
|
87
|
+
break
|
88
|
+
}
|
89
|
+
default: {
|
90
|
+
casesHandled(migrationOptions)
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
// Import the temporary in-memory database into the persistent database
|
95
|
+
yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
|
96
|
+
Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
|
97
|
+
)
|
98
|
+
|
99
|
+
// TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
|
100
|
+
// We've disabled this for now as it made the code too complex, as we often run syncing right after
|
101
|
+
// so the snapshot is no longer up to date
|
102
|
+
// const snapshotFromTmpDb = tmpSyncDb.export()
|
103
|
+
|
104
|
+
tmpSyncDb.close()
|
105
|
+
}).pipe(
|
106
|
+
Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
|
107
|
+
Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
|
108
|
+
Effect.withPerformanceMeasure('@livestore/common:leader-thread:recreateDb'),
|
109
|
+
)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import type { WebChannel } from '@livestore/utils/effect'
|
2
|
+
import { Schema } from '@livestore/utils/effect'
|
3
|
+
|
4
|
+
import { IntentionalShutdownCause } from '../index.js'
|
5
|
+
|
6
|
+
export class DedicatedWorkerDisconnectBroadcast extends Schema.TaggedStruct('DedicatedWorkerDisconnectBroadcast', {}) {}
|
7
|
+
|
8
|
+
export class All extends Schema.Union(IntentionalShutdownCause, DedicatedWorkerDisconnectBroadcast) {}
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Used internally by an adapter to shutdown gracefully.
|
12
|
+
*/
|
13
|
+
export type ShutdownChannel = WebChannel.WebChannel<typeof All.Type, typeof All.Type>
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import type {
|
2
|
+
Deferred,
|
3
|
+
Effect,
|
4
|
+
Fiber,
|
5
|
+
HttpClient,
|
6
|
+
Option,
|
7
|
+
Queue,
|
8
|
+
Scope,
|
9
|
+
SubscriptionRef,
|
10
|
+
WebChannel,
|
11
|
+
} from '@livestore/utils/effect'
|
12
|
+
import { Context, Schema } from '@livestore/utils/effect'
|
13
|
+
|
14
|
+
import type {
|
15
|
+
BootStatus,
|
16
|
+
Devtools,
|
17
|
+
InvalidPushError,
|
18
|
+
MakeSynchronousDatabase,
|
19
|
+
PersistenceInfo,
|
20
|
+
SyncBackend,
|
21
|
+
SynchronousDatabase,
|
22
|
+
UnexpectedError,
|
23
|
+
} from '../index.js'
|
24
|
+
import type { EventId, LiveStoreSchema, MutationEvent } from '../schema/mod.js'
|
25
|
+
import type { PayloadUpstream, SyncState } from '../sync/syncstate.js'
|
26
|
+
import type { ShutdownChannel } from './shutdown-channel.js'
|
27
|
+
|
28
|
+
export type ShutdownState = 'running' | 'shutting-down'
|
29
|
+
|
30
|
+
export class OuterWorkerCtx extends Context.Tag('OuterWorkerCtx')<
|
31
|
+
OuterWorkerCtx,
|
32
|
+
{
|
33
|
+
innerFiber: Fiber.RuntimeFiber<any, any>
|
34
|
+
}
|
35
|
+
>() {}
|
36
|
+
|
37
|
+
export const InitialSyncOptionsSkip = Schema.TaggedStruct('Skip', {})
|
38
|
+
export type InitialSyncOptionsSkip = typeof InitialSyncOptionsSkip.Type
|
39
|
+
|
40
|
+
export const InitialSyncOptionsBlocking = Schema.TaggedStruct('Blocking', {
|
41
|
+
timeout: Schema.DurationFromMillis,
|
42
|
+
})
|
43
|
+
|
44
|
+
export type InitialSyncOptionsBlocking = typeof InitialSyncOptionsBlocking.Type
|
45
|
+
|
46
|
+
export const InitialSyncOptions = Schema.Union(InitialSyncOptionsSkip, InitialSyncOptionsBlocking)
|
47
|
+
export type InitialSyncOptions = typeof InitialSyncOptions.Type
|
48
|
+
|
49
|
+
export type InitialSyncInfo = Option.Option<{
|
50
|
+
cursor: EventId.EventId
|
51
|
+
metadata: Option.Option<Schema.JsonValue>
|
52
|
+
}>
|
53
|
+
|
54
|
+
// export type InitialSetup =
|
55
|
+
// | { _tag: 'Recreate'; snapshotRef: Ref.Ref<Uint8Array | undefined>; syncInfo: InitialSyncInfo }
|
56
|
+
// | { _tag: 'Reuse'; syncInfo: InitialSyncInfo }
|
57
|
+
|
58
|
+
export type LeaderDatabase = SynchronousDatabase<{ dbPointer: number; persistenceInfo: PersistenceInfo }>
|
59
|
+
export type PersistenceInfoPair = { db: PersistenceInfo; mutationLog: PersistenceInfo }
|
60
|
+
|
61
|
+
export type DevtoolsOptions =
|
62
|
+
| {
|
63
|
+
enabled: false
|
64
|
+
}
|
65
|
+
| {
|
66
|
+
enabled: true
|
67
|
+
makeContext: Effect.Effect<
|
68
|
+
{
|
69
|
+
devtoolsWebChannel: WebChannel.WebChannel<Devtools.MessageToAppLeader, Devtools.MessageFromAppLeader>
|
70
|
+
shutdownChannel: ShutdownChannel
|
71
|
+
persistenceInfo: PersistenceInfoPair
|
72
|
+
},
|
73
|
+
UnexpectedError,
|
74
|
+
Scope.Scope
|
75
|
+
>
|
76
|
+
}
|
77
|
+
|
78
|
+
export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
|
79
|
+
LeaderThreadCtx,
|
80
|
+
{
|
81
|
+
schema: LiveStoreSchema
|
82
|
+
storeId: string
|
83
|
+
originId: string
|
84
|
+
makeSyncDb: MakeSynchronousDatabase
|
85
|
+
db: LeaderDatabase
|
86
|
+
dbLog: LeaderDatabase
|
87
|
+
bootStatusQueue: Queue.Queue<BootStatus>
|
88
|
+
// TODO we should find a more elegant way to handle cases which need this ref for their implementation
|
89
|
+
shutdownStateSubRef: SubscriptionRef.SubscriptionRef<ShutdownState>
|
90
|
+
mutationEventSchema: MutationEvent.ForMutationDefRecord<any>
|
91
|
+
// devtools: DevtoolsContext
|
92
|
+
syncBackend: SyncBackend | undefined
|
93
|
+
syncProcessor: SyncProcessor
|
94
|
+
connectedClientSessionPullQueues: PullQueueSet
|
95
|
+
}
|
96
|
+
>() {}
|
97
|
+
|
98
|
+
export type InitialBlockingSyncContext = {
|
99
|
+
blockingDeferred: Deferred.Deferred<void> | undefined
|
100
|
+
update: (_: { remaining: number; processed: number }) => Effect.Effect<void>
|
101
|
+
}
|
102
|
+
|
103
|
+
export type PullQueueItem = {
|
104
|
+
// mutationEvents: ReadonlyArray<MutationEvent.AnyEncoded>
|
105
|
+
// backendHead: number
|
106
|
+
payload: PayloadUpstream
|
107
|
+
// TODO move `remaining` into `PayloadUpstream`
|
108
|
+
remaining: number
|
109
|
+
}
|
110
|
+
|
111
|
+
export interface SyncProcessor {
|
112
|
+
push: (
|
113
|
+
/** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
|
114
|
+
batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
115
|
+
) => Effect.Effect<void, UnexpectedError | InvalidPushError, HttpClient.HttpClient | LeaderThreadCtx>
|
116
|
+
|
117
|
+
pushPartial: (mutationEvent: MutationEvent.PartialAnyEncoded) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
|
118
|
+
boot: (args: {
|
119
|
+
dbReady: Deferred.Deferred<void>
|
120
|
+
}) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient>
|
121
|
+
syncState: Effect.Effect<SyncState, UnexpectedError>
|
122
|
+
}
|
123
|
+
|
124
|
+
export interface PullQueueSet {
|
125
|
+
makeQueue: (
|
126
|
+
since: EventId.EventId,
|
127
|
+
) => Effect.Effect<Queue.Queue<PullQueueItem>, UnexpectedError, Scope.Scope | LeaderThreadCtx>
|
128
|
+
offer: (item: PullQueueItem) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
|
129
|
+
}
|
package/src/mutation.ts
CHANGED
@@ -1,9 +1,8 @@
|
|
1
|
-
import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
2
1
|
import { Schema } from '@livestore/utils/effect'
|
3
2
|
|
4
3
|
import { SessionIdSymbol } from './adapter-types.js'
|
5
|
-
import type
|
6
|
-
import type { MutationDef
|
4
|
+
import type * as MutationEvent from './schema/MutationEvent.js'
|
5
|
+
import type { MutationDef } from './schema/mutations.js'
|
7
6
|
import type { PreparedBindValues } from './util.js'
|
8
7
|
import { prepareBindValues } from './util.js'
|
9
8
|
|
@@ -12,7 +11,7 @@ export const getExecArgsFromMutation = ({
|
|
12
11
|
mutationEventDecoded,
|
13
12
|
}: {
|
14
13
|
mutationDef: MutationDef.Any
|
15
|
-
mutationEventDecoded: MutationEvent.Any
|
14
|
+
mutationEventDecoded: MutationEvent.Any | MutationEvent.PartialAny
|
16
15
|
}): ReadonlyArray<{
|
17
16
|
statementSql: string
|
18
17
|
bindValues: PreparedBindValues
|
@@ -52,23 +51,6 @@ export const getExecArgsFromMutation = ({
|
|
52
51
|
})
|
53
52
|
}
|
54
53
|
|
55
|
-
export const makeShouldExcludeMutationFromLog = memoizeByRef((schema: LiveStoreSchema) => {
|
56
|
-
const migrationOptions = schema.migrationOptions
|
57
|
-
const mutationLogExclude =
|
58
|
-
migrationOptions.strategy === 'from-mutation-log'
|
59
|
-
? (migrationOptions.excludeMutations ?? new Set(['livestore.RawSql']))
|
60
|
-
: new Set(['livestore.RawSql'])
|
61
|
-
|
62
|
-
return (mutationName: string, mutationEventDecoded: MutationEvent.Any): boolean => {
|
63
|
-
if (mutationLogExclude.has(mutationName)) return true
|
64
|
-
|
65
|
-
const mutationDef = schema.mutations.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
|
66
|
-
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
67
|
-
|
68
|
-
return execArgsArr.some((_) => _.statementSql.includes('__livestore'))
|
69
|
-
}
|
70
|
-
})
|
71
|
-
|
72
54
|
// NOTE we should explore whether there is a more elegant solution
|
73
55
|
// e.g. by leveraging the schema to replace the sessionIdSymbol
|
74
56
|
export const replaceSessionIdSymbol = (
|
package/src/otel.ts
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
import { makeNoopTracer } from '@livestore/utils'
|
2
|
+
import { Effect, identity, Layer, OtelTracer } from '@livestore/utils/effect'
|
3
|
+
import * as otel from '@opentelemetry/api'
|
4
|
+
|
5
|
+
export const provideOtel =
|
6
|
+
({ otelTracer, parentSpanContext }: { otelTracer?: otel.Tracer; parentSpanContext?: otel.Context }) =>
|
7
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, Exclude<R, OtelTracer.OtelTracer>> => {
|
8
|
+
const OtelTracerLive = Layer.succeed(OtelTracer.OtelTracer, otelTracer ?? makeNoopTracer())
|
9
|
+
|
10
|
+
const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
|
11
|
+
Layer.provideMerge(OtelTracerLive),
|
12
|
+
) as any as Layer.Layer<OtelTracer.OtelTracer>
|
13
|
+
|
14
|
+
return effect.pipe(
|
15
|
+
parentSpanContext
|
16
|
+
? Effect.withParentSpan(OtelTracer.makeExternalSpan(otel.trace.getSpanContext(parentSpanContext)!))
|
17
|
+
: identity,
|
18
|
+
Effect.provide(TracingLive),
|
19
|
+
)
|
20
|
+
}
|
package/src/query-builder/api.ts
CHANGED
@@ -3,7 +3,7 @@ import { type Option, Predicate, type Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
4
4
|
import type { SessionIdSymbol } from '../adapter-types.js'
|
5
5
|
import type { QueryInfo } from '../query-info.js'
|
6
|
-
import type { DbSchema } from '../schema/
|
6
|
+
import type { DbSchema } from '../schema/mod.js'
|
7
7
|
import type { SqliteDsl } from '../schema/table-def.js'
|
8
8
|
import type { SqlValue } from '../util.js'
|
9
9
|
|
@@ -243,8 +243,9 @@ export namespace QueryBuilder {
|
|
243
243
|
>
|
244
244
|
|
245
245
|
/**
|
246
|
-
*
|
246
|
+
* Gets a single row from the table and will create it if it doesn't exist yet.
|
247
247
|
*/
|
248
|
+
// TODO maybe call `getsert`?
|
248
249
|
readonly row: TTableDef['options']['isSingleton'] extends true
|
249
250
|
? () => QueryBuilder<RowQuery.Result<TTableDef>, TTableDef, QueryBuilder.ApiFeature, QueryInfo.Row>
|
250
251
|
: TTableDef['options']['deriveMutations']['enabled'] extends false
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import { Schema } from '@livestore/utils/effect'
|
2
2
|
import { describe, expect, it } from 'vitest'
|
3
3
|
|
4
|
-
import { DbSchema } from '../schema/
|
4
|
+
import { DbSchema } from '../schema/mod.js'
|
5
|
+
import { getResultSchema } from './impl.js'
|
5
6
|
|
6
7
|
const todos = DbSchema.table(
|
7
8
|
'todos',
|
@@ -26,6 +27,12 @@ const comments = DbSchema.table('comments', {
|
|
26
27
|
const db = { todos: todos.query, comments: comments.query }
|
27
28
|
|
28
29
|
describe('query builder', () => {
|
30
|
+
describe('result schema', () => {
|
31
|
+
it('should print the schema', () => {
|
32
|
+
expect(String(getResultSchema(db.todos))).toMatchInlineSnapshot(`"ReadonlyArray<todos>"`)
|
33
|
+
})
|
34
|
+
})
|
35
|
+
|
29
36
|
describe('basic queries', () => {
|
30
37
|
it('should handle simple SELECT queries', () => {
|
31
38
|
expect(db.todos.asSql()).toMatchInlineSnapshot(`
|
@@ -87,6 +94,33 @@ describe('query builder', () => {
|
|
87
94
|
"query": "SELECT id, text FROM 'todos' WHERE deletedAt <= ?",
|
88
95
|
}
|
89
96
|
`)
|
97
|
+
expect(
|
98
|
+
db.todos
|
99
|
+
.select('id', 'text')
|
100
|
+
.where({ status: { op: 'IN', value: ['active'] } })
|
101
|
+
.asSql(),
|
102
|
+
).toMatchInlineSnapshot(`
|
103
|
+
{
|
104
|
+
"bindValues": [
|
105
|
+
"active",
|
106
|
+
],
|
107
|
+
"query": "SELECT id, text FROM 'todos' WHERE status IN (?)",
|
108
|
+
}
|
109
|
+
`)
|
110
|
+
expect(
|
111
|
+
db.todos
|
112
|
+
.select('id', 'text')
|
113
|
+
.where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })
|
114
|
+
.asSql(),
|
115
|
+
).toMatchInlineSnapshot(`
|
116
|
+
{
|
117
|
+
"bindValues": [
|
118
|
+
"active",
|
119
|
+
"completed",
|
120
|
+
],
|
121
|
+
"query": "SELECT id, text FROM 'todos' WHERE status NOT IN (?, ?)",
|
122
|
+
}
|
123
|
+
`)
|
90
124
|
})
|
91
125
|
|
92
126
|
it('should handle OFFSET and LIMIT clauses', () => {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Option, Predicate, Schema } from '@livestore/utils/effect'
|
2
2
|
|
3
3
|
import type { QueryInfo } from '../query-info.js'
|
4
|
-
import type { DbSchema } from '../schema/
|
4
|
+
import type { DbSchema } from '../schema/mod.js'
|
5
5
|
import type { QueryBuilder, QueryBuilderAst } from './api.js'
|
6
6
|
import { QueryBuilderAstSymbol, TypeId } from './api.js'
|
7
7
|
|
@@ -119,7 +119,8 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
119
119
|
return makeQueryBuilder(tableDef, {
|
120
120
|
...ast,
|
121
121
|
limit: Option.some(1),
|
122
|
-
|
122
|
+
// TODO improve
|
123
|
+
pickFirst: options?.fallback ? { fallback: options.fallback } : { fallback: () => undefined },
|
123
124
|
})
|
124
125
|
},
|
125
126
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
@@ -154,7 +155,14 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
154
155
|
[TypeId]: TypeId,
|
155
156
|
[QueryBuilderAstSymbol]: ast,
|
156
157
|
asSql: () => astToSql(ast),
|
157
|
-
toString: () =>
|
158
|
+
toString: () => {
|
159
|
+
try {
|
160
|
+
return astToSql(ast).query
|
161
|
+
} catch (cause) {
|
162
|
+
console.debug(`QueryBuilder.toString(): Error converting query builder to string`, cause, ast)
|
163
|
+
return `Error converting query builder to string`
|
164
|
+
}
|
165
|
+
},
|
158
166
|
...api,
|
159
167
|
} satisfies QueryBuilder<TResult, TTableDef>
|
160
168
|
}
|
@@ -197,9 +205,18 @@ const astToSql = (ast: QueryBuilderAst) => {
|
|
197
205
|
if (colDef === undefined) {
|
198
206
|
throw new Error(`Column ${col} not found`)
|
199
207
|
}
|
200
|
-
const
|
201
|
-
|
202
|
-
|
208
|
+
const isArray = op === 'IN' || op === 'NOT IN'
|
209
|
+
const colSchema = isArray ? Schema.Array(colDef.schema) : colDef.schema
|
210
|
+
const encodedValue = Schema.encodeSync(colSchema)(value)
|
211
|
+
|
212
|
+
if (isArray) {
|
213
|
+
bindValues.push(...encodedValue)
|
214
|
+
const placeholders = Array.from({ length: encodedValue.length }, () => '?').join(', ')
|
215
|
+
return `${col} ${op} (${placeholders})`
|
216
|
+
} else {
|
217
|
+
bindValues.push(encodedValue)
|
218
|
+
return `${col} ${op} ?`
|
219
|
+
}
|
203
220
|
}
|
204
221
|
})
|
205
222
|
.join(' AND ')}`
|
package/src/query-info.ts
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
1
|
+
import { isDevEnv, 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
5
|
import { getExecArgsFromMutation } from './mutation.js'
|
6
|
-
import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/
|
7
|
-
import { MUTATION_LOG_META_TABLE } from './schema/
|
6
|
+
import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/mod.js'
|
7
|
+
import { EventId, MUTATION_LOG_META_TABLE } from './schema/mod.js'
|
8
8
|
import type { PreparedBindValues } from './util.js'
|
9
9
|
import { sql } from './util.js'
|
10
10
|
|
@@ -72,11 +72,7 @@ This likely means the schema has changed in an incompatible way.
|
|
72
72
|
|
73
73
|
for (const { statementSql, bindValues } of execArgsArr) {
|
74
74
|
// TODO cache prepared statements for mutations
|
75
|
-
db.execute(
|
76
|
-
statementSql,
|
77
|
-
bindValues,
|
78
|
-
import.meta.env.DEV ? makeExecuteOptions(statementSql, bindValues) : undefined,
|
79
|
-
)
|
75
|
+
db.execute(statementSql, bindValues, isDevEnv() ? makeExecuteOptions(statementSql, bindValues) : undefined)
|
80
76
|
// console.log(`Re-executed mutation ${mutationSql}`, bindValues)
|
81
77
|
}
|
82
78
|
}).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
|
@@ -85,7 +81,7 @@ This likely means the schema has changed in an incompatible way.
|
|
85
81
|
|
86
82
|
const stmt = logDb.prepare(sql`\
|
87
83
|
SELECT * FROM ${MUTATION_LOG_META_TABLE}
|
88
|
-
WHERE idGlobal >
|
84
|
+
WHERE idGlobal > $idGlobal OR (idGlobal = $idGlobal AND idLocal > $idLocal)
|
89
85
|
ORDER BY idGlobal ASC, idLocal ASC
|
90
86
|
LIMIT ${CHUNK_SIZE}
|
91
87
|
`)
|
@@ -101,9 +97,9 @@ LIMIT ${CHUNK_SIZE}
|
|
101
97
|
const lastId = Chunk.isChunk(item)
|
102
98
|
? Chunk.last(item).pipe(
|
103
99
|
Option.map((_) => ({ global: _.idGlobal, local: _.idLocal })),
|
104
|
-
Option.
|
100
|
+
Option.getOrElse(() => EventId.ROOT),
|
105
101
|
)
|
106
|
-
:
|
102
|
+
: EventId.ROOT
|
107
103
|
const nextItem = Chunk.fromIterable(
|
108
104
|
stmt.select<MutationLogMetaRow>({
|
109
105
|
$idGlobal: lastId?.global,
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
2
|
+
|
3
|
+
/**
|
4
|
+
* LiveStore event id value consisting of a globally unique event sequence number
|
5
|
+
* and a local sequence number.
|
6
|
+
*
|
7
|
+
* The local sequence number is only used for localOnly mutations and starts from 0 for each global sequence number.
|
8
|
+
*/
|
9
|
+
export type EventId = { global: number; local: number }
|
10
|
+
|
11
|
+
export const EventId = Schema.Struct({
|
12
|
+
global: Schema.Number,
|
13
|
+
local: Schema.Number,
|
14
|
+
}).annotations({ title: 'LiveStore.EventId' })
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Compare two event ids i.e. checks if the first event id is less than the second.
|
18
|
+
*/
|
19
|
+
export const compare = (a: EventId, b: EventId) => {
|
20
|
+
if (a.global !== b.global) {
|
21
|
+
return a.global - b.global
|
22
|
+
}
|
23
|
+
return a.local - b.local
|
24
|
+
}
|
25
|
+
|
26
|
+
export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.local === b.local
|
27
|
+
|
28
|
+
export type EventIdPair = { id: EventId; parentId: EventId }
|
29
|
+
|
30
|
+
export const ROOT = { global: -1, local: 0 } satisfies EventId
|
31
|
+
|
32
|
+
export const isGreaterThan = (a: EventId, b: EventId) => {
|
33
|
+
return a.global > b.global || (a.global === b.global && a.local > b.local)
|
34
|
+
}
|
35
|
+
|
36
|
+
export const nextPair = (id: EventId, isLocal: boolean) => {
|
37
|
+
if (isLocal) {
|
38
|
+
return { id: { global: id.global, local: id.local + 1 }, parentId: id }
|
39
|
+
}
|
40
|
+
|
41
|
+
return {
|
42
|
+
id: { global: id.global + 1, local: 0 },
|
43
|
+
// NOTE we always point to `local: 0` for non-localOnly mutations
|
44
|
+
parentId: { global: id.global, local: 0 },
|
45
|
+
}
|
46
|
+
}
|