@livestore/common 0.3.1 → 0.3.2-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.
Files changed (172) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +35 -0
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
  4. package/dist/ClientSessionLeaderThreadProxy.js +6 -0
  5. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
  6. package/dist/adapter-types.d.ts +10 -161
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +5 -49
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/defs.d.ts +20 -0
  11. package/dist/defs.d.ts.map +1 -0
  12. package/dist/defs.js +12 -0
  13. package/dist/defs.js.map +1 -0
  14. package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
  15. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  16. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  17. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  18. package/dist/devtools/devtools-messages-leader.d.ts +26 -24
  19. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  20. package/dist/errors.d.ts +50 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +36 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
  25. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  26. package/dist/leader-thread/LeaderSyncProcessor.js +112 -122
  27. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  28. package/dist/leader-thread/eventlog.d.ts +17 -6
  29. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  30. package/dist/leader-thread/eventlog.js +32 -17
  31. package/dist/leader-thread/eventlog.js.map +1 -1
  32. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  33. package/dist/leader-thread/leader-worker-devtools.js +1 -2
  34. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  35. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  36. package/dist/leader-thread/make-leader-thread-layer.js +37 -7
  37. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  38. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  39. package/dist/leader-thread/materialize-event.js +7 -1
  40. package/dist/leader-thread/materialize-event.js.map +1 -1
  41. package/dist/leader-thread/mod.d.ts +1 -0
  42. package/dist/leader-thread/mod.d.ts.map +1 -1
  43. package/dist/leader-thread/mod.js +1 -0
  44. package/dist/leader-thread/mod.js.map +1 -1
  45. package/dist/leader-thread/recreate-db.d.ts +13 -6
  46. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  47. package/dist/leader-thread/recreate-db.js +1 -3
  48. package/dist/leader-thread/recreate-db.js.map +1 -1
  49. package/dist/leader-thread/types.d.ts +5 -7
  50. package/dist/leader-thread/types.d.ts.map +1 -1
  51. package/dist/make-client-session.d.ts +1 -1
  52. package/dist/make-client-session.d.ts.map +1 -1
  53. package/dist/make-client-session.js +1 -1
  54. package/dist/make-client-session.js.map +1 -1
  55. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  56. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  57. package/dist/rematerialize-from-eventlog.js +10 -2
  58. package/dist/rematerialize-from-eventlog.js.map +1 -1
  59. package/dist/schema/EventDef.d.ts +2 -2
  60. package/dist/schema/EventDef.d.ts.map +1 -1
  61. package/dist/schema/EventDef.js +2 -2
  62. package/dist/schema/EventDef.js.map +1 -1
  63. package/dist/schema/EventSequenceNumber.d.ts +20 -2
  64. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  65. package/dist/schema/EventSequenceNumber.js +71 -19
  66. package/dist/schema/EventSequenceNumber.js.map +1 -1
  67. package/dist/schema/EventSequenceNumber.test.js +88 -3
  68. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  69. package/dist/schema/LiveStoreEvent.d.ts +25 -11
  70. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  71. package/dist/schema/LiveStoreEvent.js +12 -4
  72. package/dist/schema/LiveStoreEvent.js.map +1 -1
  73. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
  74. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  75. package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
  76. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
  77. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  78. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  79. package/dist/schema/state/sqlite/query-builder/api.d.ts +35 -8
  80. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  81. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
  82. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  83. package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
  84. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  85. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -81
  86. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
  88. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  89. package/dist/schema/state/sqlite/system-tables.d.ts +67 -62
  90. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  91. package/dist/schema/state/sqlite/system-tables.js +8 -17
  92. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  93. package/dist/schema/state/sqlite/table-def.d.ts +1 -1
  94. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  95. package/dist/schema-management/migrations.d.ts +3 -1
  96. package/dist/schema-management/migrations.d.ts.map +1 -1
  97. package/dist/schema-management/migrations.js.map +1 -1
  98. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  99. package/dist/sql-queries/sql-queries.js +2 -0
  100. package/dist/sql-queries/sql-queries.js.map +1 -1
  101. package/dist/sqlite-types.d.ts +72 -0
  102. package/dist/sqlite-types.d.ts.map +1 -0
  103. package/dist/sqlite-types.js +5 -0
  104. package/dist/sqlite-types.js.map +1 -0
  105. package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -2
  106. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  107. package/dist/sync/ClientSessionSyncProcessor.js +16 -13
  108. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  109. package/dist/sync/next/graphology.d.ts.map +1 -1
  110. package/dist/sync/next/graphology.js +0 -6
  111. package/dist/sync/next/graphology.js.map +1 -1
  112. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  113. package/dist/sync/next/rebase-events.js +1 -0
  114. package/dist/sync/next/rebase-events.js.map +1 -1
  115. package/dist/sync/next/test/compact-events.test.js +1 -1
  116. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  117. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  118. package/dist/sync/next/test/event-fixtures.js +12 -3
  119. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  120. package/dist/sync/sync.d.ts +2 -0
  121. package/dist/sync/sync.d.ts.map +1 -1
  122. package/dist/sync/sync.js +3 -0
  123. package/dist/sync/sync.js.map +1 -1
  124. package/dist/sync/syncstate.d.ts +13 -4
  125. package/dist/sync/syncstate.d.ts.map +1 -1
  126. package/dist/sync/syncstate.js +23 -10
  127. package/dist/sync/syncstate.js.map +1 -1
  128. package/dist/sync/syncstate.test.js +17 -17
  129. package/dist/sync/syncstate.test.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.d.ts.map +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/version.js.map +1 -1
  134. package/package.json +7 -6
  135. package/src/ClientSessionLeaderThreadProxy.ts +40 -0
  136. package/src/adapter-types.ts +19 -166
  137. package/src/defs.ts +17 -0
  138. package/src/errors.ts +49 -0
  139. package/src/leader-thread/LeaderSyncProcessor.ts +141 -180
  140. package/src/leader-thread/eventlog.ts +78 -56
  141. package/src/leader-thread/leader-worker-devtools.ts +1 -2
  142. package/src/leader-thread/make-leader-thread-layer.ts +52 -8
  143. package/src/leader-thread/materialize-event.ts +8 -1
  144. package/src/leader-thread/mod.ts +1 -0
  145. package/src/leader-thread/recreate-db.ts +99 -91
  146. package/src/leader-thread/types.ts +6 -11
  147. package/src/make-client-session.ts +2 -2
  148. package/src/rematerialize-from-eventlog.ts +10 -2
  149. package/src/schema/EventDef.ts +5 -3
  150. package/src/schema/EventSequenceNumber.test.ts +120 -3
  151. package/src/schema/EventSequenceNumber.ts +95 -23
  152. package/src/schema/LiveStoreEvent.ts +20 -4
  153. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
  154. package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
  155. package/src/schema/state/sqlite/mod.ts +1 -1
  156. package/src/schema/state/sqlite/query-builder/api.ts +38 -8
  157. package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
  158. package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
  159. package/src/schema/state/sqlite/system-tables.ts +9 -22
  160. package/src/schema/state/sqlite/table-def.ts +1 -1
  161. package/src/schema-management/migrations.ts +3 -1
  162. package/src/sql-queries/sql-queries.ts +2 -0
  163. package/src/sqlite-types.ts +76 -0
  164. package/src/sync/ClientSessionSyncProcessor.ts +17 -20
  165. package/src/sync/next/graphology.ts +0 -6
  166. package/src/sync/next/rebase-events.ts +1 -0
  167. package/src/sync/next/test/compact-events.test.ts +1 -1
  168. package/src/sync/next/test/event-fixtures.ts +12 -3
  169. package/src/sync/sync.ts +3 -0
  170. package/src/sync/syncstate.test.ts +17 -17
  171. package/src/sync/syncstate.ts +31 -10
  172. package/src/version.ts +1 -1
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/complexity/noArguments: using arguments is fine here */
1
2
  import { casesHandled, shouldNeverHappen } from '@livestore/utils'
2
3
  import { Match, Option, Predicate, Schema } from '@livestore/utils/effect'
3
4
 
@@ -36,7 +37,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
36
37
  select: { columns },
37
38
  }) as any
38
39
  },
39
- // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
40
+ // biome-ignore lint/complexity/useArrowFunction: prefer function over arrow function for this case
40
41
  where: function () {
41
42
  if (ast._tag === 'InsertQuery') return invalidQueryBuilder('Cannot use where with insert')
42
43
  if (ast._tag === 'RowQuery') return invalidQueryBuilder('Cannot use where with row')
@@ -136,7 +137,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
136
137
  ),
137
138
  })
138
139
  },
139
- first: (options) => {
140
+ first: (behaviour) => {
140
141
  assertSelectQueryBuilderAst(ast)
141
142
 
142
143
  if (ast.limit._tag === 'Some') return invalidQueryBuilder(`.first() can't be called after .limit()`)
@@ -144,8 +145,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
144
145
  return makeQueryBuilder(tableDef, {
145
146
  ...ast,
146
147
  limit: Option.some(1),
147
- pickFirst:
148
- options?.fallback !== undefined && options.fallback !== 'throws' ? { fallback: options.fallback } : 'throws',
148
+ pickFirst: { _tag: 'enabled', ...(behaviour ?? { behaviour: 'undefined' }) },
149
149
  })
150
150
  },
151
151
  // // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
@@ -249,7 +249,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
249
249
  return {
250
250
  [QueryBuilderTypeId]: QueryBuilderTypeId,
251
251
  [QueryBuilderAstSymbol]: ast,
252
- ['ResultType']: 'only-for-type-inference' as TResult,
252
+ ResultType: 'only-for-type-inference' as TResult,
253
253
  asSql: () => astToSql(ast),
254
254
  toString: () => {
255
255
  try {
@@ -266,7 +266,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
266
266
  const emptyAst = (tableDef: TableDefBase): QueryBuilderAst.SelectQuery => ({
267
267
  _tag: 'SelectQuery',
268
268
  columns: [],
269
- pickFirst: false,
269
+ pickFirst: { _tag: 'disabled' },
270
270
  select: { columns: [] },
271
271
  orderBy: [],
272
272
  offset: Option.none(),
@@ -280,28 +280,28 @@ const emptyAst = (tableDef: TableDefBase): QueryBuilderAst.SelectQuery => ({
280
280
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
281
281
  function assertSelectQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.SelectQuery {
282
282
  if (ast._tag !== 'SelectQuery') {
283
- return shouldNeverHappen('Expected SelectQuery but got ' + ast._tag)
283
+ return shouldNeverHappen(`Expected SelectQuery but got ${ast._tag}`)
284
284
  }
285
285
  }
286
286
 
287
287
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
288
288
  function assertInsertQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.InsertQuery {
289
289
  if (ast._tag !== 'InsertQuery') {
290
- return shouldNeverHappen('Expected InsertQuery but got ' + ast._tag)
290
+ return shouldNeverHappen(`Expected InsertQuery but got ${ast._tag}`)
291
291
  }
292
292
  }
293
293
 
294
294
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
295
295
  function assertWriteQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.WriteQuery {
296
296
  if (ast._tag !== 'InsertQuery' && ast._tag !== 'UpdateQuery' && ast._tag !== 'DeleteQuery') {
297
- return shouldNeverHappen('Expected WriteQuery but got ' + ast._tag)
297
+ return shouldNeverHappen(`Expected WriteQuery but got ${ast._tag}`)
298
298
  }
299
299
  }
300
300
 
301
301
  const isRowQuery = (ast: QueryBuilderAst): ast is QueryBuilderAst.RowQuery => ast._tag === 'RowQuery'
302
302
 
303
303
  export const invalidQueryBuilder = (msg?: string) => {
304
- return shouldNeverHappen('Invalid query builder' + (msg ? `: ${msg}` : ''))
304
+ return shouldNeverHappen(`Invalid query builder${msg ? `: ${msg}` : ''}`)
305
305
  }
306
306
 
307
307
  export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<any> => {
@@ -309,9 +309,12 @@ export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<
309
309
  switch (queryAst._tag) {
310
310
  case 'SelectQuery': {
311
311
  const arraySchema = Schema.Array(queryAst.resultSchemaSingle)
312
- if (queryAst.pickFirst === false) {
312
+ if (queryAst.pickFirst._tag === 'disabled') {
313
313
  return arraySchema
314
- } else if (queryAst.pickFirst === 'throws') {
314
+ } else if (queryAst.pickFirst.behaviour === 'undefined') {
315
+ const arraySchema = Schema.Array(Schema.UndefinedOr(queryAst.resultSchemaSingle))
316
+ return arraySchema.pipe(Schema.headOrElse(() => undefined))
317
+ } else if (queryAst.pickFirst.behaviour === 'error') {
315
318
  // Will throw if the array is empty
316
319
  return arraySchema.pipe(Schema.headOrElse())
317
320
  } else {
@@ -4,7 +4,7 @@ import * as EventSequenceNumber from '../../EventSequenceNumber.js'
4
4
  import { SqliteDsl } from './db-schema/mod.js'
5
5
  import { table } from './table-def.js'
6
6
 
7
- /// Read model DB
7
+ /// State DB
8
8
 
9
9
  export const SCHEMA_META_TABLE = '__livestore_schema'
10
10
 
@@ -46,6 +46,7 @@ export const sessionChangesetMetaTable = table({
46
46
  // TODO bring back primary key
47
47
  seqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
48
48
  seqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
49
+ seqNumRebaseGeneration: SqliteDsl.integer({}),
49
50
  changeset: SqliteDsl.blob({ nullable: true }),
50
51
  debug: SqliteDsl.json({ nullable: true }),
51
52
  },
@@ -54,25 +55,7 @@ export const sessionChangesetMetaTable = table({
54
55
 
55
56
  export type SessionChangesetMetaRow = typeof sessionChangesetMetaTable.Type
56
57
 
57
- export const LEADER_MERGE_COUNTER_TABLE = '__livestore_leader_merge_counter'
58
-
59
- // TODO get rid of this table in favour of client-only merge generation
60
- export const leaderMergeCounterTable = table({
61
- name: LEADER_MERGE_COUNTER_TABLE,
62
- columns: {
63
- id: SqliteDsl.integer({ primaryKey: true, schema: Schema.Literal(0) }),
64
- mergeCounter: SqliteDsl.integer({ primaryKey: true }),
65
- },
66
- })
67
-
68
- export type LeaderMergeCounterRow = typeof leaderMergeCounterTable.Type
69
-
70
- export const stateSystemTables = [
71
- schemaMetaTable,
72
- schemaEventDefsMetaTable,
73
- sessionChangesetMetaTable,
74
- leaderMergeCounterTable,
75
- ]
58
+ export const stateSystemTables = [schemaMetaTable, schemaEventDefsMetaTable, sessionChangesetMetaTable] as const
76
59
 
77
60
  export const isStateSystemTable = (tableName: string) => stateSystemTables.some((_) => _.sqliteDef.name === tableName)
78
61
 
@@ -86,8 +69,11 @@ export const eventlogMetaTable = table({
86
69
  // TODO Adjust modeling so a global event never needs a client id component
87
70
  seqNumGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.GlobalEventSequenceNumber }),
88
71
  seqNumClient: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.ClientEventSequenceNumber }),
72
+ seqNumRebaseGeneration: SqliteDsl.integer({ primaryKey: true }),
89
73
  parentSeqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
90
74
  parentSeqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
75
+ parentSeqNumRebaseGeneration: SqliteDsl.integer({}),
76
+ /** Event definition name */
91
77
  name: SqliteDsl.text({}),
92
78
  argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
93
79
  clientId: SqliteDsl.text({}),
@@ -97,7 +83,7 @@ export const eventlogMetaTable = table({
97
83
  },
98
84
  indexes: [
99
85
  { columns: ['seqNumGlobal'], name: 'idx_eventlog_seqNumGlobal' },
100
- { columns: ['seqNumGlobal', 'seqNumClient'], name: 'idx_eventlog_seqNum' },
86
+ { columns: ['seqNumGlobal', 'seqNumClient', 'seqNumRebaseGeneration'], name: 'idx_eventlog_seqNum' },
101
87
  ],
102
88
  })
103
89
 
@@ -105,6 +91,7 @@ export type EventlogMetaRow = typeof eventlogMetaTable.Type
105
91
 
106
92
  export const SYNC_STATUS_TABLE = '__livestore_sync_status'
107
93
 
94
+ // TODO support sync backend identity (to detect if sync backend changes)
108
95
  export const syncStatusTable = table({
109
96
  name: SYNC_STATUS_TABLE,
110
97
  columns: {
@@ -114,4 +101,4 @@ export const syncStatusTable = table({
114
101
 
115
102
  export type SyncStatusRow = typeof syncStatusTable.Type
116
103
 
117
- export const eventlogSystemTables = [eventlogMetaTable, syncStatusTable]
104
+ export const eventlogSystemTables = [eventlogMetaTable, syncStatusTable] as const
@@ -1,4 +1,4 @@
1
- import { type Nullable } from '@livestore/utils'
1
+ import type { Nullable } from '@livestore/utils'
2
2
  import type { Schema, Types } from '@livestore/utils/effect'
3
3
 
4
4
  import { SqliteDsl } from './db-schema/mod.js'
@@ -1,7 +1,9 @@
1
1
  import { memoizeByStringifyArgs } from '@livestore/utils'
2
2
  import { Effect, Schema as EffectSchema } from '@livestore/utils/effect'
3
3
 
4
- import type { MigrationsReport, MigrationsReportEntry, SqliteDb, UnexpectedError } from '../adapter-types.js'
4
+ import type { SqliteDb } from '../adapter-types.js'
5
+ import type { MigrationsReport, MigrationsReportEntry } from '../defs.js'
6
+ import type { UnexpectedError } from '../errors.js'
5
7
  import type { LiveStoreSchema } from '../schema/mod.js'
6
8
  import { SqliteAst, SqliteDsl } from '../schema/state/sqlite/db-schema/mod.js'
7
9
  import type { SchemaEventDefsMetaRow, SchemaMetaRow } from '../schema/state/sqlite/system-tables.js'
@@ -106,6 +106,7 @@ export const insertRows = <TColumns extends SqliteDsl.Columns>({
106
106
 
107
107
  const bindValues = valuesArray.reduce(
108
108
  (acc, values, itemIndex) => ({
109
+ // biome-ignore lint/performance/noAccumulatingSpread: TODO improve this some day
109
110
  ...acc,
110
111
  ...makeBindValues({ columns, values, variablePrefix: `item_${itemIndex}_` }),
111
112
  }),
@@ -292,6 +293,7 @@ Error: ${parseErrorStr}
292
293
  Value:`,
293
294
  value,
294
295
  )
296
+ // biome-ignore lint/suspicious/noDebugger: debug
295
297
  debugger
296
298
  throw res.left
297
299
  } else {
@@ -0,0 +1,76 @@
1
+ import { type Effect, Schema } from '@livestore/utils/effect'
2
+ import type { SqliteError, UnexpectedError } from './errors.js'
3
+ import type { EventSequenceNumber } from './schema/mod.js'
4
+ import type { QueryBuilder } from './schema/state/sqlite/query-builder/api.js'
5
+ import type { PreparedBindValues } from './util.js'
6
+
7
+ /**
8
+ * Common interface for SQLite databases used by LiveStore to facilitate a consistent API across different platforms.
9
+ * Always assumes a synchronous SQLite build with the `bytecode` and `session` extensions enabled.
10
+ * Can be either in-memory or persisted to disk.
11
+ */
12
+ export interface SqliteDb<TReq = any, TMetadata extends TReq = TReq> {
13
+ _tag: 'SqliteDb'
14
+ metadata: TMetadata
15
+ /** Debug information (currently not persisted and only available at runtime) */
16
+ debug: SqliteDebugInfo
17
+ prepare(queryStr: string): PreparedStatement
18
+ execute(
19
+ queryStr: string,
20
+ bindValues?: PreparedBindValues | undefined,
21
+ options?: { onRowsChanged?: (rowsChanged: number) => void },
22
+ ): void
23
+ execute(queryBuilder: QueryBuilder.Any, options?: { onRowsChanged?: (rowsChanged: number) => void }): void
24
+
25
+ select<T>(queryStr: string, bindValues?: PreparedBindValues | undefined): ReadonlyArray<T>
26
+ select<T>(queryBuilder: QueryBuilder<T, any, any>): T
27
+
28
+ export(): Uint8Array
29
+ import: (data: Uint8Array | SqliteDb<TReq>) => void
30
+ close(): void
31
+ destroy(): void
32
+ session(): SqliteDbSession
33
+ makeChangeset: (data: Uint8Array) => SqliteDbChangeset
34
+ }
35
+
36
+ export type SqliteDebugInfo = { head: EventSequenceNumber.EventSequenceNumber }
37
+
38
+ // TODO refactor this helper type. It's quite cumbersome to use and should be revisited.
39
+ export type MakeSqliteDb<
40
+ TReq = { dbPointer: number; persistenceInfo: PersistenceInfo },
41
+ TInput_ extends { _tag: string } = { _tag: string },
42
+ TMetadata_ extends TReq = TReq,
43
+ R = never,
44
+ > = <
45
+ TInput extends TInput_,
46
+ TMetadata extends TMetadata_ & { _tag: TInput['_tag'] } = TMetadata_ & { _tag: TInput['_tag'] },
47
+ >(
48
+ input: TInput,
49
+ ) => Effect.Effect<SqliteDb<TReq, Extract<TMetadata, { _tag: TInput['_tag'] }>>, SqliteError | UnexpectedError, R>
50
+
51
+ export interface PreparedStatement {
52
+ execute(bindValues: PreparedBindValues | undefined, options?: { onRowsChanged?: (rowsChanged: number) => void }): void
53
+ select<T>(bindValues: PreparedBindValues | undefined): ReadonlyArray<T>
54
+ finalize(): void
55
+ sql: string
56
+ }
57
+
58
+ export type SqliteDbSession = {
59
+ changeset: () => Uint8Array | undefined
60
+ finish: () => void
61
+ }
62
+
63
+ export type SqliteDbChangeset = {
64
+ // TODO combining changesets (requires changes in the SQLite WASM binding)
65
+ invert: () => SqliteDbChangeset
66
+ apply: () => void
67
+ }
68
+
69
+ export const PersistenceInfo = Schema.Struct(
70
+ {
71
+ fileName: Schema.String,
72
+ },
73
+ { key: Schema.String, value: Schema.Any },
74
+ ).annotations({ title: 'LiveStore.PersistenceInfo' })
75
+
76
+ export type PersistenceInfo<With extends {} = {}> = typeof PersistenceInfo.Type & With
@@ -4,11 +4,10 @@ import { Option, type Runtime, type Scope } from '@livestore/utils/effect'
4
4
  import { BucketQueue, Effect, FiberHandle, Queue, Schema, Stream, Subscribable } from '@livestore/utils/effect'
5
5
  import * as otel from '@opentelemetry/api'
6
6
 
7
- import type { ClientSession, UnexpectedError } from '../adapter-types.js'
7
+ import { type ClientSession, SyncError, type UnexpectedError } from '../adapter-types.js'
8
8
  import * as EventSequenceNumber from '../schema/EventSequenceNumber.js'
9
9
  import * as LiveStoreEvent from '../schema/LiveStoreEvent.js'
10
- import { getEventDef, type LiveStoreSchema, SystemTables } from '../schema/mod.js'
11
- import { sql } from '../util.js'
10
+ import { getEventDef, type LiveStoreSchema } from '../schema/mod.js'
12
11
  import * as SyncState from './syncstate.js'
13
12
 
14
13
  /**
@@ -21,6 +20,10 @@ import * as SyncState from './syncstate.js'
21
20
  * - We might need to make the rebase behaviour configurable e.g. to let users manually trigger a rebase
22
21
  *
23
22
  * Longer term we should evalutate whether we can unify the ClientSessionSyncProcessor with the LeaderSyncProcessor.
23
+ *
24
+ * The session and leader sync processor are different in the following ways:
25
+ * - The leader sync processor pulls regular LiveStore events, while the session sync processor pulls SyncState.PayloadUpstream items
26
+ * - The session sync processor has no downstream nodes.
24
27
  */
25
28
  export const makeClientSessionSyncProcessor = ({
26
29
  schema,
@@ -37,7 +40,7 @@ export const makeClientSessionSyncProcessor = ({
37
40
  clientSession: ClientSession
38
41
  runtime: Runtime.Runtime<Scope.Scope>
39
42
  materializeEvent: (
40
- eventDecoded: LiveStoreEvent.PartialAnyDecoded,
43
+ eventDecoded: LiveStoreEvent.AnyDecoded,
41
44
  options: { otelContext: otel.Context; withChangeset: boolean; materializerHashLeader: Option.Option<number> },
42
45
  ) => {
43
46
  writeTables: Set<string>
@@ -82,7 +85,10 @@ export const makeClientSessionSyncProcessor = ({
82
85
  let baseEventSequenceNumber = syncStateRef.current.localHead
83
86
  const encodedEventDefs = batch.map(({ name, args }) => {
84
87
  const eventDef = getEventDef(schema, name)
85
- const nextNumPair = EventSequenceNumber.nextPair(baseEventSequenceNumber, eventDef.eventDef.options.clientOnly)
88
+ const nextNumPair = EventSequenceNumber.nextPair({
89
+ seqNum: baseEventSequenceNumber,
90
+ isClient: eventDef.eventDef.options.clientOnly,
91
+ })
86
92
  baseEventSequenceNumber = nextNumPair.seqNum
87
93
  return new LiveStoreEvent.EncodedWithMeta(
88
94
  Schema.encodeUnknownSync(eventSchema)({
@@ -103,7 +109,7 @@ export const makeClientSessionSyncProcessor = ({
103
109
  })
104
110
 
105
111
  if (mergeResult._tag === 'unexpected-error') {
106
- return shouldNeverHappen('Unexpected error in client-session-sync-processor', mergeResult.cause)
112
+ return shouldNeverHappen('Unexpected error in client-session-sync-processor', mergeResult.message)
107
113
  }
108
114
 
109
115
  span.addEvent('local-push', {
@@ -184,18 +190,11 @@ export const makeClientSessionSyncProcessor = ({
184
190
 
185
191
  yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
186
192
 
187
- const getMergeCounter = () =>
188
- clientSession.sqliteDb.select<{ mergeCounter: number }>(
189
- sql`SELECT mergeCounter FROM ${SystemTables.LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
190
- )[0]?.mergeCounter ?? 0
191
-
192
193
  // NOTE We need to lazily call `.pull` as we want the cursor to be updated
193
194
  yield* Stream.suspend(() =>
194
- clientSession.leaderThread.events.pull({
195
- cursor: { mergeCounter: getMergeCounter(), eventNum: syncStateRef.current.localHead },
196
- }),
195
+ clientSession.leaderThread.events.pull({ cursor: syncStateRef.current.upstreamHead }),
197
196
  ).pipe(
198
- Stream.tap(({ payload, mergeCounter: leaderMergeCounter }) =>
197
+ Stream.tap(({ payload }) =>
199
198
  Effect.gen(function* () {
200
199
  // yield* Effect.logDebug('ClientSessionSyncProcessor:pull', payload)
201
200
 
@@ -211,13 +210,13 @@ export const makeClientSessionSyncProcessor = ({
211
210
  })
212
211
 
213
212
  if (mergeResult._tag === 'unexpected-error') {
214
- return yield* Effect.fail(mergeResult.cause)
213
+ return yield* new SyncError({ cause: mergeResult.message })
215
214
  } else if (mergeResult._tag === 'reject') {
216
215
  return shouldNeverHappen('Unexpected reject in client-session-sync-processor', mergeResult)
217
216
  }
218
217
 
219
218
  syncStateRef.current = mergeResult.newSyncState
220
- syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
219
+ yield* syncStateUpdateQueue.offer(mergeResult.newSyncState)
221
220
 
222
221
  if (mergeResult._tag === 'rebase') {
223
222
  span.addEvent('merge:pull:rebase', {
@@ -226,7 +225,7 @@ export const makeClientSessionSyncProcessor = ({
226
225
  newEventsCount: mergeResult.newEvents.length,
227
226
  rollbackCount: mergeResult.rollbackEvents.length,
228
227
  res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
229
- leaderMergeCounter,
228
+ rebaseGeneration: mergeResult.newSyncState.localHead.rebaseGeneration,
230
229
  })
231
230
 
232
231
  debugInfo.rebaseCount++
@@ -243,7 +242,6 @@ export const makeClientSessionSyncProcessor = ({
243
242
  'merge:pull:rebase: rollback',
244
243
  mergeResult.rollbackEvents.length,
245
244
  ...mergeResult.rollbackEvents.slice(0, 10).map((_) => _.toJSON()),
246
- { leaderMergeCounter },
247
245
  ).pipe(Effect.provide(runtime), Effect.runSync)
248
246
  }
249
247
 
@@ -263,7 +261,6 @@ export const makeClientSessionSyncProcessor = ({
263
261
  payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
264
262
  newEventsCount: mergeResult.newEvents.length,
265
263
  res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
266
- leaderMergeCounter,
267
264
  })
268
265
 
269
266
  debugInfo.advanceCount++
@@ -14,15 +14,9 @@ export declare class IGraph<
14
14
  }
15
15
 
16
16
  export const DirectedGraph = class DirectedGraph extends graphology.DirectedGraph {
17
- constructor(options?: graphologyTypes.GraphOptions) {
18
- super(options)
19
- }
20
17
  } as typeof IGraph
21
18
 
22
19
  export const Graph = class Graph extends graphology.Graph {
23
- constructor(options?: graphologyTypes.GraphOptions) {
24
- super(options)
25
- }
26
20
  } as typeof IGraph
27
21
 
28
22
  // export const graphology = graphology_ as graphologyTypes
@@ -38,6 +38,7 @@ export const defaultRebaseFn: RebaseFn = ({ pendingLocalEvents }) => {
38
38
  return { rebasedLocalEvents: pendingLocalEvents }
39
39
  }
40
40
 
41
+ // TODO replace in favour of current rebase impl
41
42
  export const rebaseEvents = ({
42
43
  rebaseFn,
43
44
  pendingLocalEvents,
@@ -64,7 +64,7 @@ const factsSetToString = (facts: EventDefFacts, prefix: string) =>
64
64
  export const customSerializer = {
65
65
  test: (val: unknown) => Array.isArray(val),
66
66
  print: (val: unknown[], _serialize: (item: unknown) => string) => {
67
- return '[\n' + (val as any[]).map((item) => ' ' + customStringify(item)).join('\n') + '\n]'
67
+ return `[\n${(val as any[]).map((item) => ` ${customStringify(item)}`).join('\n')}\n]`
68
68
  },
69
69
  } as any
70
70
 
@@ -144,7 +144,10 @@ export const toEventNodes = (
144
144
 
145
145
  const eventNodes = partialEvents.map((partialEvent) => {
146
146
  const eventDef = eventDefs[partialEvent.name]!
147
- const eventNum = EventSequenceNumber.nextPair(currentEventSequenceNumber, eventDef.options.clientOnly).seqNum
147
+ const eventNum = EventSequenceNumber.nextPair({
148
+ seqNum: currentEventSequenceNumber,
149
+ isClient: eventDef.options.clientOnly,
150
+ }).seqNum
148
151
  currentEventSequenceNumber = eventNum
149
152
 
150
153
  const factsSnapshot = factsSnapshotForDag(historyDagFromNodes(nodesAcc, { skipFactsCheck: true }), undefined)
@@ -221,8 +224,14 @@ const getParentNum = (eventNum: EventSequenceNumber.EventSequenceNumber): EventS
221
224
  const clientParentNum = eventNum.client - 1
222
225
 
223
226
  if (clientParentNum < 0) {
224
- return EventSequenceNumber.make({ global: globalParentNum - 1, client: EventSequenceNumber.clientDefault })
227
+ return EventSequenceNumber.make({
228
+ global: globalParentNum - 1,
229
+ client: EventSequenceNumber.clientDefault,
230
+ })
225
231
  }
226
232
 
227
- return EventSequenceNumber.make({ global: globalParentNum, client: clientParentNum })
233
+ return EventSequenceNumber.make({
234
+ global: globalParentNum,
235
+ client: clientParentNum,
236
+ })
228
237
  }
package/src/sync/sync.ts CHANGED
@@ -80,6 +80,7 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
80
80
 
81
81
  export class IsOfflineError extends Schema.TaggedError<IsOfflineError>()('IsOfflineError', {}) {}
82
82
 
83
+ // TODO gt rid of this error in favour of SyncError
83
84
  export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('InvalidPushError', {
84
85
  reason: Schema.Union(
85
86
  Schema.TaggedStruct('Unexpected', {
@@ -92,10 +93,12 @@ export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('In
92
93
  ),
93
94
  }) {}
94
95
 
96
+ // TODO gt rid of this error in favour of SyncError
95
97
  export class InvalidPullError extends Schema.TaggedError<InvalidPullError>()('InvalidPullError', {
96
98
  message: Schema.String,
97
99
  }) {}
98
100
 
101
+ // TODO gt rid of this error in favour of SyncError
99
102
  export class LeaderAheadError extends Schema.TaggedError<LeaderAheadError>()('LeaderAheadError', {
100
103
  minimumExpectedNum: EventSequenceNumber.EventSequenceNumber,
101
104
  providedNum: EventSequenceNumber.EventSequenceNumber,
@@ -7,10 +7,10 @@ import * as SyncState from './syncstate.js'
7
7
 
8
8
  class TestEvent extends LiveStoreEvent.EncodedWithMeta {
9
9
  constructor(
10
- seqNum: EventSequenceNumber.EventSequenceNumber | typeof EventSequenceNumber.EventSequenceNumber.Encoded,
11
- parentSeqNum: EventSequenceNumber.EventSequenceNumber,
10
+ seqNum: EventSequenceNumber.EventSequenceNumberInput,
11
+ parentSeqNum: EventSequenceNumber.EventSequenceNumberInput,
12
12
  public readonly payload: string,
13
- public readonly isLocal: boolean,
13
+ public readonly isClient: boolean,
14
14
  ) {
15
15
  super({
16
16
  seqNum: EventSequenceNumber.make(seqNum),
@@ -22,8 +22,8 @@ class TestEvent extends LiveStoreEvent.EncodedWithMeta {
22
22
  })
23
23
  }
24
24
 
25
- rebase_ = (parentSeqNum: EventSequenceNumber.EventSequenceNumber) => {
26
- return this.rebase(parentSeqNum, this.isLocal)
25
+ rebase_ = (parentSeqNum: EventSequenceNumber.EventSequenceNumber, rebaseGeneration: number) => {
26
+ return this.rebase({ parentSeqNum, isClient: this.isClient, rebaseGeneration })
27
27
  }
28
28
 
29
29
  // Only used for Vitest printing
@@ -41,7 +41,7 @@ const e2_1 = new TestEvent({ global: 2, client: 1 }, e2_0.seqNum, 'a', true)
41
41
 
42
42
  const isEqualEvent = LiveStoreEvent.isEqualEncoded
43
43
 
44
- const isClientEvent = (event: LiveStoreEvent.EncodedWithMeta) => (event as TestEvent).isLocal
44
+ const isClientEvent = (event: LiveStoreEvent.EncodedWithMeta) => (event as TestEvent).isClient
45
45
 
46
46
  describe('syncstate', () => {
47
47
  describe('merge', () => {
@@ -62,8 +62,8 @@ describe('syncstate', () => {
62
62
  upstreamHead: EventSequenceNumber.ROOT,
63
63
  localHead: e2_0.seqNum,
64
64
  })
65
- const e1_0_e2_0 = e1_0.rebase_(e2_0.seqNum)
66
- const e1_1_e2_1 = e1_1.rebase_(e1_0_e2_0.seqNum)
65
+ const e1_0_e2_0 = e1_0.rebase_(e2_0.seqNum, 0)
66
+ const e1_1_e2_1 = e1_1.rebase_(e1_0_e2_0.seqNum, 0)
67
67
  const result = merge({
68
68
  syncState,
69
69
  payload: SyncState.PayloadUpstreamRebase.make({
@@ -71,7 +71,7 @@ describe('syncstate', () => {
71
71
  newEvents: [e1_0_e2_0, e1_1_e2_1],
72
72
  }),
73
73
  })
74
- const e2_0_e3_0 = e2_0.rebase_(e1_0_e2_0.seqNum)
74
+ const e2_0_e3_0 = e2_0.rebase_(e1_0_e2_0.seqNum, 1)
75
75
  expectRebase(result)
76
76
  expectEventArraysEqual(result.newSyncState.pending, [e2_0_e3_0])
77
77
  expect(result.newSyncState.upstreamHead).toMatchObject(e1_1_e2_1.seqNum)
@@ -86,7 +86,7 @@ describe('syncstate', () => {
86
86
  upstreamHead: EventSequenceNumber.ROOT,
87
87
  localHead: e2_0.seqNum,
88
88
  })
89
- const e1_1_e2_0 = e1_1.rebase_(e1_0.seqNum)
89
+ const e1_1_e2_0 = e1_1.rebase_(e1_0.seqNum, 0)
90
90
  const result = merge({
91
91
  syncState,
92
92
  payload: SyncState.PayloadUpstreamRebase.make({
@@ -94,7 +94,7 @@ describe('syncstate', () => {
94
94
  rollbackEvents: [e1_1],
95
95
  }),
96
96
  })
97
- const e2_0_e3_0 = e2_0.rebase_(e1_1_e2_0.seqNum)
97
+ const e2_0_e3_0 = e2_0.rebase_(e1_1_e2_0.seqNum, 1)
98
98
  expectRebase(result)
99
99
  expectEventArraysEqual(result.newSyncState.pending, [e2_0_e3_0])
100
100
  expect(result.newSyncState.upstreamHead).toMatchObject(e1_1_e2_0.seqNum)
@@ -326,7 +326,7 @@ describe('syncstate', () => {
326
326
  })
327
327
  const result = merge({ syncState, payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: [e1_1] }) })
328
328
 
329
- const e1_0_e1_2 = e1_0.rebase_(e1_1.seqNum)
329
+ const e1_0_e1_2 = e1_0.rebase_(e1_1.seqNum, 1)
330
330
 
331
331
  expectRebase(result)
332
332
  expectEventArraysEqual(result.newSyncState.pending, [e1_0_e1_2])
@@ -344,7 +344,7 @@ describe('syncstate', () => {
344
344
  localHead: e2_0_b.seqNum,
345
345
  })
346
346
  const result = merge({ syncState, payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: [e2_0] }) })
347
- const e2_0_e3_0 = e2_0_b.rebase_(e2_0.seqNum)
347
+ const e2_0_e3_0 = e2_0_b.rebase_(e2_0.seqNum, 1)
348
348
 
349
349
  expectRebase(result)
350
350
  expectEventArraysEqual(result.newSyncState.pending, [e2_0_e3_0])
@@ -365,7 +365,7 @@ describe('syncstate', () => {
365
365
  payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: [e1_1, e1_2, e1_3, e2_0] }),
366
366
  })
367
367
 
368
- const e1_0_e3_0 = e1_0.rebase_(e2_0.seqNum)
368
+ const e1_0_e3_0 = e1_0.rebase_(e2_0.seqNum, 1)
369
369
 
370
370
  expectRebase(result)
371
371
  expectEventArraysEqual(result.newSyncState.pending, [e1_0_e3_0])
@@ -384,7 +384,7 @@ describe('syncstate', () => {
384
384
  payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: [e1_0, e1_2, e1_3, e2_0] }),
385
385
  })
386
386
 
387
- const e1_1_e2_1 = e1_1.rebase_(e2_0.seqNum)
387
+ const e1_1_e2_1 = e1_1.rebase_(e2_0.seqNum, 1)
388
388
 
389
389
  expectRebase(result)
390
390
  expectEventArraysEqual(result.newSyncState.pending, [e1_1_e2_1])
@@ -405,8 +405,8 @@ describe('syncstate', () => {
405
405
  payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: [e1_1, e1_2, e1_3, e2_0] }),
406
406
  })
407
407
 
408
- const e1_0_e2_1 = e1_0.rebase_(e2_0.seqNum)
409
- const e1_1_e2_2 = e1_1.rebase_(e1_0_e2_1.seqNum)
408
+ const e1_0_e2_1 = e1_0.rebase_(e2_0.seqNum, 1)
409
+ const e1_1_e2_2 = e1_1.rebase_(e1_0_e2_1.seqNum, 1)
410
410
 
411
411
  expectRebase(result)
412
412
  expectEventArraysEqual(result.newSyncState.pending, [e1_0_e2_1, e1_1_e2_2])