@livestore/common 0.3.1-dev.0 → 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 (185) 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 -156
  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-leader.d.ts +26 -24
  18. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  19. package/dist/errors.d.ts +50 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +36 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
  28. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  29. package/dist/leader-thread/LeaderSyncProcessor.js +122 -123
  30. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  31. package/dist/leader-thread/eventlog.d.ts +17 -6
  32. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  33. package/dist/leader-thread/eventlog.js +34 -17
  34. package/dist/leader-thread/eventlog.js.map +1 -1
  35. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  36. package/dist/leader-thread/leader-worker-devtools.js +1 -2
  37. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  38. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  39. package/dist/leader-thread/make-leader-thread-layer.js +37 -7
  40. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  41. package/dist/leader-thread/materialize-event.d.ts +3 -3
  42. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  43. package/dist/leader-thread/materialize-event.js +27 -10
  44. package/dist/leader-thread/materialize-event.js.map +1 -1
  45. package/dist/leader-thread/mod.d.ts +2 -0
  46. package/dist/leader-thread/mod.d.ts.map +1 -1
  47. package/dist/leader-thread/mod.js +2 -0
  48. package/dist/leader-thread/mod.js.map +1 -1
  49. package/dist/leader-thread/recreate-db.d.ts +13 -6
  50. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  51. package/dist/leader-thread/recreate-db.js +1 -3
  52. package/dist/leader-thread/recreate-db.js.map +1 -1
  53. package/dist/leader-thread/types.d.ts +6 -7
  54. package/dist/leader-thread/types.d.ts.map +1 -1
  55. package/dist/make-client-session.d.ts +1 -1
  56. package/dist/make-client-session.d.ts.map +1 -1
  57. package/dist/make-client-session.js +1 -1
  58. package/dist/make-client-session.js.map +1 -1
  59. package/dist/materializer-helper.d.ts +13 -2
  60. package/dist/materializer-helper.d.ts.map +1 -1
  61. package/dist/materializer-helper.js +25 -11
  62. package/dist/materializer-helper.js.map +1 -1
  63. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  64. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  65. package/dist/rematerialize-from-eventlog.js +12 -4
  66. package/dist/rematerialize-from-eventlog.js.map +1 -1
  67. package/dist/schema/EventDef.d.ts +8 -3
  68. package/dist/schema/EventDef.d.ts.map +1 -1
  69. package/dist/schema/EventDef.js +5 -2
  70. package/dist/schema/EventDef.js.map +1 -1
  71. package/dist/schema/EventSequenceNumber.d.ts +20 -2
  72. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  73. package/dist/schema/EventSequenceNumber.js +71 -19
  74. package/dist/schema/EventSequenceNumber.js.map +1 -1
  75. package/dist/schema/EventSequenceNumber.test.js +88 -3
  76. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  77. package/dist/schema/LiveStoreEvent.d.ts +56 -8
  78. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  79. package/dist/schema/LiveStoreEvent.js +34 -8
  80. package/dist/schema/LiveStoreEvent.js.map +1 -1
  81. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
  82. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  83. package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
  84. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
  85. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  86. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/query-builder/api.d.ts +36 -9
  88. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  89. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
  90. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  91. package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
  92. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  93. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -86
  94. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
  95. package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
  96. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  97. package/dist/schema/state/sqlite/system-tables.d.ts +380 -432
  98. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  99. package/dist/schema/state/sqlite/system-tables.js +8 -17
  100. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  101. package/dist/schema/state/sqlite/table-def.d.ts +2 -2
  102. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  103. package/dist/schema-management/migrations.d.ts +3 -1
  104. package/dist/schema-management/migrations.d.ts.map +1 -1
  105. package/dist/schema-management/migrations.js.map +1 -1
  106. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  107. package/dist/sql-queries/sql-queries.js +2 -0
  108. package/dist/sql-queries/sql-queries.js.map +1 -1
  109. package/dist/sqlite-db-helper.d.ts +7 -0
  110. package/dist/sqlite-db-helper.d.ts.map +1 -0
  111. package/dist/sqlite-db-helper.js +29 -0
  112. package/dist/sqlite-db-helper.js.map +1 -0
  113. package/dist/sqlite-types.d.ts +72 -0
  114. package/dist/sqlite-types.d.ts.map +1 -0
  115. package/dist/sqlite-types.js +5 -0
  116. package/dist/sqlite-types.js.map +1 -0
  117. package/dist/sync/ClientSessionSyncProcessor.d.ts +12 -3
  118. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  119. package/dist/sync/ClientSessionSyncProcessor.js +37 -19
  120. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  121. package/dist/sync/next/graphology.d.ts.map +1 -1
  122. package/dist/sync/next/graphology.js +0 -6
  123. package/dist/sync/next/graphology.js.map +1 -1
  124. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  125. package/dist/sync/next/rebase-events.js +1 -0
  126. package/dist/sync/next/rebase-events.js.map +1 -1
  127. package/dist/sync/next/test/compact-events.test.js +1 -1
  128. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  129. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  130. package/dist/sync/next/test/event-fixtures.js +12 -3
  131. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  132. package/dist/sync/sync.d.ts +2 -0
  133. package/dist/sync/sync.d.ts.map +1 -1
  134. package/dist/sync/sync.js +3 -0
  135. package/dist/sync/sync.js.map +1 -1
  136. package/dist/sync/syncstate.d.ts +13 -4
  137. package/dist/sync/syncstate.d.ts.map +1 -1
  138. package/dist/sync/syncstate.js +23 -10
  139. package/dist/sync/syncstate.js.map +1 -1
  140. package/dist/sync/syncstate.test.js +17 -17
  141. package/dist/sync/syncstate.test.js.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/package.json +7 -6
  145. package/src/ClientSessionLeaderThreadProxy.ts +40 -0
  146. package/src/adapter-types.ts +19 -161
  147. package/src/defs.ts +17 -0
  148. package/src/errors.ts +49 -0
  149. package/src/index.ts +1 -0
  150. package/src/leader-thread/LeaderSyncProcessor.ts +157 -181
  151. package/src/leader-thread/eventlog.ts +78 -54
  152. package/src/leader-thread/leader-worker-devtools.ts +1 -2
  153. package/src/leader-thread/make-leader-thread-layer.ts +52 -8
  154. package/src/leader-thread/materialize-event.ts +33 -12
  155. package/src/leader-thread/mod.ts +2 -0
  156. package/src/leader-thread/recreate-db.ts +99 -91
  157. package/src/leader-thread/types.ts +10 -12
  158. package/src/make-client-session.ts +2 -2
  159. package/src/materializer-helper.ts +45 -19
  160. package/src/rematerialize-from-eventlog.ts +12 -4
  161. package/src/schema/EventDef.ts +16 -4
  162. package/src/schema/EventSequenceNumber.test.ts +120 -3
  163. package/src/schema/EventSequenceNumber.ts +95 -23
  164. package/src/schema/LiveStoreEvent.ts +49 -8
  165. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
  166. package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
  167. package/src/schema/state/sqlite/mod.ts +1 -1
  168. package/src/schema/state/sqlite/query-builder/api.ts +39 -9
  169. package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
  170. package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
  171. package/src/schema/state/sqlite/system-tables.ts +9 -22
  172. package/src/schema/state/sqlite/table-def.ts +2 -2
  173. package/src/schema-management/migrations.ts +3 -1
  174. package/src/sql-queries/sql-queries.ts +2 -0
  175. package/src/sqlite-db-helper.ts +41 -0
  176. package/src/sqlite-types.ts +76 -0
  177. package/src/sync/ClientSessionSyncProcessor.ts +51 -28
  178. package/src/sync/next/graphology.ts +0 -6
  179. package/src/sync/next/rebase-events.ts +1 -0
  180. package/src/sync/next/test/compact-events.test.ts +1 -1
  181. package/src/sync/next/test/event-fixtures.ts +12 -3
  182. package/src/sync/sync.ts +3 -0
  183. package/src/sync/syncstate.test.ts +17 -17
  184. package/src/sync/syncstate.ts +31 -10
  185. package/src/version.ts +1 -1
@@ -12,13 +12,13 @@ import type {
12
12
  import { Context, Schema } from '@livestore/utils/effect'
13
13
  import type { MeshNode } from '@livestore/webmesh'
14
14
 
15
- import type { LeaderPullCursor, SqliteError } from '../adapter-types.js'
15
+ import type { MigrationsReport } from '../defs.js'
16
+ import type { SqliteError } from '../errors.js'
16
17
  import type {
17
18
  BootStatus,
18
19
  Devtools,
19
20
  LeaderAheadError,
20
21
  MakeSqliteDb,
21
- MigrationsReport,
22
22
  PersistenceInfo,
23
23
  SqliteDb,
24
24
  SyncBackend,
@@ -121,7 +121,10 @@ export type MaterializeEvent = (
121
121
  skipEventlog?: boolean
122
122
  },
123
123
  ) => Effect.Effect<
124
- { sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' } },
124
+ {
125
+ sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' }
126
+ hash: Option.Option<number>
127
+ },
125
128
  SqliteError | UnexpectedError
126
129
  >
127
130
 
@@ -133,16 +136,12 @@ export type InitialBlockingSyncContext = {
133
136
  export interface LeaderSyncProcessor {
134
137
  /** Used by client sessions to subscribe to upstream sync state changes */
135
138
  pull: (args: {
136
- cursor: LeaderPullCursor
137
- }) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }, UnexpectedError>
139
+ cursor: EventSequenceNumber.EventSequenceNumber
140
+ }) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type }, UnexpectedError>
138
141
  /** The `pullQueue` API can be used instead of `pull` when more convenient */
139
142
  pullQueue: (args: {
140
- cursor: LeaderPullCursor
141
- }) => Effect.Effect<
142
- Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
143
- UnexpectedError,
144
- Scope.Scope
145
- >
143
+ cursor: EventSequenceNumber.EventSequenceNumber
144
+ }) => Effect.Effect<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>, UnexpectedError, Scope.Scope>
146
145
 
147
146
  /** Used by client sessions to push events to the leader thread */
148
147
  push: (
@@ -170,5 +169,4 @@ export interface LeaderSyncProcessor {
170
169
  LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
171
170
  >
172
171
  syncState: Subscribable.Subscribable<SyncState.SyncState>
173
- getMergeCounter: () => number
174
172
  }
@@ -39,7 +39,7 @@ export const makeClientSession = <R>({
39
39
  sessionId: string
40
40
  isLeader: boolean
41
41
  lockStatus: SubscriptionRef.SubscriptionRef<LockStatus>
42
- leaderThread: ClientSessionLeaderThreadProxy
42
+ leaderThread: ClientSessionLeaderThreadProxy.ClientSessionLeaderThreadProxy
43
43
  sqliteDb: SqliteDb
44
44
  connectWebmeshNode: (args: {
45
45
  webmeshNode: Webmesh.MeshNode
@@ -133,4 +133,4 @@ export const makeClientSession = <R>({
133
133
  shutdown,
134
134
  debugInstanceId,
135
135
  } satisfies ClientSession
136
- })
136
+ }).pipe(Effect.withSpan('@livestore/common:make-client-session'))
@@ -1,26 +1,27 @@
1
- import { isNil, isReadonlyArray } from '@livestore/utils'
2
- import { Schema } from '@livestore/utils/effect'
1
+ import { isDevEnv, isNil, isReadonlyArray } from '@livestore/utils'
2
+ import { Hash, Option, Schema } from '@livestore/utils/effect'
3
3
 
4
4
  import type { SqliteDb } from './adapter-types.js'
5
5
  import { SessionIdSymbol } from './adapter-types.js'
6
6
  import type { EventDef, Materializer, MaterializerContextQuery, MaterializerResult } from './schema/EventDef.js'
7
7
  import type * as LiveStoreEvent from './schema/LiveStoreEvent.js'
8
+ import { getEventDef, type LiveStoreSchema } from './schema/schema.js'
8
9
  import type { QueryBuilder } from './schema/state/sqlite/query-builder/api.js'
9
10
  import { isQueryBuilder } from './schema/state/sqlite/query-builder/api.js'
10
11
  import { getResultSchema } from './schema/state/sqlite/query-builder/impl.js'
11
- import { type BindValues } from './sql-queries/sql-queries.js'
12
+ import type { BindValues } from './sql-queries/sql-queries.js'
12
13
  import type { ParamsObject, PreparedBindValues } from './util.js'
13
14
  import { prepareBindValues } from './util.js'
14
15
 
15
- export const getExecArgsFromEvent = ({
16
+ export const getExecStatementsFromMaterializer = ({
16
17
  eventDef,
17
18
  materializer,
18
- db,
19
+ dbState,
19
20
  event,
20
21
  }: {
21
22
  eventDef: EventDef.AnyWithoutFn
22
23
  materializer: Materializer
23
- db: SqliteDb
24
+ dbState: SqliteDb
24
25
  /** Both encoded and decoded events are supported to reduce the number of times we need to decode/encode */
25
26
  event:
26
27
  | {
@@ -53,25 +54,25 @@ export const getExecArgsFromEvent = ({
53
54
  ) => {
54
55
  if (isQueryBuilder(rawQueryOrQueryBuilder)) {
55
56
  const { query, bindValues } = rawQueryOrQueryBuilder.asSql()
56
- const rawResults = db.select(query, prepareBindValues(bindValues, query))
57
+ const rawResults = dbState.select(query, prepareBindValues(bindValues, query))
57
58
  const resultSchema = getResultSchema(rawQueryOrQueryBuilder)
58
59
  return Schema.decodeSync(resultSchema)(rawResults)
59
60
  } else {
60
61
  const { query, bindValues } = rawQueryOrQueryBuilder
61
- return db.select(query, prepareBindValues(bindValues, query))
62
+ return dbState.select(query, prepareBindValues(bindValues, query))
62
63
  }
63
64
  }
64
65
 
65
- const res = materializer(eventArgsDecoded, {
66
- eventDef,
67
- query,
68
- // TODO properly implement this
69
- currentFacts: new Map(),
70
- })
71
-
72
- const statementRes = mapMaterializerResult(res)
66
+ const statementResults = fromMaterializerResult(
67
+ materializer(eventArgsDecoded, {
68
+ eventDef,
69
+ query,
70
+ // TODO properly implement this
71
+ currentFacts: new Map(),
72
+ }),
73
+ )
73
74
 
74
- return statementRes.map((statementRes) => {
75
+ return statementResults.map((statementRes) => {
75
76
  const statementSql = statementRes.sql
76
77
 
77
78
  const bindValues = typeof statementRes === 'string' ? eventArgsEncoded : statementRes.bindValues
@@ -82,7 +83,32 @@ export const getExecArgsFromEvent = ({
82
83
  })
83
84
  }
84
85
 
85
- const mapMaterializerResult = (
86
+ export const makeMaterializerHash =
87
+ ({ schema, dbState }: { schema: LiveStoreSchema; dbState: SqliteDb }) =>
88
+ (event: LiveStoreEvent.AnyEncodedGlobal): Option.Option<number> => {
89
+ if (isDevEnv()) {
90
+ const { eventDef, materializer } = getEventDef(schema, event.name)
91
+ const materializerResults = getExecStatementsFromMaterializer({
92
+ eventDef,
93
+ materializer,
94
+ dbState,
95
+ event: { decoded: undefined, encoded: event },
96
+ })
97
+ return Option.some(Hash.string(JSON.stringify(materializerResults)))
98
+ }
99
+
100
+ return Option.none()
101
+ }
102
+
103
+ export const hashMaterializerResults = (
104
+ materializerResults: ReadonlyArray<{
105
+ statementSql: string
106
+ bindValues: PreparedBindValues
107
+ writeTables: ReadonlySet<string> | undefined
108
+ }>,
109
+ ) => Hash.string(JSON.stringify(materializerResults))
110
+
111
+ const fromMaterializerResult = (
86
112
  materializerResult: MaterializerResult | ReadonlyArray<MaterializerResult>,
87
113
  ): ReadonlyArray<{
88
114
  sql: string
@@ -90,7 +116,7 @@ const mapMaterializerResult = (
90
116
  writeTables: ReadonlySet<string> | undefined
91
117
  }> => {
92
118
  if (isReadonlyArray(materializerResult)) {
93
- return materializerResult.flatMap(mapMaterializerResult)
119
+ return materializerResult.flatMap(fromMaterializerResult)
94
120
  }
95
121
  if (isQueryBuilder(materializerResult)) {
96
122
  const { query, bindValues } = materializerResult.asSql()
@@ -27,13 +27,13 @@ export const rematerializeFromEventlog = ({
27
27
  `SELECT COUNT(*) AS count FROM ${SystemTables.EVENTLOG_META_TABLE}`,
28
28
  )[0]!.count
29
29
 
30
- const hashEvent = memoizeByRef((event: EventDef.AnyWithoutFn) => Schema.hash(event.schema))
30
+ const hashEventDef = memoizeByRef((event: EventDef.AnyWithoutFn) => Schema.hash(event.schema))
31
31
 
32
32
  const processEvent = (row: SystemTables.EventlogMetaRow) =>
33
33
  Effect.gen(function* () {
34
34
  const eventDef = getEventDef(schema, row.name)
35
35
 
36
- if (hashEvent(eventDef.eventDef) !== row.schemaHash) {
36
+ if (hashEventDef(eventDef.eventDef) !== row.schemaHash) {
37
37
  yield* Effect.logWarning(
38
38
  `Schema hash mismatch for event definition ${row.name}. Trying to materialize event anyway.`,
39
39
  )
@@ -56,8 +56,16 @@ This likely means the schema has changed in an incompatible way.
56
56
  )
57
57
 
58
58
  const eventEncoded = LiveStoreEvent.EncodedWithMeta.make({
59
- seqNum: { global: row.seqNumGlobal, client: row.seqNumClient },
60
- parentSeqNum: { global: row.parentSeqNumGlobal, client: row.parentSeqNumClient },
59
+ seqNum: {
60
+ global: row.seqNumGlobal,
61
+ client: row.seqNumClient,
62
+ rebaseGeneration: row.seqNumRebaseGeneration,
63
+ },
64
+ parentSeqNum: {
65
+ global: row.parentSeqNumGlobal,
66
+ client: row.parentSeqNumClient,
67
+ rebaseGeneration: row.parentSeqNumRebaseGeneration,
68
+ },
61
69
  name: row.name,
62
70
  args,
63
71
  clientId: row.clientId,
@@ -27,12 +27,20 @@ export type EventDef<TName extends string, TType, TEncoded = TType, TDerived ext
27
27
  derived: TDerived
28
28
  }
29
29
 
30
- /** Helper function to construct a partial mutation event */
31
- (args: TType): {
30
+ /** Helper function to construct a partial event */
31
+ (
32
+ args: TType,
33
+ ): {
32
34
  name: TName
33
35
  args: TType
34
36
  }
35
37
 
38
+ /** Helper function to construct a partial encoded event */
39
+ encoded: (args: TEncoded) => {
40
+ name: TName
41
+ args: TEncoded
42
+ }
43
+
36
44
  readonly Event: {
37
45
  name: TName
38
46
  args: TType
@@ -123,6 +131,10 @@ export const defineEvent = <TName extends string, TType, TEncoded = TType, TDeri
123
131
 
124
132
  Object.defineProperty(makePartialEvent, 'name', { value: name })
125
133
  Object.defineProperty(makePartialEvent, 'schema', { value: schema })
134
+ Object.defineProperty(makePartialEvent, 'encoded', {
135
+ value: (args: TEncoded) => ({ name: name, args }),
136
+ })
137
+
126
138
  Object.defineProperty(makePartialEvent, 'options', {
127
139
  value: {
128
140
  clientOnly: options?.clientOnly ?? false,
@@ -184,14 +196,14 @@ export type Materializer<TEventDef extends EventDef.AnyWithoutFn = EventDef.AnyW
184
196
  ) => SingleOrReadonlyArray<MaterializerResult>
185
197
 
186
198
  export const defineMaterializer = <TEventDef extends EventDef.AnyWithoutFn>(
187
- eventDef: TEventDef,
199
+ _eventDef: TEventDef,
188
200
  materializer: Materializer<TEventDef>,
189
201
  ): Materializer<TEventDef> => {
190
202
  return materializer
191
203
  }
192
204
 
193
205
  export const materializers = <TInputRecord extends Record<string, EventDef.AnyWithoutFn>>(
194
- eventDefRecord: TInputRecord,
206
+ _eventDefRecord: TInputRecord,
195
207
  handlers: {
196
208
  [TEventName in TInputRecord[keyof TInputRecord]['name'] as Extract<
197
209
  TInputRecord[keyof TInputRecord],
@@ -4,9 +4,126 @@ import { expect } from 'vitest'
4
4
  import { EventSequenceNumber } from './mod.js'
5
5
 
6
6
  Vitest.describe('EventSequenceNumber', () => {
7
- Vitest.test('nextPair', () => {
7
+ Vitest.test('nextPair (no rebase)', () => {
8
8
  const e_0_0 = EventSequenceNumber.make({ global: 0, client: 0 })
9
- expect(EventSequenceNumber.nextPair(e_0_0, false).seqNum).toStrictEqual({ global: 1, client: 0 })
10
- expect(EventSequenceNumber.nextPair(e_0_0, true).seqNum).toStrictEqual({ global: 0, client: 1 })
9
+ expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: false }).seqNum).toStrictEqual({
10
+ global: 1,
11
+ client: 0,
12
+ rebaseGeneration: 0,
13
+ })
14
+ expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: true }).seqNum).toStrictEqual({
15
+ global: 0,
16
+ client: 1,
17
+ rebaseGeneration: 0,
18
+ })
19
+ })
20
+
21
+ Vitest.test('nextPair (rebase)', () => {
22
+ const e_0_0 = EventSequenceNumber.make({ global: 0, client: 0 })
23
+ expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: false, rebaseGeneration: 1 }).seqNum).toStrictEqual({
24
+ global: 1,
25
+ client: 0,
26
+ rebaseGeneration: 1,
27
+ })
28
+ expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: true, rebaseGeneration: 1 }).seqNum).toStrictEqual({
29
+ global: 0,
30
+ client: 1,
31
+ rebaseGeneration: 1,
32
+ })
33
+
34
+ const e_0_0_g1 = EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 2 })
35
+ expect(EventSequenceNumber.nextPair({ seqNum: e_0_0_g1, isClient: false }).seqNum).toStrictEqual({
36
+ global: 1,
37
+ client: 0,
38
+ rebaseGeneration: 2,
39
+ })
40
+ })
41
+
42
+ Vitest.test('toString', () => {
43
+ expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 0 }))).toBe('e0')
44
+ expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 1 }))).toBe(
45
+ 'e0r1',
46
+ )
47
+ expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 1 }))).toBe('e0+1')
48
+ expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 1 }))).toBe(
49
+ 'e0+1r1',
50
+ )
51
+ expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 5, client: 3, rebaseGeneration: 2 }))).toBe(
52
+ 'e5+3r2',
53
+ )
54
+ })
55
+
56
+ Vitest.test('fromString', () => {
57
+ // Basic cases
58
+ expect(EventSequenceNumber.fromString('e0')).toStrictEqual(EventSequenceNumber.make({ global: 0, client: 0 }))
59
+ expect(EventSequenceNumber.fromString('e0r1')).toStrictEqual(
60
+ EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 1 }),
61
+ )
62
+ expect(EventSequenceNumber.fromString('e0+1')).toStrictEqual(EventSequenceNumber.make({ global: 0, client: 1 }))
63
+ expect(EventSequenceNumber.fromString('e0+1r1')).toStrictEqual(
64
+ EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 1 }),
65
+ )
66
+ expect(EventSequenceNumber.fromString('e5+3r2')).toStrictEqual(
67
+ EventSequenceNumber.make({ global: 5, client: 3, rebaseGeneration: 2 }),
68
+ )
69
+
70
+ // Error cases
71
+ expect(() => EventSequenceNumber.fromString('0')).toThrow(
72
+ 'Invalid event sequence number string: must start with "e"',
73
+ )
74
+ expect(() => EventSequenceNumber.fromString('eabc')).toThrow(
75
+ 'Invalid event sequence number string: invalid number format',
76
+ )
77
+ expect(() => EventSequenceNumber.fromString('e0+abc')).toThrow(
78
+ 'Invalid event sequence number string: invalid number format',
79
+ )
80
+ expect(() => EventSequenceNumber.fromString('e0rabc')).toThrow(
81
+ 'Invalid event sequence number string: invalid number format',
82
+ )
83
+ })
84
+
85
+ Vitest.test('toString/fromString roundtrip', () => {
86
+ const testCases = [
87
+ { global: 0, client: 0, rebaseGeneration: 0 },
88
+ { global: 0, client: 0, rebaseGeneration: 1 },
89
+ { global: 0, client: 1, rebaseGeneration: 0 },
90
+ { global: 0, client: 1, rebaseGeneration: 1 },
91
+ { global: 5, client: 3, rebaseGeneration: 2 },
92
+ { global: 100, client: 50, rebaseGeneration: 10 },
93
+ ]
94
+
95
+ for (const testCase of testCases) {
96
+ const original = EventSequenceNumber.make(testCase)
97
+ const str = EventSequenceNumber.toString(original)
98
+ const parsed = EventSequenceNumber.fromString(str)
99
+ expect(parsed).toStrictEqual(original)
100
+ }
101
+ })
102
+
103
+ Vitest.test('compare', () => {
104
+ const e_0_0_r0 = EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 0 })
105
+ const e_0_0_r1 = EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 1 })
106
+ const e_0_1_r0 = EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 0 })
107
+ const e_0_1_r1 = EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 1 })
108
+ const e_1_0_r0 = EventSequenceNumber.make({ global: 1, client: 0, rebaseGeneration: 0 })
109
+ const e_1_1_r0 = EventSequenceNumber.make({ global: 1, client: 1, rebaseGeneration: 0 })
110
+
111
+ // Global comparison (strongest level)
112
+ expect(EventSequenceNumber.compare(e_0_0_r0, e_1_0_r0)).toBeLessThan(0)
113
+ expect(EventSequenceNumber.compare(e_1_0_r0, e_0_0_r0)).toBeGreaterThan(0)
114
+ expect(EventSequenceNumber.compare(e_0_1_r1, e_1_0_r0)).toBeLessThan(0) // global overrides client and rebase
115
+
116
+ // Client comparison (second level)
117
+ expect(EventSequenceNumber.compare(e_0_0_r0, e_0_1_r0)).toBeLessThan(0)
118
+ expect(EventSequenceNumber.compare(e_0_1_r0, e_0_0_r0)).toBeGreaterThan(0)
119
+ expect(EventSequenceNumber.compare(e_0_0_r1, e_0_1_r0)).toBeLessThan(0) // client overrides rebase
120
+
121
+ // Rebase generation comparison (weakest level)
122
+ expect(EventSequenceNumber.compare(e_0_0_r0, e_0_0_r1)).toBeLessThan(0)
123
+ expect(EventSequenceNumber.compare(e_0_0_r1, e_0_0_r0)).toBeGreaterThan(0)
124
+
125
+ // Equal comparison
126
+ expect(EventSequenceNumber.compare(e_0_0_r0, e_0_0_r0)).toBe(0)
127
+ expect(EventSequenceNumber.compare(e_1_1_r0, e_1_1_r0)).toBe(0)
11
128
  })
12
129
  })
@@ -10,6 +10,8 @@ export const GlobalEventSequenceNumber = Schema.fromBrand(globalEventSequenceNum
10
10
 
11
11
  export const clientDefault = 0 as any as ClientEventSequenceNumber
12
12
 
13
+ export const rebaseGenerationDefault = 0
14
+
13
15
  /**
14
16
  * LiveStore event sequence number value consisting of a globally unique event sequence number
15
17
  * and a client sequence number.
@@ -20,16 +22,17 @@ export type EventSequenceNumber = {
20
22
  global: GlobalEventSequenceNumber
21
23
  client: ClientEventSequenceNumber
22
24
  /**
23
- * TODO add generation number in favour of LEADER_MERGE_COUNTER_TABLE
25
+ * Generation integer that is incremented whenever the client rebased.
26
+ * Starts from and resets to 0 for each global sequence number.
24
27
  */
25
- // generation: number
28
+ rebaseGeneration: number
26
29
  }
27
30
 
28
- // export const EventSequenceNumber = Schema.Struct({})
29
- // export const EventSequenceNumber = Schema.Struct({})
30
- // export const ClientEventSequenceNumber = Schema.Struct({})
31
- // export const GlobalEventSequenceNumber = Schema.Struct({})
31
+ export type EventSequenceNumberInput =
32
+ | EventSequenceNumber
33
+ | (Omit<typeof EventSequenceNumber.Encoded, 'rebaseGeneration'> & { rebaseGeneration?: number })
32
34
 
35
+ // TODO adjust name to `ClientEventSequenceNumber`
33
36
  /**
34
37
  * NOTE: Client mutation events with a non-0 client id, won't be synced to the sync backend.
35
38
  */
@@ -40,36 +43,83 @@ export const EventSequenceNumber = Schema.Struct({
40
43
 
41
44
  // TODO also provide a way to see "confirmation level" of event (e.g. confirmed by leader/sync backend)
42
45
 
43
- // TODO: actually add this field
44
46
  // Client only
45
- // generation: Schema.Number.pipe(Schema.optional),
46
- }).annotations({ title: 'LiveStore.EventSequenceNumber' })
47
+ rebaseGeneration: Schema.Int,
48
+ }).annotations({
49
+ title: 'LiveStore.EventSequenceNumber',
50
+ pretty: () => (seqNum) => toString(seqNum),
51
+ })
47
52
 
48
53
  /**
49
54
  * Compare two event sequence numbers i.e. checks if the first event sequence number is less than the second.
55
+ * Comparison hierarchy: global > client > rebaseGeneration
50
56
  */
51
57
  export const compare = (a: EventSequenceNumber, b: EventSequenceNumber) => {
52
58
  if (a.global !== b.global) {
53
59
  return a.global - b.global
54
60
  }
55
- return a.client - b.client
61
+ if (a.client !== b.client) {
62
+ return a.client - b.client
63
+ }
64
+ return a.rebaseGeneration - b.rebaseGeneration
56
65
  }
57
66
 
58
67
  /**
59
68
  * Convert an event sequence number to a string representation.
60
69
  */
61
- export const toString = (seqNum: EventSequenceNumber) =>
62
- seqNum.client === 0 ? `e${seqNum.global}` : `e${seqNum.global}+${seqNum.client}`
70
+ export const toString = (seqNum: EventSequenceNumber) => {
71
+ const rebaseGenerationStr = seqNum.rebaseGeneration > 0 ? `r${seqNum.rebaseGeneration}` : ''
72
+ return seqNum.client === 0
73
+ ? `e${seqNum.global}${rebaseGenerationStr}`
74
+ : `e${seqNum.global}+${seqNum.client}${rebaseGenerationStr}`
75
+ }
63
76
 
64
77
  /**
65
78
  * Convert a string representation of an event sequence number to an event sequence number.
79
+ * Parses strings in the format: e{global}[+{client}][r{rebaseGeneration}]
80
+ * Examples: "e0", "e0r1", "e0+1", "e0+1r1"
66
81
  */
67
82
  export const fromString = (str: string): EventSequenceNumber => {
68
- const [global, client] = str.slice(1, -1).split(',').map(Number)
69
- if (global === undefined || client === undefined) {
70
- throw new Error('Invalid event sequence number string')
83
+ if (!str.startsWith('e')) {
84
+ throw new Error('Invalid event sequence number string: must start with "e"')
85
+ }
86
+
87
+ // Remove the 'e' prefix
88
+ const remaining = str.slice(1)
89
+
90
+ // Parse rebase generation if present
91
+ let rebaseGeneration = rebaseGenerationDefault
92
+ let withoutRebase = remaining
93
+ const rebaseMatch = remaining.match(/r(\d+)$/)
94
+ if (rebaseMatch !== null) {
95
+ rebaseGeneration = Number.parseInt(rebaseMatch[1]!, 10)
96
+ withoutRebase = remaining.slice(0, -rebaseMatch[0].length)
97
+ }
98
+
99
+ // Parse global and client parts
100
+ const parts = withoutRebase.split('+')
101
+
102
+ // Validate that parts contain only digits (and possibly empty for client)
103
+ if (parts[0] === '' || !/^\d+$/.test(parts[0]!)) {
104
+ throw new Error('Invalid event sequence number string: invalid number format')
105
+ }
106
+
107
+ if (parts.length > 1 && parts[1] !== undefined && (parts[1] === '' || !/^\d+$/.test(parts[1]))) {
108
+ throw new Error('Invalid event sequence number string: invalid number format')
109
+ }
110
+
111
+ const global = Number.parseInt(parts[0]!, 10)
112
+ const client = parts.length > 1 && parts[1] !== undefined ? Number.parseInt(parts[1], 10) : 0
113
+
114
+ if (Number.isNaN(global) || Number.isNaN(client) || Number.isNaN(rebaseGeneration)) {
115
+ throw new TypeError('Invalid event sequence number string: invalid number format')
116
+ }
117
+
118
+ return {
119
+ global: global as any as GlobalEventSequenceNumber,
120
+ client: client as any as ClientEventSequenceNumber,
121
+ rebaseGeneration,
71
122
  }
72
- return { global, client } as EventSequenceNumber
73
123
  }
74
124
 
75
125
  export const isEqual = (a: EventSequenceNumber, b: EventSequenceNumber) =>
@@ -80,6 +130,7 @@ export type EventSequenceNumberPair = { seqNum: EventSequenceNumber; parentSeqNu
80
130
  export const ROOT = {
81
131
  global: 0 as any as GlobalEventSequenceNumber,
82
132
  client: clientDefault,
133
+ rebaseGeneration: rebaseGenerationDefault,
83
134
  } satisfies EventSequenceNumber
84
135
 
85
136
  export const isGreaterThan = (a: EventSequenceNumber, b: EventSequenceNumber) => {
@@ -101,21 +152,42 @@ export const diff = (a: EventSequenceNumber, b: EventSequenceNumber) => {
101
152
  }
102
153
  }
103
154
 
104
- export const make = (seqNum: EventSequenceNumber | typeof EventSequenceNumber.Encoded): EventSequenceNumber => {
105
- return Schema.is(EventSequenceNumber)(seqNum) ? seqNum : Schema.decodeSync(EventSequenceNumber)(seqNum)
155
+ export const make = (seqNum: EventSequenceNumberInput): EventSequenceNumber => {
156
+ return Schema.is(EventSequenceNumber)(seqNum)
157
+ ? seqNum
158
+ : Schema.decodeSync(EventSequenceNumber)({
159
+ ...seqNum,
160
+ rebaseGeneration: seqNum.rebaseGeneration ?? rebaseGenerationDefault,
161
+ })
106
162
  }
107
163
 
108
- export const nextPair = (seqNum: EventSequenceNumber, isLocal: boolean): EventSequenceNumberPair => {
109
- if (isLocal) {
164
+ export const nextPair = ({
165
+ seqNum,
166
+ isClient,
167
+ rebaseGeneration,
168
+ }: {
169
+ seqNum: EventSequenceNumber
170
+ isClient: boolean
171
+ rebaseGeneration?: number
172
+ }): EventSequenceNumberPair => {
173
+ if (isClient) {
110
174
  return {
111
- seqNum: { global: seqNum.global, client: (seqNum.client + 1) as any as ClientEventSequenceNumber },
175
+ seqNum: {
176
+ global: seqNum.global,
177
+ client: (seqNum.client + 1) as any as ClientEventSequenceNumber,
178
+ rebaseGeneration: rebaseGeneration ?? seqNum.rebaseGeneration,
179
+ },
112
180
  parentSeqNum: seqNum,
113
181
  }
114
182
  }
115
183
 
116
184
  return {
117
- seqNum: { global: (seqNum.global + 1) as any as GlobalEventSequenceNumber, client: clientDefault },
185
+ seqNum: {
186
+ global: (seqNum.global + 1) as any as GlobalEventSequenceNumber,
187
+ client: clientDefault,
188
+ rebaseGeneration: rebaseGeneration ?? seqNum.rebaseGeneration,
189
+ },
118
190
  // NOTE we always point to `client: 0` for non-clientOnly events
119
- parentSeqNum: { global: seqNum.global, client: clientDefault },
191
+ parentSeqNum: { global: seqNum.global, client: clientDefault, rebaseGeneration: seqNum.rebaseGeneration },
120
192
  }
121
193
  }
@@ -171,12 +171,25 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('LiveStoreEve
171
171
  Schema.TaggedStruct('unset', {}),
172
172
  ),
173
173
  syncMetadata: Schema.Option(Schema.JsonValue),
174
+ /** Used to detect if the materializer is side effecting (during dev) */
175
+ materializerHashLeader: Schema.Option(Schema.Number),
176
+ materializerHashSession: Schema.Option(Schema.Number),
174
177
  }).pipe(
175
178
  Schema.mutable,
176
179
  Schema.optional,
177
180
  Schema.withDefaults({
178
- constructor: () => ({ sessionChangeset: { _tag: 'unset' as const }, syncMetadata: Option.none() }),
179
- decoding: () => ({ sessionChangeset: { _tag: 'unset' as const }, syncMetadata: Option.none() }),
181
+ constructor: () => ({
182
+ sessionChangeset: { _tag: 'unset' as const },
183
+ syncMetadata: Option.none(),
184
+ materializerHashLeader: Option.none(),
185
+ materializerHashSession: Option.none(),
186
+ }),
187
+ decoding: () => ({
188
+ sessionChangeset: { _tag: 'unset' as const },
189
+ syncMetadata: Option.none(),
190
+ materializerHashLeader: Option.none(),
191
+ materializerHashSession: Option.none(),
192
+ }),
180
193
  }),
181
194
  ),
182
195
  }) {
@@ -208,18 +221,46 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('LiveStoreEve
208
221
  * +---- global number
209
222
  * Client num is ommitted for global events
210
223
  */
211
- rebase = (parentSeqNum: EventSequenceNumber.EventSequenceNumber, isClient: boolean) =>
224
+ rebase = ({
225
+ parentSeqNum,
226
+ isClient,
227
+ rebaseGeneration,
228
+ }: {
229
+ parentSeqNum: EventSequenceNumber.EventSequenceNumber
230
+ isClient: boolean
231
+ rebaseGeneration: number
232
+ }) =>
212
233
  new EncodedWithMeta({
213
234
  ...this,
214
- ...EventSequenceNumber.nextPair(parentSeqNum, isClient),
235
+ ...EventSequenceNumber.nextPair({ seqNum: parentSeqNum, isClient, rebaseGeneration }),
215
236
  })
216
237
 
217
- static fromGlobal = (event: AnyEncodedGlobal, syncMetadata: Option.Option<Schema.JsonValue>) =>
238
+ static fromGlobal = (
239
+ event: AnyEncodedGlobal,
240
+ meta: {
241
+ syncMetadata: Option.Option<Schema.JsonValue>
242
+ materializerHashLeader: Option.Option<number>
243
+ materializerHashSession: Option.Option<number>
244
+ },
245
+ ) =>
218
246
  new EncodedWithMeta({
219
247
  ...event,
220
- seqNum: { global: event.seqNum, client: EventSequenceNumber.clientDefault },
221
- parentSeqNum: { global: event.parentSeqNum, client: EventSequenceNumber.clientDefault },
222
- meta: { sessionChangeset: { _tag: 'unset' as const }, syncMetadata },
248
+ seqNum: {
249
+ global: event.seqNum,
250
+ client: EventSequenceNumber.clientDefault,
251
+ rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
252
+ },
253
+ parentSeqNum: {
254
+ global: event.parentSeqNum,
255
+ client: EventSequenceNumber.clientDefault,
256
+ rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
257
+ },
258
+ meta: {
259
+ sessionChangeset: { _tag: 'unset' as const },
260
+ syncMetadata: meta.syncMetadata,
261
+ materializerHashLeader: meta.materializerHashLeader,
262
+ materializerHashSession: meta.materializerHashSession,
263
+ },
223
264
  })
224
265
 
225
266
  toGlobal = (): AnyEncodedGlobal => ({