@livestore/common 0.3.0-dev.4 → 0.3.0-dev.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +26 -23
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js.map +1 -1
  5. package/dist/derived-mutations.d.ts +4 -4
  6. package/dist/derived-mutations.d.ts.map +1 -1
  7. package/dist/derived-mutations.test.js.map +1 -1
  8. package/dist/devtools/devtools-bridge.d.ts +2 -1
  9. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  10. package/dist/devtools/devtools-messages.d.ts +90 -102
  11. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  12. package/dist/devtools/devtools-messages.js +9 -6
  13. package/dist/devtools/devtools-messages.js.map +1 -1
  14. package/dist/leader-thread/apply-mutation.d.ts +5 -2
  15. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  16. package/dist/leader-thread/apply-mutation.js +37 -25
  17. package/dist/leader-thread/apply-mutation.js.map +1 -1
  18. package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -1
  19. package/dist/leader-thread/leader-sync-processor.js +20 -12
  20. package/dist/leader-thread/leader-sync-processor.js.map +1 -1
  21. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  22. package/dist/leader-thread/leader-worker-devtools.js +22 -66
  23. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  24. package/dist/leader-thread/make-leader-thread-layer.d.ts +4 -2
  25. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  26. package/dist/leader-thread/make-leader-thread-layer.js +5 -2
  27. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  28. package/dist/leader-thread/mutationlog.d.ts +4 -17
  29. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  30. package/dist/leader-thread/mutationlog.js +2 -1
  31. package/dist/leader-thread/mutationlog.js.map +1 -1
  32. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  33. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  34. package/dist/leader-thread/recreate-db.js +9 -3
  35. package/dist/leader-thread/recreate-db.js.map +1 -1
  36. package/dist/leader-thread/types.d.ts +7 -5
  37. package/dist/leader-thread/types.d.ts.map +1 -1
  38. package/dist/leader-thread/types.js.map +1 -1
  39. package/dist/mutation.d.ts +9 -2
  40. package/dist/mutation.d.ts.map +1 -1
  41. package/dist/mutation.js +5 -5
  42. package/dist/mutation.js.map +1 -1
  43. package/dist/query-builder/impl.d.ts +1 -1
  44. package/dist/rehydrate-from-mutationlog.d.ts +2 -2
  45. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  46. package/dist/rehydrate-from-mutationlog.js +13 -19
  47. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  48. package/dist/schema/EventId.d.ts +16 -14
  49. package/dist/schema/EventId.d.ts.map +1 -1
  50. package/dist/schema/EventId.js +15 -7
  51. package/dist/schema/EventId.js.map +1 -1
  52. package/dist/schema/MutationEvent.d.ts +49 -74
  53. package/dist/schema/MutationEvent.d.ts.map +1 -1
  54. package/dist/schema/MutationEvent.js +29 -13
  55. package/dist/schema/MutationEvent.js.map +1 -1
  56. package/dist/schema/system-tables.d.ts +26 -26
  57. package/dist/schema/system-tables.d.ts.map +1 -1
  58. package/dist/schema/system-tables.js +19 -11
  59. package/dist/schema/system-tables.js.map +1 -1
  60. package/dist/schema-management/migrations.js +6 -6
  61. package/dist/schema-management/migrations.js.map +1 -1
  62. package/dist/sync/client-session-sync-processor.d.ts +4 -4
  63. package/dist/sync/client-session-sync-processor.d.ts.map +1 -1
  64. package/dist/sync/next/history-dag-common.d.ts +1 -4
  65. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  66. package/dist/sync/next/history-dag-common.js +1 -1
  67. package/dist/sync/next/history-dag-common.js.map +1 -1
  68. package/dist/sync/next/rebase-events.d.ts +3 -3
  69. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  70. package/dist/sync/next/rebase-events.js +3 -2
  71. package/dist/sync/next/rebase-events.js.map +1 -1
  72. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  73. package/dist/sync/next/test/mutation-fixtures.js +3 -9
  74. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  75. package/dist/sync/sync.d.ts +6 -6
  76. package/dist/sync/sync.d.ts.map +1 -1
  77. package/dist/sync/sync.js.map +1 -1
  78. package/dist/sync/syncstate.d.ts +10 -10
  79. package/dist/sync/syncstate.test.js +2 -6
  80. package/dist/sync/syncstate.test.js.map +1 -1
  81. package/dist/sync/validate-push-payload.d.ts +2 -2
  82. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  83. package/dist/sync/validate-push-payload.js +2 -2
  84. package/dist/sync/validate-push-payload.js.map +1 -1
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/package.json +6 -5
  88. package/src/adapter-types.ts +22 -24
  89. package/src/derived-mutations.test.ts +1 -1
  90. package/src/derived-mutations.ts +9 -5
  91. package/src/devtools/devtools-bridge.ts +2 -1
  92. package/src/devtools/devtools-messages.ts +9 -6
  93. package/src/leader-thread/apply-mutation.ts +48 -30
  94. package/src/leader-thread/leader-sync-processor.ts +26 -15
  95. package/src/leader-thread/leader-worker-devtools.ts +30 -109
  96. package/src/leader-thread/make-leader-thread-layer.ts +15 -5
  97. package/src/leader-thread/mutationlog.ts +9 -5
  98. package/src/leader-thread/recreate-db.ts +9 -5
  99. package/src/leader-thread/types.ts +7 -8
  100. package/src/mutation.ts +17 -7
  101. package/src/rehydrate-from-mutationlog.ts +15 -23
  102. package/src/schema/EventId.ts +23 -9
  103. package/src/schema/MutationEvent.ts +40 -20
  104. package/src/schema/system-tables.ts +19 -11
  105. package/src/schema-management/migrations.ts +6 -6
  106. package/src/sync/client-session-sync-processor.ts +4 -4
  107. package/src/sync/next/history-dag-common.ts +1 -1
  108. package/src/sync/next/rebase-events.ts +7 -7
  109. package/src/sync/next/test/mutation-fixtures.ts +3 -10
  110. package/src/sync/sync.ts +4 -2
  111. package/src/sync/syncstate.test.ts +4 -4
  112. package/src/sync/validate-push-payload.ts +7 -4
  113. package/src/version.ts +1 -1
@@ -29,8 +29,28 @@ export type SynchronousDatabaseChangeset = {
29
29
  export type ClientSession = {
30
30
  /** SQLite database with synchronous API running in the same thread (usually in-memory) */
31
31
  syncDb: SynchronousDatabase
32
- /** The coordinator is responsible for persisting the database, syncing etc */
33
- coordinator: Coordinator
32
+ devtools: { enabled: boolean }
33
+ clientId: string
34
+ sessionId: string
35
+ /** Status info whether current session is leader or not */
36
+ lockStatus: SubscriptionRef.SubscriptionRef<LockStatus>
37
+ shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => Effect.Effect<void>
38
+ /** A proxy API to communicate with the leader thread */
39
+ leaderThread: ClientSessionLeaderThreadProxy
40
+ }
41
+
42
+ export type ClientSessionLeaderThreadProxy = {
43
+ mutations: {
44
+ pull: Stream.Stream<{ payload: PayloadUpstream; remaining: number }, UnexpectedError>
45
+ push(batch: ReadonlyArray<MutationEvent.AnyEncoded>): Effect.Effect<void, UnexpectedError | InvalidPushError>
46
+ initialMutationEventId: EventId
47
+ }
48
+ export: Effect.Effect<Uint8Array, UnexpectedError>
49
+ getMutationLogData: Effect.Effect<Uint8Array, UnexpectedError>
50
+ getSyncState: Effect.Effect<SyncState, UnexpectedError>
51
+ networkStatus: SubscriptionRef.SubscriptionRef<NetworkStatus>
52
+ /** For debugging purposes it can be useful to manually trigger devtools messages (e.g. to reset the database) */
53
+ sendDevtoolsMessage: (message: Devtools.MessageToAppLeader) => Effect.Effect<void, UnexpectedError>
34
54
  }
35
55
 
36
56
  export type SynchronousDatabase<TReq = any, TMetadata extends TReq = TReq> = {
@@ -103,28 +123,6 @@ export const BootStatus = Schema.Union(
103
123
 
104
124
  export type BootStatus = typeof BootStatus.Type
105
125
 
106
- // TODO refactor `Coordinator` to embrace more of the "leader semantics"
107
- export type Coordinator = {
108
- devtools: {
109
- enabled: boolean
110
- // TODO incorporate sessionId and rethink appHostId
111
- appHostId: string
112
- }
113
- sessionId: string
114
- // TODO is exposing the lock status really needed (or only relevant for web adapter?)
115
- lockStatus: SubscriptionRef.SubscriptionRef<LockStatus>
116
- mutations: {
117
- pull: Stream.Stream<{ payload: PayloadUpstream; remaining: number }, UnexpectedError>
118
- push(batch: ReadonlyArray<MutationEvent.AnyEncoded>): Effect.Effect<void, UnexpectedError | InvalidPushError>
119
- initialMutationEventId: EventId
120
- }
121
- export: Effect.Effect<Uint8Array, UnexpectedError>
122
- getMutationLogData: Effect.Effect<Uint8Array, UnexpectedError>
123
- getLeaderSyncState: Effect.Effect<SyncState, UnexpectedError>
124
- networkStatus: SubscriptionRef.SubscriptionRef<NetworkStatus>
125
- shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => Effect.Effect<void>
126
- }
127
-
128
126
  /**
129
127
  * Can be used in queries to refer to the current session id.
130
128
  * Will be replaced with the actual session id at runtime
@@ -94,7 +94,7 @@ describe('derived mutations', () => {
94
94
  })
95
95
  })
96
96
 
97
- const patchId = (muationEvent: MutationEvent.PartialAny) => {
97
+ const patchId = (muationEvent: MutationEvent.PartialAnyDecoded) => {
98
98
  // TODO use new id paradigm
99
99
  const id = `00000000-0000-0000-0000-000000000000`
100
100
  return { ...muationEvent, id }
@@ -136,8 +136,10 @@ export namespace DerivedMutationHelperFns {
136
136
  > = SqliteDsl.AnyIfConstained<
137
137
  TColumns,
138
138
  UseShortcut<TOptions> extends true
139
- ? (values?: GetValForKey<SqliteDsl.FromColumns.InsertRowDecoded<TColumns>, 'value'>) => MutationEvent.PartialAny
140
- : (values: SqliteDsl.FromColumns.InsertRowDecoded<TColumns>) => MutationEvent.PartialAny
139
+ ? (
140
+ values?: GetValForKey<SqliteDsl.FromColumns.InsertRowDecoded<TColumns>, 'value'>,
141
+ ) => MutationEvent.PartialAnyDecoded
142
+ : (values: SqliteDsl.FromColumns.InsertRowDecoded<TColumns>) => MutationEvent.PartialAnyDecoded
141
143
  >
142
144
 
143
145
  export type UpdateMutationFn<
@@ -146,17 +148,19 @@ export namespace DerivedMutationHelperFns {
146
148
  > = SqliteDsl.AnyIfConstained<
147
149
  TColumns,
148
150
  UseShortcut<TOptions> extends true
149
- ? (values: Partial<GetValForKey<SqliteDsl.FromColumns.RowDecoded<TColumns>, 'value'>>) => MutationEvent.PartialAny
151
+ ? (
152
+ values: Partial<GetValForKey<SqliteDsl.FromColumns.RowDecoded<TColumns>, 'value'>>,
153
+ ) => MutationEvent.PartialAnyDecoded
150
154
  : (args: {
151
155
  where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
152
156
  values: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
153
- }) => MutationEvent.PartialAny
157
+ }) => MutationEvent.PartialAnyDecoded
154
158
  >
155
159
 
156
160
  export type DeleteMutationFn<
157
161
  TColumns extends SqliteDsl.ConstraintColumns,
158
162
  _TOptions extends DbSchema.TableOptions,
159
- > = (args: { where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>> }) => MutationEvent.PartialAny
163
+ > = (args: { where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>> }) => MutationEvent.PartialAnyDecoded
160
164
 
161
165
  type UseShortcut<TOptions extends DbSchema.TableOptions> = TOptions['isSingleColumn'] extends true
162
166
  ? TOptions['isSingleton'] extends true
@@ -6,7 +6,8 @@ export type PrepareDevtoolsBridge = {
6
6
  /** Messages coming from the app host (usually responses to requests) */
7
7
  responsePubSub: PubSub.PubSub<Devtools.MessageFromAppLeader | Devtools.MessageFromAppClientSession>
8
8
  sendToAppHost: (msg: Devtools.MessageToAppLeader | Devtools.MessageToAppClientSession) => Effect.Effect<void>
9
- appHostId: string
9
+ clientId: string
10
+ sessionId: string
10
11
  copyToClipboard: (text: string) => Effect.Effect<void>
11
12
  sendEscapeKey?: Effect.Effect<void>
12
13
  isLeader: boolean
@@ -7,7 +7,8 @@ import { PreparedBindValues } from '../util.js'
7
7
  import { liveStoreVersion as pkgVersion } from '../version.js'
8
8
 
9
9
  const requestId = Schema.String
10
- const appHostId = Schema.String
10
+ const clientId = Schema.String
11
+ const sessionId = Schema.String
11
12
  const liveStoreVersion = Schema.Literal(pkgVersion)
12
13
 
13
14
  const LSDMessage = <Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) =>
@@ -23,13 +24,15 @@ const LSDChannelMessage = <Tag extends string, Fields extends Schema.Struct.Fiel
23
24
 
24
25
  const LSDStoreChannelMessage = <Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) =>
25
26
  LSDMessage(tag, {
26
- appHostId,
27
+ clientId,
28
+ sessionId,
27
29
  ...fields,
28
30
  })
29
31
 
30
32
  const LSDStoreReqResMessage = <Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) =>
31
33
  LSDMessage(tag, {
32
- appHostId,
34
+ clientId,
35
+ sessionId,
33
36
  requestId,
34
37
  ...fields,
35
38
  })
@@ -82,12 +85,12 @@ export class DebugInfoRerunQueryRes extends LSDStoreReqResMessage('LSD.DebugInfo
82
85
 
83
86
  // TODO refactor this to use push/pull semantics
84
87
  export class MutationBroadcast extends LSDMessage('LSD.Leader.MutationBroadcast', {
85
- mutationEventEncoded: MutationEvent.EncodedAny,
88
+ mutationEventEncoded: MutationEvent.AnyEncoded,
86
89
  }) {}
87
90
 
88
91
  // TODO refactor this to use push/pull semantics
89
92
  export class RunMutationReq extends LSDReqResMessage('LSD.Leader.RunMutationReq', {
90
- mutationEventEncoded: MutationEvent.EncodedAny.pipe(Schema.omit('id', 'parentId')),
93
+ mutationEventEncoded: MutationEvent.AnyEncoded.pipe(Schema.omit('id', 'parentId')),
91
94
  }) {}
92
95
 
93
96
  export class RunMutationRes extends LSDReqResMessage('LSD.Leader.RunMutationRes', {}) {}
@@ -167,7 +170,7 @@ export class SyncingInfoRes extends LSDReqResMessage('LSD.Leader.SyncingInfoRes'
167
170
  export class SyncHistorySubscribe extends LSDReqResMessage('LSD.Leader.SyncHistorySubscribe', {}) {}
168
171
  export class SyncHistoryUnsubscribe extends LSDReqResMessage('LSD.Leader.SyncHistoryUnsubscribe', {}) {}
169
172
  export class SyncHistoryRes extends LSDReqResMessage('LSD.Leader.SyncHistoryRes', {
170
- mutationEventEncoded: MutationEvent.EncodedAny,
173
+ mutationEventEncoded: MutationEvent.AnyEncodedGlobal,
171
174
  metadata: Schema.Option(Schema.JsonValue),
172
175
  }) {}
173
176
 
@@ -3,20 +3,25 @@ import type { Scope } from '@livestore/utils/effect'
3
3
  import { Effect, Option, Schema } from '@livestore/utils/effect'
4
4
 
5
5
  import type { SqliteError, SynchronousDatabase, UnexpectedError } from '../index.js'
6
+ import { getExecArgsFromMutation } from '../mutation.js'
6
7
  import {
7
- getExecArgsFromMutation,
8
+ type LiveStoreSchema,
8
9
  MUTATION_LOG_META_TABLE,
10
+ type MutationEvent,
9
11
  mutationLogMetaTable,
10
12
  SESSION_CHANGESET_META_TABLE,
11
13
  sessionChangesetMetaTable,
12
- } from '../index.js'
13
- import type { LiveStoreSchema, MutationEvent } from '../schema/mod.js'
14
+ } from '../schema/mod.js'
14
15
  import { insertRow } from '../sql-queries/index.js'
15
16
  import { execSql, execSqlPrepared } from './connection.js'
16
17
  import { LeaderThreadCtx } from './types.js'
17
18
 
18
19
  export type ApplyMutation = (
19
20
  mutationEventEncoded: MutationEvent.AnyEncoded,
21
+ options?: {
22
+ /** Needed for rehydrateFromMutationLog */
23
+ skipMutationLog?: boolean
24
+ },
20
25
  ) => Effect.Effect<void, SqliteError | UnexpectedError>
21
26
 
22
27
  export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope | LeaderThreadCtx> = Effect.gen(
@@ -31,15 +36,27 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
31
36
  [...leaderThreadCtx.schema.mutations.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
32
37
  )
33
38
 
34
- return (mutationEventEncoded) =>
39
+ return (mutationEventEncoded, options) =>
35
40
  Effect.gen(function* () {
36
- const { mutationEventSchema, schema, db, dbLog } = leaderThreadCtx
37
- const mutationEventDecoded = Schema.decodeUnknownSync(mutationEventSchema)(mutationEventEncoded)
41
+ const { schema, db, dbLog } = leaderThreadCtx
42
+ const skipMutationLog = options?.skipMutationLog ?? false
38
43
 
39
- const mutationName = mutationEventDecoded.mutation
44
+ const mutationName = mutationEventEncoded.mutation
40
45
  const mutationDef = schema.mutations.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
41
46
 
42
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
47
+ const execArgsArr = getExecArgsFromMutation({
48
+ mutationDef,
49
+ mutationEvent: { decoded: undefined, encoded: mutationEventEncoded },
50
+ })
51
+
52
+ // NOTE we might want to bring this back if we want to debug no-op mutations
53
+ // const makeExecuteOptions = (statementSql: string, bindValues: any) => ({
54
+ // onRowsChanged: (rowsChanged: number) => {
55
+ // if (rowsChanged === 0) {
56
+ // console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
57
+ // }
58
+ // },
59
+ // })
43
60
 
44
61
  // console.group('[@livestore/common:leader-thread:applyMutation]', { mutationName })
45
62
 
@@ -53,30 +70,28 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
53
70
 
54
71
  const changeset = session.changeset()
55
72
  session.finish()
56
- // NOTE for no-op mutations (e.g. if the state didn't change) the changeset will be empty
57
- // TODO possibly write a null value instead of omitting the row
58
- if (changeset !== undefined && changeset.length > 0) {
59
- // TODO use prepared statements
60
- yield* execSql(
61
- db,
62
- ...insertRow({
63
- tableName: SESSION_CHANGESET_META_TABLE,
64
- columns: sessionChangesetMetaTable.sqliteDef.columns,
65
- values: {
66
- idGlobal: mutationEventEncoded.id.global,
67
- idLocal: mutationEventEncoded.id.local,
68
- changeset,
69
- debug: execArgsArr,
70
- },
71
- }),
72
- )
73
- }
73
+
74
+ // TODO use prepared statements
75
+ yield* execSql(
76
+ db,
77
+ ...insertRow({
78
+ tableName: SESSION_CHANGESET_META_TABLE,
79
+ columns: sessionChangesetMetaTable.sqliteDef.columns,
80
+ values: {
81
+ idGlobal: mutationEventEncoded.id.global,
82
+ idLocal: mutationEventEncoded.id.local,
83
+ // NOTE the changeset will be empty (i.e. null) for no-op mutations
84
+ changeset: changeset ?? null,
85
+ debug: execArgsArr,
86
+ },
87
+ }),
88
+ )
74
89
 
75
90
  // console.groupEnd()
76
91
 
77
92
  // write to mutation_log
78
- const excludeFromMutationLog = shouldExcludeMutationFromLog(mutationName, mutationEventDecoded)
79
- if (excludeFromMutationLog === false) {
93
+ const excludeFromMutationLog = shouldExcludeMutationFromLog(mutationName, mutationEventEncoded)
94
+ if (skipMutationLog === false && excludeFromMutationLog === false) {
80
95
  yield* insertIntoMutationLog(mutationEventEncoded, dbLog, mutationDefSchemaHashMap)
81
96
  } else {
82
97
  // console.debug('[@livestore/common:leader-thread] skipping mutation log write', mutation, statementSql, bindValues)
@@ -132,11 +147,14 @@ const makeShouldExcludeMutationFromLog = memoizeByRef((schema: LiveStoreSchema)
132
147
  ? (migrationOptions.excludeMutations ?? new Set(['livestore.RawSql']))
133
148
  : new Set(['livestore.RawSql'])
134
149
 
135
- return (mutationName: string, mutationEventDecoded: MutationEvent.Any): boolean => {
150
+ return (mutationName: string, mutationEventEncoded: MutationEvent.AnyEncoded): boolean => {
136
151
  if (mutationLogExclude.has(mutationName)) return true
137
152
 
138
153
  const mutationDef = schema.mutations.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
139
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
154
+ const execArgsArr = getExecArgsFromMutation({
155
+ mutationDef,
156
+ mutationEvent: { decoded: undefined, encoded: mutationEventEncoded },
157
+ })
140
158
 
141
159
  return execArgsArr.some((_) => _.statementSql.includes('__livestore'))
142
160
  }
@@ -9,6 +9,7 @@ import {
9
9
  FiberHandle,
10
10
  Option,
11
11
  OtelTracer,
12
+ ReadonlyArray,
12
13
  Ref,
13
14
  Schema,
14
15
  Stream,
@@ -102,7 +103,7 @@ export const makeLeaderSyncProcessor = ({
102
103
  initialBlockingSyncContext: InitialBlockingSyncContext
103
104
  }): Effect.Effect<SyncProcessor, UnexpectedError, Scope.Scope> =>
104
105
  Effect.gen(function* () {
105
- const syncBackendQueue = yield* BucketQueue.make<MutationEvent.AnyEncoded>()
106
+ const syncBackendQueue = yield* BucketQueue.make<MutationEvent.EncodedWithMeta>()
106
107
 
107
108
  const stateRef = yield* Ref.make<ProcessorState>({ _tag: 'init' })
108
109
 
@@ -257,13 +258,16 @@ export const makeLeaderSyncProcessor = ({
257
258
  )
258
259
  }
259
260
 
260
- const pendingMutationEvents = yield* getMutationEventsSince({ global: initialBackendHead, local: 0 })
261
+ const pendingMutationEvents = yield* getMutationEventsSince({
262
+ global: initialBackendHead,
263
+ local: EventId.localDefault,
264
+ }).pipe(Effect.map(ReadonlyArray.map((_) => new MutationEvent.EncodedWithMeta(_))))
261
265
 
262
266
  const initialSyncState = {
263
- pending: pendingMutationEvents.map((_) => new MutationEvent.EncodedWithMeta(_)),
267
+ pending: pendingMutationEvents,
264
268
  // On the leader we don't need a rollback tail beyond `pending` items
265
269
  rollbackTail: [],
266
- upstreamHead: { global: initialBackendHead, local: 0 },
270
+ upstreamHead: { global: initialBackendHead, local: EventId.localDefault },
267
271
  localHead: initialLocalHead,
268
272
  } as SyncState.SyncState
269
273
 
@@ -410,7 +414,7 @@ const backgroundBackendPulling = ({
410
414
  initialBlockingSyncContext,
411
415
  }: {
412
416
  dbReady: Deferred.Deferred<void>
413
- initialBackendHead: number
417
+ initialBackendHead: EventId.GlobalEventId
414
418
  isLocalEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
415
419
  restartBackendPushing: (
416
420
  filteredRebasedPending: ReadonlyArray<MutationEvent.EncodedWithMeta>,
@@ -505,9 +509,9 @@ const backgroundBackendPulling = ({
505
509
  })
506
510
  }
507
511
 
508
- const fiber = yield* applyMutationItemsRef.current!({
509
- batchItems: updateResult.newEvents,
510
- }).pipe(Effect.fork)
512
+ trimChangesetRows(db, newBackendHead)
513
+
514
+ const fiber = yield* applyMutationItemsRef.current!({ batchItems: updateResult.newEvents }).pipe(Effect.fork)
511
515
 
512
516
  yield* Ref.set(stateRef, {
513
517
  _tag: 'applying-syncstate-advance',
@@ -515,7 +519,6 @@ const backgroundBackendPulling = ({
515
519
  syncState: updateResult.newSyncState,
516
520
  fiber,
517
521
  })
518
- // console.log('setRef:applying-syncstate-advance after backgroundBackendPulling', -1)
519
522
  })
520
523
 
521
524
  yield* syncBackend.pull(cursorInfo).pipe(
@@ -538,7 +541,7 @@ const backgroundBackendPulling = ({
538
541
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
539
542
 
540
543
  yield* onNewPullChunk(
541
- batch.map((_) => new MutationEvent.EncodedWithMeta(_.mutationEventEncoded)),
544
+ batch.map((_) => MutationEvent.EncodedWithMeta.fromGlobal(_.mutationEventEncoded)),
542
545
  remaining,
543
546
  )
544
547
 
@@ -570,7 +573,9 @@ const rollback = ({
570
573
  // Apply changesets in reverse order
571
574
  for (let i = rollbackEvents.length - 1; i >= 0; i--) {
572
575
  const { changeset } = rollbackEvents[i]!
573
- db.makeChangeset(changeset).invert().apply()
576
+ if (changeset !== null) {
577
+ db.makeChangeset(changeset).invert().apply()
578
+ }
574
579
  }
575
580
 
576
581
  // Delete the changeset rows
@@ -588,7 +593,7 @@ const rollback = ({
588
593
  }),
589
594
  )
590
595
 
591
- const getCursorInfo = (remoteHead: number) =>
596
+ const getCursorInfo = (remoteHead: EventId.GlobalEventId) =>
592
597
  Effect.gen(function* () {
593
598
  const { dbLog } = yield* LeaderThreadCtx
594
599
 
@@ -605,7 +610,7 @@ const getCursorInfo = (remoteHead: number) =>
605
610
  ).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
606
611
 
607
612
  return Option.some({
608
- cursor: { global: remoteHead, local: 0 },
613
+ cursor: { global: remoteHead, local: EventId.localDefault },
609
614
  metadata: syncMetadataOption,
610
615
  }) satisfies InitialSyncInfo
611
616
  }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:getCursorInfo', { attributes: { remoteHead } }))
@@ -616,7 +621,7 @@ const backgroundBackendPushing = ({
616
621
  span,
617
622
  }: {
618
623
  dbReady: Deferred.Deferred<void>
619
- syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.AnyEncoded>
624
+ syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
620
625
  span: otel.Span | undefined
621
626
  }) =>
622
627
  Effect.gen(function* () {
@@ -639,7 +644,7 @@ const backgroundBackendPushing = ({
639
644
  })
640
645
 
641
646
  // TODO handle push errors (should only happen during concurrent pull+push)
642
- const pushResult = yield* syncBackend.push(queueItems).pipe(Effect.either)
647
+ const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
643
648
 
644
649
  if (pushResult._tag === 'Left') {
645
650
  span?.addEvent('backend-push-error', { error: pushResult.left.toString() })
@@ -664,3 +669,9 @@ const backgroundBackendPushing = ({
664
669
  }
665
670
  }
666
671
  }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:leader-thread:syncing:backend-pushing'))
672
+
673
+ const trimChangesetRows = (db: SynchronousDatabase, newHead: EventId.EventId) => {
674
+ // Since we're using the session changeset rows to query for the current head,
675
+ // we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
676
+ db.execute(sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE idGlobal < ${newHead.global}`)
677
+ }
@@ -1,18 +1,11 @@
1
- import { Effect, FiberMap, Option, PubSub, Queue, Stream, SubscriptionRef } from '@livestore/utils/effect'
1
+ import { Effect, FiberMap, Option, Stream, SubscriptionRef } from '@livestore/utils/effect'
2
2
 
3
3
  import { Devtools, IntentionalShutdownCause, liveStoreVersion, UnexpectedError } from '../index.js'
4
4
  import { MUTATION_LOG_META_TABLE, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '../schema/mod.js'
5
- import type { ShutdownChannel } from './shutdown-channel.js'
6
5
  import type { DevtoolsOptions, PersistenceInfoPair } from './types.js'
7
6
  import { LeaderThreadCtx } from './types.js'
8
7
 
9
- type SendMessageToDevtools = (
10
- message: Devtools.MessageFromAppLeader,
11
- options?: {
12
- /** Send message even if not connected (e.g. for initial broadcast messages) */
13
- force: boolean
14
- },
15
- ) => Effect.Effect<void>
8
+ type SendMessageToDevtools = (message: Devtools.MessageFromAppLeader) => Effect.Effect<void>
16
9
 
17
10
  // TODO bind scope to the webchannel lifetime
18
11
  export const bootDevtools = (options: DevtoolsOptions) =>
@@ -21,44 +14,24 @@ export const bootDevtools = (options: DevtoolsOptions) =>
21
14
  return
22
15
  }
23
16
 
24
- const { persistenceInfo, shutdownChannel, devtoolsWebChannel } = yield* options.makeContext
17
+ const { connectedClientSessionPullQueues, syncProcessor, extraIncomingMessagesQueue } = yield* LeaderThreadCtx
25
18
 
26
- const isConnected = yield* SubscriptionRef.make(true)
27
-
28
- const incomingMessagesPubSub = yield* PubSub.unbounded<Devtools.MessageToAppLeader>().pipe(
29
- Effect.acquireRelease(PubSub.shutdown),
30
- )
19
+ yield* listenToDevtools({
20
+ incomingMessages: Stream.fromQueue(extraIncomingMessagesQueue),
21
+ sendMessage: () => Effect.void,
22
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
31
23
 
32
- const incomingMessages = Stream.fromPubSub(incomingMessagesPubSub)
24
+ const { persistenceInfo, devtoolsWebChannel } = yield* options.makeContext
33
25
 
34
- const outgoingMessagesQueue = yield* Queue.unbounded<Devtools.MessageFromAppLeader>().pipe(
35
- Effect.acquireRelease(Queue.shutdown),
36
- )
26
+ const sendMessage: SendMessageToDevtools = (message) =>
27
+ devtoolsWebChannel
28
+ .send(message)
29
+ .pipe(
30
+ Effect.withSpan('@livestore/common:leader-thread:devtools:sendToDevtools'),
31
+ Effect.interruptible,
32
+ Effect.ignoreLogged,
33
+ )
37
34
 
38
- const devtoolsCoordinatorChannel = devtoolsWebChannel
39
- // coordinatorMessagePortOrChannel instanceof MessagePort
40
- // ? yield* WebChannel.messagePortChannel({
41
- // port: coordinatorMessagePortOrChannel,
42
- // schema: { send: Devtools.MessageFromAppLeader, listen: Devtools.MessageToAppLeader },
43
- // })
44
- // : coordinatorMessagePortOrChannel
45
-
46
- const sendMessage: SendMessageToDevtools = (message, options) =>
47
- Effect.gen(function* () {
48
- if (options?.force === true || (yield* isConnected)) {
49
- yield* devtoolsCoordinatorChannel.send(message)
50
- } else {
51
- yield* Queue.offer(outgoingMessagesQueue, message)
52
- }
53
- }).pipe(
54
- Effect.withSpan('@livestore/common:leader-thread:devtools:sendToDevtools'),
55
- Effect.interruptible,
56
- Effect.ignoreLogged,
57
- )
58
-
59
- // broadcastCallbacks.add((message) => sendMessage(message))
60
-
61
- const { connectedClientSessionPullQueues, syncProcessor } = yield* LeaderThreadCtx
62
35
  const { localHead } = yield* syncProcessor.syncState
63
36
 
64
37
  // TODO close queue when devtools disconnects
@@ -69,13 +42,8 @@ export const bootDevtools = (options: DevtoolsOptions) =>
69
42
  Effect.gen(function* () {
70
43
  if (msg.payload._tag === 'upstream-advance') {
71
44
  for (const mutationEventEncoded of msg.payload.newEvents) {
72
- yield* sendMessage(
73
- Devtools.MutationBroadcast.make({
74
- mutationEventEncoded,
75
-
76
- liveStoreVersion,
77
- }),
78
- )
45
+ // TODO refactor with push semantics
46
+ yield* sendMessage(Devtools.MutationBroadcast.make({ mutationEventEncoded, liveStoreVersion }))
79
47
  }
80
48
  } else {
81
49
  yield* Effect.logWarning('TODO implement rebases in devtools')
@@ -86,68 +54,25 @@ export const bootDevtools = (options: DevtoolsOptions) =>
86
54
  Effect.forkScoped,
87
55
  )
88
56
 
89
- yield* devtoolsCoordinatorChannel.listen.pipe(
90
- Stream.flatten(),
91
- // Stream.tapLogWithLabel('@livestore/common:leader-thread:devtools:onPortMessage'),
92
- Stream.tap((msg) =>
93
- Effect.gen(function* () {
94
- // yield* Effect.logDebug(`[@livestore/common:leader-thread:devtools] message from port: ${msg._tag}`, msg)
95
- // if (msg._tag === 'LSD.MessagePortForStoreRes') {
96
- // yield* Deferred.succeed(storeMessagePortDeferred, msg.port)
97
- // } else {
98
- yield* PubSub.publish(incomingMessagesPubSub, msg)
99
- // }
100
- }),
101
- ),
102
- Stream.runDrain,
103
- Effect.withSpan(`@livestore/common:leader-thread:devtools:onPortMessage`),
104
- Effect.ignoreLogged,
105
- Effect.forkScoped,
106
- )
107
-
108
- // yield* sendMessage(Devtools.AppHostReady.make({ appHostId, liveStoreVersion, isLeader }), { force: true })
109
-
110
- // yield* sendMessage(Devtools.MessagePortForStoreReq.make({ appHostId, liveStoreVersion, requestId: nanoid() }), {
111
- // force: true,
112
- // })
113
-
114
57
  yield* listenToDevtools({
115
- incomingMessages,
58
+ incomingMessages: devtoolsWebChannel.listen.pipe(Stream.flatten(), Stream.orDie),
116
59
  sendMessage,
117
- // isConnected,
118
- // disconnect,
119
- // storeId,
120
- // appHostId,
121
- // isLeader,
122
60
  persistenceInfo,
123
- shutdownChannel,
124
- })
61
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
125
62
  }).pipe(Effect.withSpan('@livestore/common:leader-thread:devtools:boot'))
126
63
 
127
64
  const listenToDevtools = ({
128
65
  incomingMessages,
129
66
  sendMessage,
130
- // isConnected,
131
- // disconnect,
132
- // appHostId,
133
- // storeId,
134
- // isLeader,
135
67
  persistenceInfo,
136
- shutdownChannel,
137
68
  }: {
138
69
  incomingMessages: Stream.Stream<Devtools.MessageToAppLeader>
139
70
  sendMessage: SendMessageToDevtools
140
- // isConnected: SubscriptionRef.SubscriptionRef<boolean>
141
- // disconnect: Effect.Effect<void>
142
- // appHostId: string
143
- // storeId: string
144
- // isLeader: boolean
145
- persistenceInfo: PersistenceInfoPair
146
- shutdownChannel: ShutdownChannel
71
+ persistenceInfo?: PersistenceInfoPair
147
72
  }) =>
148
73
  Effect.gen(function* () {
149
- const innerWorkerCtx = yield* LeaderThreadCtx
150
- const { syncBackend, makeSyncDb, db, dbLog, shutdownStateSubRef, syncProcessor } = innerWorkerCtx
74
+ const { syncBackend, makeSyncDb, db, dbLog, shutdownStateSubRef, shutdownChannel, syncProcessor } =
75
+ yield* LeaderThreadCtx
151
76
 
152
77
  type RequestId = string
153
78
  const subscriptionFiberMap = yield* FiberMap.make<RequestId>()
@@ -158,15 +83,6 @@ const listenToDevtools = ({
158
83
  // yield* Effect.logDebug('[@livestore/common:leader-thread:devtools] incomingMessage', decodedEvent)
159
84
 
160
85
  if (decodedEvent._tag === 'LSD.Disconnect') {
161
- // yield* SubscriptionRef.set(isConnected, false)
162
-
163
- // yield* disconnect
164
-
165
- // TODO is there a better place for this?
166
- // yield* sendMessage(Devtools.AppHostReady.make({ appHostId, liveStoreVersion, isLeader }), {
167
- // force: true,
168
- // })
169
-
170
86
  return
171
87
  }
172
88
 
@@ -226,7 +142,7 @@ const listenToDevtools = ({
226
142
 
227
143
  yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
228
144
 
229
- yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-import' }))
145
+ yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-import' })) ?? Effect.void
230
146
 
231
147
  return
232
148
  }
@@ -243,11 +159,16 @@ const listenToDevtools = ({
243
159
 
244
160
  yield* sendMessage(Devtools.ResetAllDataRes.make({ ...reqPayload }))
245
161
 
246
- yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-reset' }))
162
+ yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-reset' })) ?? Effect.void
247
163
 
248
164
  return
249
165
  }
250
166
  case 'LSD.Leader.DatabaseFileInfoReq': {
167
+ if (persistenceInfo === undefined) {
168
+ console.log('[@livestore/common:leader-thread:devtools] persistenceInfo is required for this request')
169
+ return
170
+ }
171
+
251
172
  const dbSizeQuery = `SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();`
252
173
  const dbFileSize = db.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
253
174
  const mutationLogFileSize = dbLog.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size