@livestore/common 0.0.54-dev.22 → 0.0.54-dev.23

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.
Files changed (51) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +0 -2
  3. package/dist/__tests__/fixture.d.ts.map +1 -1
  4. package/dist/adapter-types.d.ts +59 -4
  5. package/dist/adapter-types.d.ts.map +1 -1
  6. package/dist/adapter-types.js +16 -11
  7. package/dist/adapter-types.js.map +1 -1
  8. package/dist/devtools/devtools-messages.d.ts +41 -39
  9. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  10. package/dist/devtools/devtools-messages.js +27 -28
  11. package/dist/devtools/devtools-messages.js.map +1 -1
  12. package/dist/devtools/devtools-window-message.d.ts +26 -0
  13. package/dist/devtools/devtools-window-message.d.ts.map +1 -0
  14. package/dist/devtools/devtools-window-message.js +30 -0
  15. package/dist/devtools/devtools-window-message.js.map +1 -0
  16. package/dist/devtools/index.d.ts +1 -0
  17. package/dist/devtools/index.d.ts.map +1 -1
  18. package/dist/devtools/index.js +1 -0
  19. package/dist/devtools/index.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/rehydrate-from-mutationlog.d.ts +8 -3
  25. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  26. package/dist/rehydrate-from-mutationlog.js +71 -56
  27. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  28. package/dist/schema/system-tables.d.ts +3 -8
  29. package/dist/schema/system-tables.d.ts.map +1 -1
  30. package/dist/schema/table-def.d.ts +0 -2
  31. package/dist/schema/table-def.d.ts.map +1 -1
  32. package/dist/schema/table-def.js +0 -1
  33. package/dist/schema/table-def.js.map +1 -1
  34. package/dist/schema-management/migrations.d.ts +9 -4
  35. package/dist/schema-management/migrations.d.ts.map +1 -1
  36. package/dist/schema-management/migrations.js +24 -13
  37. package/dist/schema-management/migrations.js.map +1 -1
  38. package/dist/version.d.ts +2 -0
  39. package/dist/version.d.ts.map +1 -0
  40. package/dist/version.js +3 -0
  41. package/dist/version.js.map +1 -0
  42. package/package.json +3 -3
  43. package/src/adapter-types.ts +46 -15
  44. package/src/devtools/devtools-messages.ts +55 -48
  45. package/src/devtools/devtools-window-message.ts +25 -0
  46. package/src/devtools/index.ts +1 -0
  47. package/src/index.ts +1 -0
  48. package/src/rehydrate-from-mutationlog.ts +100 -64
  49. package/src/schema/table-def.ts +0 -4
  50. package/src/schema-management/migrations.ts +104 -84
  51. package/src/version.ts +3 -0
@@ -1,11 +1,10 @@
1
- import { version as pkgVersion } from '@livestore/common/package.json'
2
- import { Schema } from '@livestore/utils/effect'
3
- import { type SqliteDsl as __SqliteDsl } from 'effect-db-schema'
1
+ import { Schema, Transferable } from '@livestore/utils/effect'
4
2
 
5
3
  import { NetworkStatus } from '../adapter-types.js'
6
4
  import { DebugInfo } from '../debug-info.js'
7
5
  import { mutationEventSchemaEncodedAny } from '../schema/mutations.js'
8
6
  import { PreparedBindValues } from '../util.js'
7
+ import { liveStoreVersion as pkgVersion } from '../version.js'
9
8
 
10
9
  const requestId = Schema.String
11
10
  const channelId = Schema.String
@@ -20,20 +19,21 @@ export class SnapshotReq extends Schema.TaggedStruct('LSD.SnapshotReq', {
20
19
  export class SnapshotRes extends Schema.TaggedStruct('LSD.SnapshotRes', {
21
20
  liveStoreVersion,
22
21
  requestId,
23
- snapshot: Schema.Uint8Array,
22
+ snapshot: Transferable.Uint8Array,
24
23
  }).annotations({ identifier: 'LSD.SnapshotRes' }) {}
25
24
 
26
- export class LoadSnapshotReq extends Schema.TaggedStruct('LSD.LoadSnapshotReq', {
25
+ export class LoadDatabaseFileReq extends Schema.TaggedStruct('LSD.LoadDatabaseFileReq', {
27
26
  liveStoreVersion,
28
27
  requestId,
29
28
  channelId,
30
- snapshot: Schema.Uint8Array,
31
- }).annotations({ identifier: 'LSD.LoadSnapshotReq' }) {}
29
+ data: Transferable.Uint8Array,
30
+ }).annotations({ identifier: 'LSD.LoadDatabaseFileReq' }) {}
32
31
 
33
- export class LoadSnapshotRes extends Schema.TaggedStruct('LSD.LoadSnapshotRes', {
32
+ export class LoadDatabaseFileRes extends Schema.TaggedStruct('LSD.LoadDatabaseFileRes', {
34
33
  liveStoreVersion,
35
34
  requestId,
36
- }).annotations({ identifier: 'LSD.LoadSnapshotRes' }) {}
35
+ status: Schema.Literal('ok', 'unsupported-file', 'unsupported-database'),
36
+ }).annotations({ identifier: 'LSD.LoadDatabaseFileRes' }) {}
37
37
 
38
38
  export class DebugInfoReq extends Schema.TaggedStruct('LSD.DebugInfoReq', {
39
39
  liveStoreVersion,
@@ -102,21 +102,9 @@ export class MutationLogRes extends Schema.TaggedStruct('LSD.MutationLogRes', {
102
102
  liveStoreVersion,
103
103
  requestId,
104
104
  channelId,
105
- mutationLog: Schema.Uint8Array,
105
+ mutationLog: Transferable.Uint8Array,
106
106
  }).annotations({ identifier: 'LSD.MutationLogRes' }) {}
107
107
 
108
- export class LoadMutationLogReq extends Schema.TaggedStruct('LSD.LoadMutationLogReq', {
109
- liveStoreVersion,
110
- requestId,
111
- channelId,
112
- mutationLog: Schema.Uint8Array,
113
- }).annotations({ identifier: 'LSD.LoadMutationLogReq' }) {}
114
-
115
- export class LoadMutationLogRes extends Schema.TaggedStruct('LSD.LoadMutationLogRes', {
116
- liveStoreVersion,
117
- requestId,
118
- }).annotations({ identifier: 'LSD.LoadMutationLogRes' }) {}
119
-
120
108
  export class ReactivityGraphSubscribe extends Schema.TaggedStruct('LSD.ReactivityGraphSubscribe', {
121
109
  liveStoreVersion,
122
110
  requestId,
@@ -178,6 +166,19 @@ export class ResetAllDataRes extends Schema.TaggedStruct('LSD.ResetAllDataRes',
178
166
  requestId,
179
167
  }).annotations({ identifier: 'LSD.ResetAllDataRes' }) {}
180
168
 
169
+ export class MessagePortForStoreReq extends Schema.TaggedStruct('LSD.MessagePortForStoreReq', {
170
+ liveStoreVersion,
171
+ requestId,
172
+ channelId,
173
+ }).annotations({ identifier: 'LSD.MessagePortForStoreReq' }) {}
174
+
175
+ export class MessagePortForStoreRes extends Schema.TaggedStruct('LSD.MessagePortForStoreRes', {
176
+ liveStoreVersion,
177
+ requestId,
178
+ channelId,
179
+ port: Transferable.MessagePort,
180
+ }).annotations({ identifier: 'LSD.MessagePortForStoreRes' }) {}
181
+
181
182
  export class NetworkStatusChanged extends Schema.TaggedStruct('LSD.NetworkStatusChanged', {
182
183
  liveStoreVersion,
183
184
  channelId,
@@ -215,11 +216,22 @@ export class Pong extends Schema.TaggedStruct('LSD.Pong', {
215
216
  requestId,
216
217
  }).annotations({ identifier: 'LSD.Pong' }) {}
217
218
 
218
- export const MessageToAppHost = Schema.Union(
219
+ export const MessageToAppHostCoordinator = Schema.Union(
219
220
  SnapshotReq,
220
- LoadSnapshotReq,
221
+ LoadDatabaseFileReq,
221
222
  MutationLogReq,
222
- LoadMutationLogReq,
223
+ ResetAllDataReq,
224
+ MessagePortForStoreRes,
225
+ DevtoolsReady,
226
+ Disconnect,
227
+ DevtoolsConnected,
228
+ RunMutationReq,
229
+ Ping,
230
+ ).annotations({ identifier: 'LSD.MessageToAppHostCoordinator' })
231
+
232
+ export type MessageToAppHostCoordinator = typeof MessageToAppHostCoordinator.Type
233
+
234
+ export const MessageToAppHostStore = Schema.Union(
223
235
  DebugInfoReq,
224
236
  DebugInfoResetReq,
225
237
  DebugInfoRerunQueryReq,
@@ -227,39 +239,34 @@ export const MessageToAppHost = Schema.Union(
227
239
  ReactivityGraphUnsubscribe,
228
240
  LiveQueriesSubscribe,
229
241
  LiveQueriesUnsubscribe,
230
- ResetAllDataReq,
231
- DevtoolsReady,
232
- Disconnect,
233
- DevtoolsConnected,
234
- RunMutationReq,
235
- Ping,
236
- ).annotations({ identifier: 'LSD.MessageToAppHost' })
242
+ // Ping,
243
+ ).annotations({ identifier: 'LSD.MessageToAppHostStore' })
237
244
 
238
- export type MessageToAppHost = typeof MessageToAppHost.Type
245
+ export type MessageToAppHostStore = typeof MessageToAppHostStore.Type
239
246
 
240
- export const MessageFromAppHost = Schema.Union(
247
+ export const MessageFromAppHostCoordinator = Schema.Union(
241
248
  SnapshotRes,
242
- LoadSnapshotRes,
249
+ LoadDatabaseFileRes,
243
250
  MutationLogRes,
244
- LoadMutationLogRes,
245
- DebugInfoRes,
246
- DebugInfoResetRes,
247
- DebugInfoRerunQueryRes,
248
- ReactivityGraphRes,
249
- LiveQueriesRes,
250
251
  ResetAllDataRes,
252
+ MessagePortForStoreReq,
251
253
  Disconnect,
252
254
  MutationBroadcast,
253
255
  AppHostReady,
254
256
  NetworkStatusChanged,
255
257
  RunMutationRes,
256
258
  Pong,
257
- ).annotations({ identifier: 'LSD.MessageFromAppHost' })
259
+ ).annotations({ identifier: 'LSD.MessageFromAppHostCoordinator' })
260
+
261
+ export type MessageFromAppHostCoordinator = typeof MessageFromAppHostCoordinator.Type
258
262
 
259
- export type MessageFromAppHost = typeof MessageFromAppHost.Type
263
+ export const MessageFromAppHostStore = Schema.Union(
264
+ DebugInfoRes,
265
+ DebugInfoResetRes,
266
+ DebugInfoRerunQueryRes,
267
+ ReactivityGraphRes,
268
+ LiveQueriesRes,
269
+ // Pong,
270
+ ).annotations({ identifier: 'LSD.MessageFromAppHostStore' })
260
271
 
261
- // TODO make specific over app key
262
- export const makeBroadcastChannels = () => ({
263
- fromAppHost: new BroadcastChannel(`livestore-devtools-from-app-host`),
264
- toAppHost: new BroadcastChannel(`livestore-devtools-to-app-host`),
265
- })
272
+ export type MessageFromAppHostStore = typeof MessageFromAppHostStore.Type
@@ -0,0 +1,25 @@
1
+ import { Schema, Transferable } from '@livestore/utils/effect'
2
+
3
+ const channelId = Schema.String
4
+
5
+ export namespace DevtoolsWindowMessage {
6
+ /** Message is being created in contentscript-iframe, sent to contentscript and then sent to Store */
7
+ export class MessagePortReady extends Schema.TaggedStruct('LSD.WindowMessage.MessagePortForStore', {
8
+ port: Transferable.MessagePort,
9
+ channelId,
10
+ }) {}
11
+
12
+ export class ContentscriptListening extends Schema.TaggedStruct('LSD.WindowMessage.ContentscriptListening', {}) {}
13
+
14
+ // export class ContentscriptReady extends Schema.TaggedStruct('LSD.WindowMessage.ContentscriptReady', {
15
+ // channelId,
16
+ // }) {}
17
+
18
+ export class StoreReady extends Schema.TaggedStruct('LSD.WindowMessage.StoreReady', {
19
+ channelId,
20
+ }) {}
21
+
22
+ export class MessageForStore extends Schema.Union(MessagePortReady, ContentscriptListening) {}
23
+
24
+ export class MessageForContentscript extends Schema.Union(StoreReady) {}
25
+ }
@@ -1 +1,2 @@
1
1
  export * from './devtools-messages.js'
2
+ export * from './devtools-window-message.js'
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from './sync/index.js'
11
11
  export * as Devtools from './devtools/index.js'
12
12
  export * from './debug-info.js'
13
13
  export * from './bounded-collections.js'
14
+ export * from './version.js'
@@ -1,91 +1,127 @@
1
1
  import { shouldNeverHappen } from '@livestore/utils'
2
- import { Schema } from '@livestore/utils/effect'
2
+ import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
3
3
 
4
- import type { InMemoryDatabase, MigrationOptionsFromMutationLog } from './adapter-types.js'
4
+ import { type InMemoryDatabase, type MigrationOptionsFromMutationLog, SqliteError } from './adapter-types.js'
5
5
  import { getExecArgsFromMutation } from './mutation.js'
6
6
  import type { LiveStoreSchema, MutationLogMetaRow } from './schema/index.js'
7
7
  import { MUTATION_LOG_META_TABLE } from './schema/index.js'
8
+ import type { PreparedBindValues } from './util.js'
9
+ import { sql } from './util.js'
8
10
 
9
- export const rehydrateFromMutationLog = async ({
11
+ export const rehydrateFromMutationLog = ({
10
12
  logDb,
11
13
  db,
12
14
  schema,
13
15
  migrationOptions,
16
+ onProgress,
14
17
  }: {
15
18
  logDb: InMemoryDatabase
16
19
  db: InMemoryDatabase
17
20
  schema: LiveStoreSchema
18
21
  migrationOptions: MigrationOptionsFromMutationLog
19
- }) => {
20
- try {
21
- // TODO possibly implement this in a streaming fashion
22
- const stmt = logDb.prepare(`SELECT * FROM ${MUTATION_LOG_META_TABLE} ORDER BY id ASC`)
23
- const results = stmt.select<MutationLogMetaRow>(undefined)
22
+ onProgress: (_: { done: number; total: number }) => Effect.Effect<void>
23
+ }) =>
24
+ Effect.gen(function* () {
25
+ const mutationsCount = logDb
26
+ .prepare(`SELECT COUNT(*) AS count FROM ${MUTATION_LOG_META_TABLE}`)
27
+ .select<{ count: number }>(undefined)[0]!.count
24
28
 
25
- performance.mark('livestore:hydrate-from-mutationlog:start')
29
+ const processMutation = (row: MutationLogMetaRow) =>
30
+ Effect.gen(function* () {
31
+ const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
26
32
 
27
- for (const row of results) {
28
- const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
33
+ if (migrationOptions.excludeMutations?.has(row.mutation) === true) return
29
34
 
30
- if (migrationOptions.excludeMutations?.has(row.mutation) === true) continue
31
-
32
- if (Schema.hash(mutationDef.schema) !== row.schemaHash) {
33
- console.warn(`Schema hash mismatch for mutation ${row.mutation}. Trying to apply mutation anyway.`)
34
- }
35
+ if (Schema.hash(mutationDef.schema) !== row.schemaHash) {
36
+ console.warn(`Schema hash mismatch for mutation ${row.mutation}. Trying to apply mutation anyway.`)
37
+ }
35
38
 
36
- const argsDecodedEither = Schema.decodeUnknownEither(Schema.parseJson(mutationDef.schema))(row.argsJson)
37
- if (argsDecodedEither._tag === 'Left') {
38
- return shouldNeverHappen(`\
39
+ const argsDecodedEither = Schema.decodeUnknownEither(Schema.parseJson(mutationDef.schema))(row.argsJson)
40
+ if (argsDecodedEither._tag === 'Left') {
41
+ return shouldNeverHappen(`\
39
42
  There was an error decoding the persisted mutation event args for mutation "${row.mutation}".
40
43
  This likely means the schema has changed in an incompatible way.
41
44
 
42
45
  Error: ${argsDecodedEither.left}
43
46
  `)
44
- }
45
-
46
- const mutationEventDecoded = {
47
- id: row.id,
48
- mutation: row.mutation,
49
- args: argsDecodedEither.right,
50
- }
51
- // const argsEncoded = JSON.parse(row.args_json)
52
- // const mutationSqlRes =
53
- // typeof mutation.sql === 'string'
54
- // ? mutation.sql
55
- // : mutation.sql(Schema.decodeUnknownSync(mutation.schema)(argsEncoded))
56
- // const mutationSql = typeof mutationSqlRes === 'string' ? mutationSqlRes : mutationSqlRes.sql
57
- // const bindValues = typeof mutationSqlRes === 'string' ? argsEncoded : mutationSqlRes.bindValues
58
-
59
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
60
-
61
- for (const { statementSql, bindValues } of execArgsArr) {
62
- try {
63
- const getRowsChanged = db.execute(statementSql, bindValues)
64
- if (
65
- import.meta.env.DEV &&
66
- getRowsChanged() === 0 &&
67
- migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true
68
- ) {
69
- console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
47
+ }
48
+
49
+ const mutationEventDecoded = {
50
+ id: row.id,
51
+ mutation: row.mutation,
52
+ args: argsDecodedEither.right,
53
+ }
54
+ // const argsEncoded = JSON.parse(row.args_json)
55
+ // const mutationSqlRes =
56
+ // typeof mutation.sql === 'string'
57
+ // ? mutation.sql
58
+ // : mutation.sql(Schema.decodeUnknownSync(mutation.schema)(argsEncoded))
59
+ // const mutationSql = typeof mutationSqlRes === 'string' ? mutationSqlRes : mutationSqlRes.sql
60
+ // const bindValues = typeof mutationSqlRes === 'string' ? argsEncoded : mutationSqlRes.bindValues
61
+
62
+ const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
63
+
64
+ for (const { statementSql, bindValues } of execArgsArr) {
65
+ try {
66
+ // TODO cache prepared statements for mutations
67
+ const getRowsChanged = db.execute(statementSql, bindValues)
68
+ if (
69
+ import.meta.env.DEV &&
70
+ getRowsChanged() === 0 &&
71
+ migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true
72
+ ) {
73
+ console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
74
+ }
75
+ // console.log(`Re-executed mutation ${mutationSql}`, bindValues)
76
+ } catch (e) {
77
+ yield* new SqliteError({
78
+ sql: statementSql,
79
+ bindValues,
80
+ code: (e as any).resultCode,
81
+ cause: e,
82
+ })
70
83
  }
71
- // console.log(`Re-executed mutation ${mutationSql}`, bindValues)
72
- } catch (e) {
73
- console.error(`Error executing migration for mutation ${statementSql}`, bindValues, e)
74
- debugger
75
- throw e
76
84
  }
77
- }
78
- }
79
- } catch (e) {
80
- console.error('Error while rehydrating database from mutation log', e)
81
- debugger
82
- throw e
83
- } finally {
84
- performance.mark('livestore:hydrate-from-mutationlog:end')
85
- performance.measure(
86
- 'livestore:hydrate-from-mutationlog',
87
- 'livestore:hydrate-from-mutationlog:start',
88
- 'livestore:hydrate-from-mutationlog:end',
85
+ }).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
86
+
87
+ const CHUNK_SIZE = 100
88
+
89
+ const stmt = logDb.prepare(sql`\
90
+ SELECT * FROM ${MUTATION_LOG_META_TABLE}
91
+ WHERE id > COALESCE($id, '')
92
+ ORDER BY id ASC
93
+ LIMIT ${CHUNK_SIZE}
94
+ `)
95
+
96
+ let processedMutations = 0
97
+
98
+ yield* Stream.unfoldChunk<Chunk.Chunk<MutationLogMetaRow> | { _tag: 'Initial ' }, MutationLogMetaRow>(
99
+ { _tag: 'Initial ' },
100
+ (item) => {
101
+ // End stream if no more rows
102
+ if (Chunk.isChunk(item) && item.length === 0) return Option.none()
103
+
104
+ const lastId = Chunk.isChunk(item) ? Chunk.last(item).pipe(Option.getOrUndefined)?.id : undefined
105
+ const nextItem = Chunk.fromIterable(
106
+ stmt.select<MutationLogMetaRow>({ $id: lastId } as any as PreparedBindValues),
107
+ )
108
+ const prevItem = Chunk.isChunk(item) ? item : Chunk.empty()
109
+ return Option.some([prevItem, nextItem])
110
+ },
111
+ ).pipe(
112
+ (_) => _,
113
+ Stream.bufferChunks({ capacity: 2 }),
114
+ Stream.tap((row) =>
115
+ Effect.gen(function* () {
116
+ yield* processMutation(row)
117
+
118
+ processedMutations++
119
+ yield* onProgress({ done: processedMutations, total: mutationsCount })
120
+ }),
121
+ ),
122
+ Stream.runDrain,
89
123
  )
90
- }
91
- }
124
+ }).pipe(
125
+ Effect.withPerformanceMeasure('@livestore/common:rehydrateFromMutationLog'),
126
+ Effect.withSpan('@livestore/common:rehydrateFromMutationLog'),
127
+ )
@@ -86,8 +86,6 @@ export type TableOptions = {
86
86
  * @default false
87
87
  */
88
88
  isSingleton: boolean
89
- // TODO remove
90
- dynamicRegistration: boolean
91
89
  disableAutomaticIdColumn: boolean
92
90
  /**
93
91
  * Setting this to true will automatically derive insert, update and delete mutations for this table. Example:
@@ -139,7 +137,6 @@ export const table = <
139
137
 
140
138
  const options_: TableOptions = {
141
139
  isSingleton: options?.isSingleton ?? false,
142
- dynamicRegistration: options?.dynamicRegistration ?? false,
143
140
  disableAutomaticIdColumn: options?.disableAutomaticIdColumn ?? false,
144
141
  deriveMutations:
145
142
  options?.deriveMutations === true
@@ -239,7 +236,6 @@ type WithId<TColumns extends SqliteDsl.Columns, TOptions extends TableOptions> =
239
236
 
240
237
  type WithDefaults<TOptionsInput extends TableOptionsInput, TIsSingleColumn extends boolean> = {
241
238
  isSingleton: TOptionsInput['isSingleton'] extends true ? true : false
242
- dynamicRegistration: TOptionsInput['dynamicRegistration'] extends true ? true : false
243
239
  disableAutomaticIdColumn: TOptionsInput['disableAutomaticIdColumn'] extends true ? true : false
244
240
  deriveMutations: TOptionsInput['deriveMutations'] extends true
245
241
  ? { enabled: true; localOnly: boolean }
@@ -1,5 +1,5 @@
1
1
  import { memoizeByStringifyArgs } from '@livestore/utils'
2
- import { Schema as EffectSchema } from '@livestore/utils/effect'
2
+ import { Effect, Schema as EffectSchema } from '@livestore/utils/effect'
3
3
  import * as otel from '@opentelemetry/api'
4
4
  import { SqliteAst, SqliteDsl } from 'effect-db-schema'
5
5
 
@@ -20,85 +20,104 @@ import { validateSchema } from './validate-mutation-defs.js'
20
20
 
21
21
  const getMemoizedTimestamp = memoizeByStringifyArgs(() => new Date().toISOString())
22
22
 
23
- export const makeSchemaManager = (db: InMemoryDatabase): SchemaManager => {
24
- migrateTable({
25
- db,
26
- otelContext: otel.context.active(),
27
- tableAst: schemaMutationsMetaTable.sqliteDef.ast,
28
- behaviour: 'create-if-not-exists',
23
+ export const makeSchemaManager = (db: InMemoryDatabase): Effect.Effect<SchemaManager> =>
24
+ Effect.gen(function* () {
25
+ yield* migrateTable({
26
+ db,
27
+ otelContext: otel.context.active(),
28
+ tableAst: schemaMutationsMetaTable.sqliteDef.ast,
29
+ behaviour: 'create-if-not-exists',
30
+ })
31
+
32
+ return {
33
+ getMutationDefInfos: () => {
34
+ const schemaMutationsMetaRows = dbSelect<SchemaMutationsMetaRow>(
35
+ db,
36
+ sql`SELECT * FROM ${SCHEMA_MUTATIONS_META_TABLE}`,
37
+ )
38
+
39
+ return schemaMutationsMetaRows
40
+ },
41
+ setMutationDefInfo: (info) => {
42
+ dbExecute(
43
+ db,
44
+ sql`INSERT OR REPLACE INTO ${SCHEMA_MUTATIONS_META_TABLE} (mutationName, schemaHash, updatedAt) VALUES ($mutationName, $schemaHash, $updatedAt)`,
45
+ {
46
+ mutationName: info.mutationName,
47
+ schemaHash: info.schemaHash,
48
+ updatedAt: new Date().toISOString(),
49
+ },
50
+ )
51
+ },
52
+ }
29
53
  })
30
54
 
31
- return {
32
- getMutationDefInfos: () => {
33
- const schemaMutationsMetaRows = dbSelect<SchemaMutationsMetaRow>(
34
- db,
35
- sql`SELECT * FROM ${SCHEMA_MUTATIONS_META_TABLE}`,
36
- )
37
-
38
- return schemaMutationsMetaRows
39
- },
40
- setMutationDefInfo: (info) => {
41
- dbExecute(
42
- db,
43
- sql`INSERT OR REPLACE INTO ${SCHEMA_MUTATIONS_META_TABLE} (mutationName, schemaHash, updatedAt) VALUES ($mutationName, $schemaHash, $updatedAt)`,
44
- {
45
- mutationName: info.mutationName,
46
- schemaHash: info.schemaHash,
47
- updatedAt: new Date().toISOString(),
48
- },
49
- )
50
- },
51
- }
52
- }
53
-
54
55
  // TODO more graceful DB migration (e.g. backup DB before destructive migrations)
55
56
  export const migrateDb = ({
56
57
  db,
57
58
  otelContext = otel.context.active(),
58
59
  schema,
60
+ onProgress,
59
61
  }: {
60
62
  db: InMemoryDatabase
61
63
  otelContext?: otel.Context
62
64
  schema: LiveStoreSchema
63
- }) => {
64
- migrateTable({
65
- db,
66
- otelContext,
67
- tableAst: schemaMetaTable.sqliteDef.ast,
68
- behaviour: 'create-if-not-exists',
69
- })
65
+ onProgress?: (opts: { done: number; total: number }) => Effect.Effect<void>
66
+ }) =>
67
+ Effect.gen(function* () {
68
+ yield* migrateTable({
69
+ db,
70
+ otelContext,
71
+ tableAst: schemaMetaTable.sqliteDef.ast,
72
+ behaviour: 'create-if-not-exists',
73
+ })
70
74
 
71
- validateSchema(schema, makeSchemaManager(db))
75
+ // TODO enforce that migrating tables isn't allowed once the store is running
72
76
 
73
- const schemaMetaRows = dbSelect<SchemaMetaRow>(db, sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
77
+ const schemaManager = yield* makeSchemaManager(db)
78
+ validateSchema(schema, schemaManager)
74
79
 
75
- const dbSchemaHashByTable = Object.fromEntries(
76
- schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
77
- )
80
+ const schemaMetaRows = dbSelect<SchemaMetaRow>(db, sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
78
81
 
79
- const tableDefs = new Set([
80
- // NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
81
- ...systemTables,
82
- ...Array.from(schema.tables.values()).filter((_) => _.sqliteDef.name !== SCHEMA_META_TABLE),
83
- ])
82
+ const dbSchemaHashByTable = Object.fromEntries(
83
+ schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
84
+ )
84
85
 
85
- for (const tableDef of tableDefs) {
86
- const tableAst = tableDef.sqliteDef.ast
87
- const tableName = tableAst.name
88
- const dbSchemaHash = dbSchemaHashByTable[tableName]
89
- const schemaHash = SqliteAst.hash(tableAst)
86
+ const tableDefs = new Set([
87
+ // NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
88
+ ...systemTables,
89
+ ...Array.from(schema.tables.values()).filter((_) => _.sqliteDef.name !== SCHEMA_META_TABLE),
90
+ ])
90
91
 
91
- const skipMigrations = import.meta.env.VITE_LIVESTORE_SKIP_MIGRATIONS !== undefined
92
+ const tablesToMigrate = new Set<{ tableAst: SqliteAst.Table; schemaHash: number }>()
92
93
 
93
- if (schemaHash !== dbSchemaHash && skipMigrations === false) {
94
- console.log(
95
- `Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
96
- )
94
+ for (const tableDef of tableDefs) {
95
+ const tableAst = tableDef.sqliteDef.ast
96
+ const tableName = tableAst.name
97
+ const dbSchemaHash = dbSchemaHashByTable[tableName]
98
+ const schemaHash = SqliteAst.hash(tableAst)
99
+
100
+ if (schemaHash !== dbSchemaHash) {
101
+ tablesToMigrate.add({ tableAst, schemaHash })
97
102
 
98
- migrateTable({ db, tableAst, otelContext, schemaHash, behaviour: 'drop-and-recreate' })
103
+ console.log(
104
+ `Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
105
+ )
106
+ }
99
107
  }
100
- }
101
- }
108
+
109
+ let processedTables = 0
110
+ const tablesCount = tablesToMigrate.size
111
+
112
+ for (const { tableAst, schemaHash } of tablesToMigrate) {
113
+ yield* migrateTable({ db, tableAst, otelContext, schemaHash, behaviour: 'create-if-not-exists' })
114
+
115
+ if (onProgress !== undefined) {
116
+ processedTables++
117
+ yield* onProgress({ done: processedTables, total: tablesCount })
118
+ }
119
+ }
120
+ })
102
121
 
103
122
  export const migrateTable = ({
104
123
  db,
@@ -114,36 +133,37 @@ export const migrateTable = ({
114
133
  schemaHash?: number
115
134
  behaviour: 'drop-and-recreate' | 'create-if-not-exists'
116
135
  skipMetaTable?: boolean
117
- }) => {
118
- console.log(`Migrating table '${tableAst.name}'...`)
119
- const tableName = tableAst.name
120
- const columnSpec = makeColumnSpec(tableAst)
121
-
122
- if (behaviour === 'drop-and-recreate') {
123
- // TODO need to possibly handle cascading deletes due to foreign keys
124
- dbExecute(db, sql`drop table if exists ${tableName}`)
125
- dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
126
- } else if (behaviour === 'create-if-not-exists') {
127
- dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
128
- }
136
+ }) =>
137
+ Effect.gen(function* () {
138
+ console.log(`Migrating table '${tableAst.name}'...`)
139
+ const tableName = tableAst.name
140
+ const columnSpec = makeColumnSpec(tableAst)
141
+
142
+ if (behaviour === 'drop-and-recreate') {
143
+ // TODO need to possibly handle cascading deletes due to foreign keys
144
+ dbExecute(db, sql`drop table if exists ${tableName}`)
145
+ dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
146
+ } else if (behaviour === 'create-if-not-exists') {
147
+ dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
148
+ }
129
149
 
130
- for (const index of tableAst.indexes) {
131
- dbExecute(db, createIndexFromDefinition(tableName, index))
132
- }
150
+ for (const index of tableAst.indexes) {
151
+ dbExecute(db, createIndexFromDefinition(tableName, index))
152
+ }
133
153
 
134
- if (skipMetaTable !== true) {
135
- const updatedAt = getMemoizedTimestamp()
154
+ if (skipMetaTable !== true) {
155
+ const updatedAt = getMemoizedTimestamp()
136
156
 
137
- dbExecute(
138
- db,
139
- sql`
157
+ dbExecute(
158
+ db,
159
+ sql`
140
160
  INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
141
161
  ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
142
162
  `,
143
- { tableName, schemaHash, updatedAt },
144
- )
145
- }
146
- }
163
+ { tableName, schemaHash, updatedAt },
164
+ )
165
+ }
166
+ }).pipe(Effect.withSpan('@livestore/common:migrateTable', { attributes: { tableName: tableAst.name } }))
147
167
 
148
168
  const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
149
169
  const uniqueStr = index.unique ? 'UNIQUE' : ''
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ import packageJson from '../package.json' assert { type: 'json' }
2
+
3
+ export const liveStoreVersion = packageJson.version