@livestore/common 0.3.0-dev.0 → 0.3.0-dev.10

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 (146) 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 +98 -110
  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/index.d.ts +0 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/leader-thread/LeaderSyncProcessor.d.ts +37 -0
  17. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
  18. package/dist/leader-thread/LeaderSyncProcessor.js +417 -0
  19. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
  20. package/dist/leader-thread/apply-mutation.d.ts +5 -2
  21. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  22. package/dist/leader-thread/apply-mutation.js +38 -26
  23. package/dist/leader-thread/apply-mutation.js.map +1 -1
  24. package/dist/leader-thread/leader-sync-processor.d.ts +2 -2
  25. package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -1
  26. package/dist/leader-thread/leader-sync-processor.js +20 -12
  27. package/dist/leader-thread/leader-sync-processor.js.map +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  29. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  30. package/dist/leader-thread/leader-worker-devtools.js +22 -66
  31. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.d.ts +8 -7
  33. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.js +11 -5
  35. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  36. package/dist/leader-thread/mutationlog.d.ts +4 -17
  37. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  38. package/dist/leader-thread/mutationlog.js +2 -1
  39. package/dist/leader-thread/mutationlog.js.map +1 -1
  40. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  41. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  42. package/dist/leader-thread/recreate-db.js +9 -3
  43. package/dist/leader-thread/recreate-db.js.map +1 -1
  44. package/dist/leader-thread/types.d.ts +17 -9
  45. package/dist/leader-thread/types.d.ts.map +1 -1
  46. package/dist/leader-thread/types.js.map +1 -1
  47. package/dist/mutation.d.ts +9 -2
  48. package/dist/mutation.d.ts.map +1 -1
  49. package/dist/mutation.js +5 -5
  50. package/dist/mutation.js.map +1 -1
  51. package/dist/query-builder/impl.d.ts +1 -1
  52. package/dist/rehydrate-from-mutationlog.d.ts +2 -2
  53. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  54. package/dist/rehydrate-from-mutationlog.js +13 -19
  55. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  56. package/dist/schema/EventId.d.ts +16 -14
  57. package/dist/schema/EventId.d.ts.map +1 -1
  58. package/dist/schema/EventId.js +15 -7
  59. package/dist/schema/EventId.js.map +1 -1
  60. package/dist/schema/EventId.test.d.ts +2 -0
  61. package/dist/schema/EventId.test.d.ts.map +1 -0
  62. package/dist/schema/EventId.test.js +11 -0
  63. package/dist/schema/EventId.test.js.map +1 -0
  64. package/dist/schema/MutationEvent.d.ts +49 -80
  65. package/dist/schema/MutationEvent.d.ts.map +1 -1
  66. package/dist/schema/MutationEvent.js +32 -15
  67. package/dist/schema/MutationEvent.js.map +1 -1
  68. package/dist/schema/MutationEvent.test.d.ts +2 -0
  69. package/dist/schema/MutationEvent.test.d.ts.map +1 -0
  70. package/dist/schema/MutationEvent.test.js +2 -0
  71. package/dist/schema/MutationEvent.test.js.map +1 -0
  72. package/dist/schema/system-tables.d.ts +26 -26
  73. package/dist/schema/system-tables.d.ts.map +1 -1
  74. package/dist/schema/system-tables.js +19 -11
  75. package/dist/schema/system-tables.js.map +1 -1
  76. package/dist/schema-management/migrations.js +6 -6
  77. package/dist/schema-management/migrations.js.map +1 -1
  78. package/dist/sync/ClientSessionSyncProcessor.d.ts +45 -0
  79. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
  80. package/dist/sync/ClientSessionSyncProcessor.js +134 -0
  81. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
  82. package/dist/sync/client-session-sync-processor.d.ts +4 -4
  83. package/dist/sync/client-session-sync-processor.d.ts.map +1 -1
  84. package/dist/sync/index.d.ts +1 -1
  85. package/dist/sync/index.d.ts.map +1 -1
  86. package/dist/sync/index.js +1 -1
  87. package/dist/sync/index.js.map +1 -1
  88. package/dist/sync/next/history-dag-common.d.ts +1 -4
  89. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  90. package/dist/sync/next/history-dag-common.js +1 -1
  91. package/dist/sync/next/history-dag-common.js.map +1 -1
  92. package/dist/sync/next/rebase-events.d.ts +3 -3
  93. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  94. package/dist/sync/next/rebase-events.js +3 -2
  95. package/dist/sync/next/rebase-events.js.map +1 -1
  96. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  97. package/dist/sync/next/test/mutation-fixtures.js +3 -9
  98. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  99. package/dist/sync/sync.d.ts +21 -11
  100. package/dist/sync/sync.d.ts.map +1 -1
  101. package/dist/sync/sync.js.map +1 -1
  102. package/dist/sync/syncstate.d.ts +45 -23
  103. package/dist/sync/syncstate.d.ts.map +1 -1
  104. package/dist/sync/syncstate.js +56 -12
  105. package/dist/sync/syncstate.js.map +1 -1
  106. package/dist/sync/syncstate.test.js +125 -69
  107. package/dist/sync/syncstate.test.js.map +1 -1
  108. package/dist/sync/validate-push-payload.d.ts +2 -2
  109. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  110. package/dist/sync/validate-push-payload.js +2 -2
  111. package/dist/sync/validate-push-payload.js.map +1 -1
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.d.ts.map +1 -1
  114. package/dist/version.js +1 -1
  115. package/dist/version.js.map +1 -1
  116. package/package.json +6 -5
  117. package/src/adapter-types.ts +22 -24
  118. package/src/derived-mutations.test.ts +1 -1
  119. package/src/derived-mutations.ts +9 -5
  120. package/src/devtools/devtools-bridge.ts +2 -1
  121. package/src/devtools/devtools-messages.ts +9 -6
  122. package/src/index.ts +0 -6
  123. package/src/leader-thread/{leader-sync-processor.ts → LeaderSyncProcessor.ts} +235 -230
  124. package/src/leader-thread/apply-mutation.ts +49 -31
  125. package/src/leader-thread/leader-worker-devtools.ts +30 -109
  126. package/src/leader-thread/make-leader-thread-layer.ts +24 -13
  127. package/src/leader-thread/mutationlog.ts +9 -5
  128. package/src/leader-thread/recreate-db.ts +9 -5
  129. package/src/leader-thread/types.ts +18 -11
  130. package/src/mutation.ts +17 -7
  131. package/src/rehydrate-from-mutationlog.ts +15 -23
  132. package/src/schema/EventId.test.ts +12 -0
  133. package/src/schema/EventId.ts +23 -9
  134. package/src/schema/MutationEvent.ts +46 -24
  135. package/src/schema/system-tables.ts +19 -11
  136. package/src/schema-management/migrations.ts +6 -6
  137. package/src/sync/{client-session-sync-processor.ts → ClientSessionSyncProcessor.ts} +11 -9
  138. package/src/sync/index.ts +1 -1
  139. package/src/sync/next/history-dag-common.ts +1 -1
  140. package/src/sync/next/rebase-events.ts +7 -7
  141. package/src/sync/next/test/mutation-fixtures.ts +3 -10
  142. package/src/sync/sync.ts +19 -6
  143. package/src/sync/syncstate.test.ts +127 -67
  144. package/src/sync/syncstate.ts +21 -19
  145. package/src/sync/validate-push-payload.ts +7 -4
  146. package/src/version.ts +1 -1
@@ -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)
@@ -86,7 +101,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
86
101
  attributes: {
87
102
  mutationName: mutationEventEncoded.mutation,
88
103
  mutationId: mutationEventEncoded.id,
89
- 'span.label': mutationEventEncoded.mutation,
104
+ 'span.label': `(${mutationEventEncoded.id.global},${mutationEventEncoded.id.local}) ${mutationEventEncoded.mutation}`,
90
105
  },
91
106
  }),
92
107
  // Effect.logDuration('@livestore/common:leader-thread:applyMutation'),
@@ -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
  }
@@ -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
@@ -1,41 +1,43 @@
1
- import type { HttpClient, Scope, WebChannel } from '@livestore/utils/effect'
2
- import { Deferred, Effect, FiberSet, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
1
+ import type { HttpClient, Scope } from '@livestore/utils/effect'
2
+ import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
3
3
 
4
4
  import type { BootStatus, MakeSynchronousDatabase, SqliteError, SynchronousDatabase } from '../adapter-types.js'
5
5
  import { UnexpectedError } from '../adapter-types.js'
6
+ import type * as Devtools from '../devtools/index.js'
6
7
  import type { LiveStoreSchema } from '../schema/mod.js'
7
8
  import { EventId, MutationEvent, mutationLogMetaTable, SYNC_STATUS_TABLE, syncStatusTable } from '../schema/mod.js'
8
9
  import { migrateTable } from '../schema-management/migrations.js'
9
- import type { InvalidPullError, IsOfflineError, SyncBackend } from '../sync/sync.js'
10
+ import type { InvalidPullError, IsOfflineError, SyncOptions } from '../sync/sync.js'
10
11
  import { sql } from '../util.js'
11
12
  import { execSql } from './connection.js'
12
- import { makeLeaderSyncProcessor } from './leader-sync-processor.js'
13
13
  import { bootDevtools } from './leader-worker-devtools.js'
14
+ import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
14
15
  import { makePullQueueSet } from './pull-queue-set.js'
15
16
  import { recreateDb } from './recreate-db.js'
17
+ import type { ShutdownChannel } from './shutdown-channel.js'
16
18
  import type { DevtoolsOptions, InitialBlockingSyncContext, InitialSyncOptions, ShutdownState } from './types.js'
17
19
  import { LeaderThreadCtx } from './types.js'
18
20
 
19
21
  export const makeLeaderThreadLayer = ({
20
22
  schema,
21
23
  storeId,
22
- originId,
24
+ clientId,
23
25
  makeSyncDb,
24
- makeSyncBackend,
26
+ syncOptions,
25
27
  db,
26
28
  dbLog,
27
29
  devtoolsOptions,
28
- initialSyncOptions = { _tag: 'Skip' },
30
+ shutdownChannel,
29
31
  }: {
30
32
  storeId: string
31
- originId: string
33
+ clientId: string
32
34
  schema: LiveStoreSchema
33
35
  makeSyncDb: MakeSynchronousDatabase
34
- makeSyncBackend: Effect.Effect<SyncBackend, UnexpectedError, Scope.Scope> | undefined
36
+ syncOptions: SyncOptions | undefined
35
37
  db: SynchronousDatabase
36
38
  dbLog: SynchronousDatabase
37
39
  devtoolsOptions: DevtoolsOptions
38
- initialSyncOptions: InitialSyncOptions | undefined
40
+ shutdownChannel: ShutdownChannel
39
41
  }): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
40
42
  Effect.gen(function* () {
41
43
  const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
@@ -44,25 +46,34 @@ export const makeLeaderThreadLayer = ({
44
46
  // Either happens on initial boot or if schema changes
45
47
  const dbMissing = db.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
46
48
 
47
- const syncBackend = makeSyncBackend === undefined ? undefined : yield* makeSyncBackend
49
+ const syncBackend = syncOptions === undefined ? undefined : yield* syncOptions.makeBackend({ storeId, clientId })
48
50
 
49
- const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({ initialSyncOptions, bootStatusQueue })
51
+ const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
52
+ initialSyncOptions: syncOptions?.initialSyncOptions ?? { _tag: 'Skip' },
53
+ bootStatusQueue,
54
+ })
50
55
 
51
56
  const syncProcessor = yield* makeLeaderSyncProcessor({ schema, dbMissing, dbLog, initialBlockingSyncContext })
52
57
 
58
+ const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.MessageToAppLeader>().pipe(
59
+ Effect.acquireRelease(Queue.shutdown),
60
+ )
61
+
53
62
  const ctx = {
54
63
  schema,
55
64
  bootStatusQueue,
56
65
  storeId,
57
- originId,
66
+ clientId,
58
67
  db,
59
68
  dbLog,
60
69
  makeSyncDb,
61
70
  mutationEventSchema: MutationEvent.makeMutationEventSchema(schema),
62
71
  shutdownStateSubRef: yield* SubscriptionRef.make<ShutdownState>('running'),
72
+ shutdownChannel,
63
73
  syncBackend,
64
74
  syncProcessor,
65
75
  connectedClientSessionPullQueues: yield* makePullQueueSet,
76
+ extraIncomingMessagesQueue,
66
77
  } satisfies typeof LeaderThreadCtx.Service
67
78
 
68
79
  // @ts-expect-error For debugging purposes
@@ -2,11 +2,14 @@ import { Effect, Schema } from '@livestore/utils/effect'
2
2
 
3
3
  import type { SynchronousDatabase } from '../adapter-types.js'
4
4
  import * as EventId from '../schema/EventId.js'
5
+ import type * as MutationEvent from '../schema/MutationEvent.js'
5
6
  import { MUTATION_LOG_META_TABLE, mutationLogMetaTable, SYNC_STATUS_TABLE } from '../schema/system-tables.js'
6
7
  import { prepareBindValues, sql } from '../util.js'
7
8
  import { LeaderThreadCtx } from './types.js'
8
9
 
9
- export const getMutationEventsSince = (since: EventId.EventId) =>
10
+ export const getMutationEventsSince = (
11
+ since: EventId.EventId,
12
+ ): Effect.Effect<ReadonlyArray<MutationEvent.AnyEncoded>, never, LeaderThreadCtx> =>
10
13
  Effect.gen(function* () {
11
14
  const { dbLog } = yield* LeaderThreadCtx
12
15
 
@@ -26,16 +29,17 @@ export const getMutationEventsSince = (since: EventId.EventId) =>
26
29
  .filter((_) => EventId.compare(_.id, since) > 0)
27
30
  })
28
31
 
29
- export const getLocalHeadFromDb = (dbLog: SynchronousDatabase) => {
30
- const res = dbLog.select<{ idGlobal: number; idLocal: number }>(
32
+ export const getLocalHeadFromDb = (dbLog: SynchronousDatabase): EventId.EventId => {
33
+ const res = dbLog.select<{ idGlobal: EventId.GlobalEventId; idLocal: EventId.LocalEventId }>(
31
34
  sql`select idGlobal, idLocal from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idLocal DESC limit 1`,
32
35
  )[0]
33
36
 
34
37
  return res ? { global: res.idGlobal, local: res.idLocal } : EventId.ROOT
35
38
  }
36
39
 
37
- export const getBackendHeadFromDb = (dbLog: SynchronousDatabase) =>
38
- dbLog.select<{ head: number }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ?? EventId.ROOT.global
40
+ export const getBackendHeadFromDb = (dbLog: SynchronousDatabase): EventId.GlobalEventId =>
41
+ dbLog.select<{ head: EventId.GlobalEventId }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ??
42
+ EventId.ROOT.global
39
43
 
40
44
  // TODO use prepared statements
41
45
  export const updateBackendHead = (dbLog: SynchronousDatabase, head: EventId.EventId) =>
@@ -24,7 +24,9 @@ export const recreateDb: Effect.Effect<
24
24
 
25
25
  // NOTE to speed up the operations below, we're creating a temporary in-memory database
26
26
  // and later we'll overwrite the persisted database with the new data
27
- const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
27
+ // TODO bring back this optimization
28
+ // const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
29
+ const tmpSyncDb = db
28
30
  yield* configureConnection(tmpSyncDb, { fkEnabled: true })
29
31
 
30
32
  const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
@@ -91,17 +93,19 @@ export const recreateDb: Effect.Effect<
91
93
  }
92
94
  }
93
95
 
96
+ // TODO bring back
94
97
  // Import the temporary in-memory database into the persistent database
95
- yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
96
- Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
97
- )
98
+ // yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
99
+ // Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
100
+ // )
98
101
 
99
102
  // TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
100
103
  // We've disabled this for now as it made the code too complex, as we often run syncing right after
101
104
  // so the snapshot is no longer up to date
102
105
  // const snapshotFromTmpDb = tmpSyncDb.export()
103
106
 
104
- tmpSyncDb.close()
107
+ // TODO bring back
108
+ // tmpSyncDb.close()
105
109
  }).pipe(
106
110
  Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
107
111
  Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
@@ -6,6 +6,7 @@ import type {
6
6
  Option,
7
7
  Queue,
8
8
  Scope,
9
+ Subscribable,
9
10
  SubscriptionRef,
10
11
  WebChannel,
11
12
  } from '@livestore/utils/effect'
@@ -22,7 +23,7 @@ import type {
22
23
  UnexpectedError,
23
24
  } from '../index.js'
24
25
  import type { EventId, LiveStoreSchema, MutationEvent } from '../schema/mod.js'
25
- import type { PayloadUpstream, SyncState } from '../sync/syncstate.js'
26
+ import type * as SyncState from '../sync/syncstate.js'
26
27
  import type { ShutdownChannel } from './shutdown-channel.js'
27
28
 
28
29
  export type ShutdownState = 'running' | 'shutting-down'
@@ -67,7 +68,6 @@ export type DevtoolsOptions =
67
68
  makeContext: Effect.Effect<
68
69
  {
69
70
  devtoolsWebChannel: WebChannel.WebChannel<Devtools.MessageToAppLeader, Devtools.MessageFromAppLeader>
70
- shutdownChannel: ShutdownChannel
71
71
  persistenceInfo: PersistenceInfoPair
72
72
  },
73
73
  UnexpectedError,
@@ -80,18 +80,21 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
80
80
  {
81
81
  schema: LiveStoreSchema
82
82
  storeId: string
83
- originId: string
83
+ clientId: string
84
84
  makeSyncDb: MakeSynchronousDatabase
85
85
  db: LeaderDatabase
86
86
  dbLog: LeaderDatabase
87
87
  bootStatusQueue: Queue.Queue<BootStatus>
88
88
  // TODO we should find a more elegant way to handle cases which need this ref for their implementation
89
89
  shutdownStateSubRef: SubscriptionRef.SubscriptionRef<ShutdownState>
90
+ shutdownChannel: ShutdownChannel
90
91
  mutationEventSchema: MutationEvent.ForMutationDefRecord<any>
91
92
  // devtools: DevtoolsContext
92
93
  syncBackend: SyncBackend | undefined
93
- syncProcessor: SyncProcessor
94
+ syncProcessor: LeaderSyncProcessor
94
95
  connectedClientSessionPullQueues: PullQueueSet
96
+ /** e.g. used for `store.__dev` APIs */
97
+ extraIncomingMessagesQueue: Queue.Queue<Devtools.MessageToAppLeader>
95
98
  }
96
99
  >() {}
97
100
 
@@ -101,24 +104,28 @@ export type InitialBlockingSyncContext = {
101
104
  }
102
105
 
103
106
  export type PullQueueItem = {
104
- // mutationEvents: ReadonlyArray<MutationEvent.AnyEncoded>
105
- // backendHead: number
106
- payload: PayloadUpstream
107
- // TODO move `remaining` into `PayloadUpstream`
107
+ payload: SyncState.PayloadUpstream
108
108
  remaining: number
109
109
  }
110
110
 
111
- export interface SyncProcessor {
111
+ export interface LeaderSyncProcessor {
112
112
  push: (
113
113
  /** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
114
114
  batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
115
- ) => Effect.Effect<void, UnexpectedError | InvalidPushError, HttpClient.HttpClient | LeaderThreadCtx>
115
+ options?: {
116
+ /**
117
+ * If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
118
+ * @default false
119
+ */
120
+ waitForProcessing?: boolean
121
+ },
122
+ ) => Effect.Effect<void, InvalidPushError>
116
123
 
117
124
  pushPartial: (mutationEvent: MutationEvent.PartialAnyEncoded) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
118
125
  boot: (args: {
119
126
  dbReady: Deferred.Deferred<void>
120
127
  }) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient>
121
- syncState: Effect.Effect<SyncState, UnexpectedError>
128
+ syncState: Subscribable.Subscribable<SyncState.SyncState>
122
129
  }
123
130
 
124
131
  export interface PullQueueSet {
package/src/mutation.ts CHANGED
@@ -8,10 +8,19 @@ import { prepareBindValues } from './util.js'
8
8
 
9
9
  export const getExecArgsFromMutation = ({
10
10
  mutationDef,
11
- mutationEventDecoded,
11
+ mutationEvent,
12
12
  }: {
13
13
  mutationDef: MutationDef.Any
14
- mutationEventDecoded: MutationEvent.Any | MutationEvent.PartialAny
14
+ /** Both encoded and decoded mutation events are supported to reduce the number of times we need to decode/encode */
15
+ mutationEvent:
16
+ | {
17
+ decoded: MutationEvent.AnyDecoded | MutationEvent.PartialAnyDecoded
18
+ encoded: undefined
19
+ }
20
+ | {
21
+ decoded: undefined
22
+ encoded: MutationEvent.AnyEncoded | MutationEvent.PartialAnyEncoded
23
+ }
15
24
  }): ReadonlyArray<{
16
25
  statementSql: string
17
26
  bindValues: PreparedBindValues
@@ -23,7 +32,9 @@ export const getExecArgsFromMutation = ({
23
32
 
24
33
  switch (typeof mutationDef.sql) {
25
34
  case 'function': {
26
- const res = mutationDef.sql(mutationEventDecoded.args)
35
+ const mutationArgsDecoded =
36
+ mutationEvent.decoded?.args ?? Schema.decodeUnknownSync(mutationDef.schema)(mutationEvent.encoded!.args)
37
+ const res = mutationDef.sql(mutationArgsDecoded)
27
38
  statementRes = Array.isArray(res) ? res : [res]
28
39
  break
29
40
  }
@@ -40,10 +51,9 @@ export const getExecArgsFromMutation = ({
40
51
  return statementRes.map((statementRes) => {
41
52
  const statementSql = typeof statementRes === 'string' ? statementRes : statementRes.sql
42
53
 
43
- const bindValues =
44
- typeof statementRes === 'string'
45
- ? Schema.encodeUnknownSync(mutationDef.schema)(mutationEventDecoded.args)
46
- : statementRes.bindValues
54
+ const mutationArgsEncoded =
55
+ mutationEvent.encoded?.args ?? Schema.encodeUnknownSync(mutationDef.schema)(mutationEvent.decoded!.args)
56
+ const bindValues = typeof statementRes === 'string' ? mutationArgsEncoded : statementRes.bindValues
47
57
 
48
58
  const writeTables = typeof statementRes === 'string' ? undefined : statementRes.writeTables
49
59