@livestore/livestore 0.0.39 → 0.0.40

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 (190) hide show
  1. package/README.md +15 -24
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts +192 -17
  4. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  5. package/dist/__tests__/react/fixture.js +10 -29
  6. package/dist/__tests__/react/fixture.js.map +1 -1
  7. package/dist/{mutations.d.ts → cud.d.ts} +14 -19
  8. package/dist/cud.d.ts.map +1 -0
  9. package/dist/{mutations.js → cud.js} +15 -7
  10. package/dist/cud.js.map +1 -0
  11. package/dist/cud.test.d.ts +2 -0
  12. package/dist/cud.test.d.ts.map +1 -0
  13. package/dist/cud.test.js +47 -0
  14. package/dist/cud.test.js.map +1 -0
  15. package/dist/inMemoryDatabase.d.ts +1 -1
  16. package/dist/inMemoryDatabase.d.ts.map +1 -1
  17. package/dist/inMemoryDatabase.js +1 -4
  18. package/dist/inMemoryDatabase.js.map +1 -1
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/migrations.d.ts.map +1 -1
  24. package/dist/migrations.js +11 -7
  25. package/dist/migrations.js.map +1 -1
  26. package/dist/query-info.d.ts +2 -5
  27. package/dist/query-info.d.ts.map +1 -1
  28. package/dist/query-info.js +3 -2
  29. package/dist/query-info.js.map +1 -1
  30. package/dist/react/useAtom.d.ts.map +1 -1
  31. package/dist/react/useAtom.js +2 -2
  32. package/dist/react/useAtom.js.map +1 -1
  33. package/dist/react/useQuery.test.d.ts.map +1 -0
  34. package/dist/{__tests__/react → react}/useQuery.test.js +8 -11
  35. package/dist/react/useQuery.test.js.map +1 -0
  36. package/dist/react/useRow.js +4 -4
  37. package/dist/react/useRow.js.map +1 -1
  38. package/dist/react/useRow.test.d.ts.map +1 -0
  39. package/dist/{__tests__/react → react}/useRow.test.js +14 -38
  40. package/dist/react/useRow.test.js.map +1 -0
  41. package/dist/reactive.d.ts +2 -2
  42. package/dist/reactive.d.ts.map +1 -1
  43. package/dist/reactive.js +50 -15
  44. package/dist/reactive.js.map +1 -1
  45. package/dist/reactive.test.d.ts.map +1 -0
  46. package/dist/{__tests__/reactive.test.js → reactive.test.js} +1 -1
  47. package/dist/reactive.test.js.map +1 -0
  48. package/dist/reactiveQueries/base-class.d.ts +2 -0
  49. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  50. package/dist/reactiveQueries/base-class.js +1 -0
  51. package/dist/reactiveQueries/base-class.js.map +1 -1
  52. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  53. package/dist/reactiveQueries/graphql.js +1 -0
  54. package/dist/reactiveQueries/graphql.js.map +1 -1
  55. package/dist/reactiveQueries/js.d.ts.map +1 -1
  56. package/dist/reactiveQueries/js.js +1 -0
  57. package/dist/reactiveQueries/js.js.map +1 -1
  58. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  59. package/dist/reactiveQueries/sql.js +1 -0
  60. package/dist/reactiveQueries/sql.js.map +1 -1
  61. package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
  62. package/dist/{__tests__/reactiveQueries → reactiveQueries}/sql.test.js +44 -34
  63. package/dist/reactiveQueries/sql.test.js.map +1 -0
  64. package/dist/row-query.js +7 -5
  65. package/dist/row-query.js.map +1 -1
  66. package/dist/schema/index.d.ts +20 -7
  67. package/dist/schema/index.d.ts.map +1 -1
  68. package/dist/schema/index.js +18 -3
  69. package/dist/schema/index.js.map +1 -1
  70. package/dist/schema/mutations.d.ts +81 -0
  71. package/dist/schema/mutations.d.ts.map +1 -0
  72. package/dist/schema/mutations.js +29 -0
  73. package/dist/schema/mutations.js.map +1 -0
  74. package/dist/schema/parse-utils.d.ts +3 -3
  75. package/dist/schema/parse-utils.d.ts.map +1 -1
  76. package/dist/schema/table-def.d.ts +1 -1
  77. package/dist/schema/table-def.d.ts.map +1 -1
  78. package/dist/schema/table-def.js +2 -2
  79. package/dist/schema/table-def.js.map +1 -1
  80. package/dist/storage/in-memory/index.d.ts +4 -0
  81. package/dist/storage/in-memory/index.d.ts.map +1 -1
  82. package/dist/storage/in-memory/index.js +3 -0
  83. package/dist/storage/in-memory/index.js.map +1 -1
  84. package/dist/storage/index.d.ts +4 -0
  85. package/dist/storage/index.d.ts.map +1 -1
  86. package/dist/storage/tauri/index.d.ts +4 -0
  87. package/dist/storage/tauri/index.d.ts.map +1 -1
  88. package/dist/storage/tauri/index.js +6 -0
  89. package/dist/storage/tauri/index.js.map +1 -1
  90. package/dist/storage/utils/idb.d.ts +1 -0
  91. package/dist/storage/utils/idb.d.ts.map +1 -1
  92. package/dist/storage/utils/idb.js +11 -0
  93. package/dist/storage/utils/idb.js.map +1 -1
  94. package/dist/storage/web-worker/common.d.ts +11 -0
  95. package/dist/storage/web-worker/common.d.ts.map +1 -0
  96. package/dist/storage/web-worker/common.js +2 -0
  97. package/dist/storage/web-worker/common.js.map +1 -0
  98. package/dist/storage/web-worker/index.d.ts +14 -7
  99. package/dist/storage/web-worker/index.d.ts.map +1 -1
  100. package/dist/storage/web-worker/index.js +70 -14
  101. package/dist/storage/web-worker/index.js.map +1 -1
  102. package/dist/storage/web-worker/make-worker.d.ts +20 -0
  103. package/dist/storage/web-worker/make-worker.d.ts.map +1 -0
  104. package/dist/storage/web-worker/make-worker.js +155 -0
  105. package/dist/storage/web-worker/make-worker.js.map +1 -0
  106. package/dist/storage/web-worker/vite-dev-polyfill.d.ts +2 -0
  107. package/dist/storage/web-worker/vite-dev-polyfill.d.ts.map +1 -0
  108. package/dist/storage/web-worker/vite-dev-polyfill.js +35 -0
  109. package/dist/storage/web-worker/vite-dev-polyfill.js.map +1 -0
  110. package/dist/store.d.ts +32 -42
  111. package/dist/store.d.ts.map +1 -1
  112. package/dist/store.js +82 -131
  113. package/dist/store.js.map +1 -1
  114. package/dist/utils/dev.d.ts +3 -0
  115. package/dist/utils/dev.d.ts.map +1 -0
  116. package/dist/utils/dev.js +16 -0
  117. package/dist/utils/dev.js.map +1 -0
  118. package/dist/utils/util.d.ts +2 -0
  119. package/dist/utils/util.d.ts.map +1 -1
  120. package/dist/utils/util.js +2 -0
  121. package/dist/utils/util.js.map +1 -1
  122. package/package.json +24 -12
  123. package/src/__tests__/react/fixture.tsx +12 -30
  124. package/src/cud.test.ts +52 -0
  125. package/src/{mutations.ts → cud.ts} +28 -22
  126. package/src/inMemoryDatabase.ts +2 -7
  127. package/src/index.ts +14 -8
  128. package/src/migrations.ts +10 -7
  129. package/src/query-info.ts +4 -7
  130. package/src/react/useAtom.ts +2 -2
  131. package/src/{__tests__/react → react}/useQuery.test.tsx +11 -11
  132. package/src/{__tests__/react → react}/useRow.test.tsx +21 -39
  133. package/src/react/useRow.ts +4 -4
  134. package/src/{__tests__/reactive.test.ts → reactive.test.ts} +1 -1
  135. package/src/reactive.ts +60 -19
  136. package/src/reactiveQueries/base-class.ts +4 -0
  137. package/src/reactiveQueries/graphql.ts +2 -0
  138. package/src/reactiveQueries/js.ts +2 -0
  139. package/src/{__tests__/reactiveQueries → reactiveQueries}/sql.test.ts +44 -34
  140. package/src/reactiveQueries/sql.ts +2 -0
  141. package/src/row-query.ts +11 -9
  142. package/src/schema/index.ts +47 -11
  143. package/src/schema/mutations.ts +129 -0
  144. package/src/schema/parse-utils.ts +1 -1
  145. package/src/schema/table-def.ts +7 -1
  146. package/src/storage/in-memory/index.ts +7 -0
  147. package/src/storage/index.ts +8 -0
  148. package/src/storage/tauri/index.ts +10 -0
  149. package/src/storage/utils/idb.ts +14 -0
  150. package/src/storage/web-worker/common.ts +6 -0
  151. package/src/storage/web-worker/index.ts +86 -17
  152. package/src/storage/web-worker/make-worker.ts +214 -0
  153. package/src/storage/web-worker/vite-dev-polyfill.ts +33 -0
  154. package/src/store.ts +142 -212
  155. package/src/utils/dev.ts +23 -0
  156. package/src/utils/util.ts +4 -0
  157. package/dist/__tests__/mutations.test.d.ts +0 -2
  158. package/dist/__tests__/mutations.test.d.ts.map +0 -1
  159. package/dist/__tests__/mutations.test.js +0 -40
  160. package/dist/__tests__/mutations.test.js.map +0 -1
  161. package/dist/__tests__/react/useQuery.test.d.ts.map +0 -1
  162. package/dist/__tests__/react/useQuery.test.js.map +0 -1
  163. package/dist/__tests__/react/useRow.test.d.ts.map +0 -1
  164. package/dist/__tests__/react/useRow.test.js.map +0 -1
  165. package/dist/__tests__/reactive.test.d.ts.map +0 -1
  166. package/dist/__tests__/reactive.test.js.map +0 -1
  167. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +0 -1
  168. package/dist/__tests__/reactiveQueries/sql.test.js.map +0 -1
  169. package/dist/events.d.ts +0 -7
  170. package/dist/events.d.ts.map +0 -1
  171. package/dist/events.js +0 -2
  172. package/dist/events.js.map +0 -1
  173. package/dist/mutations.d.ts.map +0 -1
  174. package/dist/mutations.js.map +0 -1
  175. package/dist/schema/action.d.ts +0 -30
  176. package/dist/schema/action.d.ts.map +0 -1
  177. package/dist/schema/action.js +0 -3
  178. package/dist/schema/action.js.map +0 -1
  179. package/dist/storage/web-worker/worker.d.ts +0 -13
  180. package/dist/storage/web-worker/worker.d.ts.map +0 -1
  181. package/dist/storage/web-worker/worker.js +0 -110
  182. package/dist/storage/web-worker/worker.js.map +0 -1
  183. package/src/__tests__/mutations.test.ts +0 -43
  184. package/src/events.ts +0 -8
  185. package/src/schema/action.ts +0 -41
  186. package/src/storage/web-worker/worker.ts +0 -141
  187. /package/dist/{__tests__/react → react}/useQuery.test.d.ts +0 -0
  188. /package/dist/{__tests__/react → react}/useRow.test.d.ts +0 -0
  189. /package/dist/{__tests__/reactive.test.d.ts → reactive.test.d.ts} +0 -0
  190. /package/dist/{__tests__/reactiveQueries → reactiveQueries}/sql.test.d.ts +0 -0
package/src/store.ts CHANGED
@@ -1,22 +1,21 @@
1
1
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
2
- import { identity } from '@livestore/utils/effect'
2
+ import { Schema } from '@livestore/utils/effect'
3
3
  import * as otel from '@opentelemetry/api'
4
4
  import type { GraphQLSchema } from 'graphql'
5
5
  import type * as Sqlite from 'sqlite-esm'
6
- import { v4 as uuid } from 'uuid'
7
6
 
8
- import type { LiveStoreEvent } from './events.js'
9
7
  import { dynamicallyRegisteredTables, globalDbGraph } from './global-state.js'
10
8
  import { InMemoryDatabase } from './inMemoryDatabase.js'
11
9
  import { migrateDb } from './migrations.js'
12
10
  import type { StackInfo } from './react/utils/stack-info.js'
13
11
  import type { DebugRefreshReasonBase, ReactiveGraph, Ref } from './reactive.js'
14
12
  import type { DbContext, DbGraph, LiveQuery } from './reactiveQueries/base-class.js'
15
- import type { ActionDefinition, GetActionArgs, LiveStoreSchema, SQLWriteStatement } from './schema/index.js'
13
+ import { type LiveStoreSchema, makeMutationEventSchema, type MutationEvent } from './schema/index.js'
16
14
  import type { Storage, StorageInit } from './storage/index.js'
15
+ import { downloadBlob } from './utils/dev.js'
17
16
  import { getDurationMsFromSpan } from './utils/otel.js'
18
17
  import type { ParamsObject } from './utils/util.js'
19
- import { isPromise, prepareBindValues, sql } from './utils/util.js'
18
+ import { isPromise, prepareBindValues } from './utils/util.js'
20
19
 
21
20
  export type BaseGraphQLContext = {
22
21
  queriedTables: Set<string>
@@ -29,11 +28,14 @@ export type GraphQLOptions<TContext> = {
29
28
  makeContext: (db: InMemoryDatabase, tracer: otel.Tracer) => TContext
30
29
  }
31
30
 
32
- export type StoreOptions<TGraphQLContext extends BaseGraphQLContext> = {
31
+ export type StoreOptions<
32
+ TGraphQLContext extends BaseGraphQLContext,
33
+ TSchema extends LiveStoreSchema = LiveStoreSchema,
34
+ > = {
33
35
  db: InMemoryDatabase
34
36
  /** A `Proxy`d version of `db` except that it also mirrors `execute` calls to the storage */
35
37
  dbProxy: InMemoryDatabase
36
- schema: LiveStoreSchema
38
+ schema: TSchema
37
39
  storage?: Storage
38
40
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
39
41
  otelTracer: otel.Tracer
@@ -44,21 +46,9 @@ export type StoreOptions<TGraphQLContext extends BaseGraphQLContext> = {
44
46
  export type RefreshReason =
45
47
  | DebugRefreshReasonBase
46
48
  | {
47
- _tag: 'applyEvent'
48
- /** The event that was applied */
49
- // note: we omit ID because it's annoying to read it given where it gets generated,
50
- // but it would be useful to have in the debugger
51
- event: Omit<LiveStoreEvent, 'id'>
52
-
53
- /** The tables that were written to by the event */
54
- writeTables: ReadonlyArray<string>
55
- }
56
- | {
57
- _tag: 'applyEvents'
58
- /** The events that was applied */
59
- // note: we omit ID because it's annoying to read it given where it gets generated,
60
- // but it would be useful to have in the debugger
61
- events: Omit<LiveStoreEvent, 'id'>[]
49
+ _tag: 'mutate'
50
+ /** The mutations that were applied */
51
+ mutations: ReadonlyArray<MutationEvent.Any>
62
52
 
63
53
  /** The tables that were written to by the event */
64
54
  writeTables: ReadonlyArray<string>
@@ -80,14 +70,17 @@ export type QueryDebugInfo = {
80
70
 
81
71
  export type StoreOtel = {
82
72
  tracer: otel.Tracer
83
- applyEventsSpanContext: otel.Context
73
+ mutationsSpanContext: otel.Context
84
74
  queriesSpanContext: otel.Context
85
75
  }
86
76
 
87
77
  let storeCount = 0
88
78
  const uniqueStoreId = () => `store-${++storeCount}`
89
79
 
90
- export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext> {
80
+ export class Store<
81
+ TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
82
+ TSchema extends LiveStoreSchema = LiveStoreSchema,
83
+ > {
91
84
  id = uniqueStoreId()
92
85
  graph: ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
93
86
  inMemoryDB: InMemoryDatabase
@@ -107,6 +100,8 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
107
100
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
108
101
  storage?: Storage
109
102
 
103
+ private mutationArgsSchema
104
+
110
105
  private constructor({
111
106
  db,
112
107
  dbProxy,
@@ -116,27 +111,31 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
116
111
  dbGraph,
117
112
  otelTracer,
118
113
  otelRootSpanContext,
119
- }: StoreOptions<TGraphQLContext>) {
114
+ }: StoreOptions<TGraphQLContext, TSchema>) {
120
115
  this.inMemoryDB = db
121
116
  this._proxyDb = dbProxy
122
117
  this.schema = schema
118
+
119
+ // TODO refactor
120
+ this.mutationArgsSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()) as any)
121
+
123
122
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
124
123
  this.tableRefs = {}
125
124
  this.activeQueries = new ReferenceCountedSet()
126
125
  this.storage = storage
127
126
 
128
- const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
129
- const otelApplyEventsSpanContext = otel.trace.setSpan(otel.context.active(), applyEventsSpan)
127
+ const mutationsSpan = otelTracer.startSpan('LiveStore:mutations', {}, otelRootSpanContext)
128
+ const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan)
130
129
 
131
130
  const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
132
131
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
133
132
 
134
133
  this.graph = dbGraph ?? globalDbGraph
135
- this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext }
134
+ this.graph.context = { store: this as any, otelTracer, rootOtelContext: otelQueriesSpanContext }
136
135
 
137
136
  this.otel = {
138
137
  tracer: otelTracer,
139
- applyEventsSpanContext: otelApplyEventsSpanContext,
138
+ mutationsSpanContext: otelMuationsSpanContext,
140
139
  queriesSpanContext: otelQueriesSpanContext,
141
140
  }
142
141
 
@@ -161,10 +160,10 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
161
160
  }
162
161
  }
163
162
 
164
- static createStore = <TGraphQLContext extends BaseGraphQLContext>(
165
- storeOptions: StoreOptions<TGraphQLContext>,
163
+ static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
164
+ storeOptions: StoreOptions<TGraphQLContext, TSchema>,
166
165
  parentSpan: otel.Span,
167
- ): Store<TGraphQLContext> => {
166
+ ): Store<TGraphQLContext, TSchema> => {
168
167
  const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
169
168
  return storeOptions.otelTracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
170
169
  try {
@@ -228,92 +227,51 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
228
227
  }
229
228
  }
230
229
 
231
- otel.trace.getSpan(this.otel.applyEventsSpanContext)!.end()
230
+ otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
232
231
  otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
233
232
  }
234
233
 
235
- /* Apply a single write event to the store, and refresh all queries in response */
236
- applyEvent = <TEventType extends string & keyof LiveStoreActionDefinitionsTypes>(
237
- eventType: TEventType,
238
- args: GetActionArgs<LiveStoreActionDefinitionsTypes[TEventType]> = {},
239
- options?: { skipRefresh?: boolean },
240
- ): { durationMs: number } => {
241
- const skipRefresh = options?.skipRefresh ?? false
242
- // console.log('applyEvent', { eventType, args, skipRefresh })
243
-
244
- const applyEventsSpan = otel.trace.getSpan(this.otel.applyEventsSpanContext)!
245
- applyEventsSpan.addEvent('applyEvent')
246
-
247
- return this.otel.tracer.startActiveSpan(
248
- 'LiveStore:applyEvent',
249
- { attributes: {} },
250
- this.otel.applyEventsSpanContext,
251
- (span) => {
252
- try {
253
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
254
- const writeTables = this.applyEventWithoutRefresh(eventType, args, otelContext).writeTables
255
-
256
- const tablesToUpdate = [] as [Ref<null, DbContext, RefreshReason>, null][]
257
- for (const tableName of writeTables) {
258
- const tableRef = this.tableRefs[tableName]
259
- assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
260
- tablesToUpdate.push([tableRef!, null])
261
- }
262
-
263
- const debugRefreshReason = {
264
- _tag: 'applyEvent' as const,
265
- event: { type: eventType, args },
266
- writeTables: [...writeTables],
267
- }
268
-
269
- // Update all table refs together in a batch, to only trigger one reactive update
270
- this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
271
-
272
- if (skipRefresh === false) {
273
- // TODO update the graph
274
- // this.graph.refresh(
275
- // {
276
- // otelHint: 'applyEvents',
277
- // debugRefreshReason,
278
- // },
279
- // otelContext,
280
- // )
281
- }
282
- } catch (e: any) {
283
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
284
-
285
- console.error(e)
286
- shouldNeverHappen(`Error applying event (${eventType}): ${e.toString()}`)
287
- } finally {
288
- span.end()
289
-
290
- return { durationMs: getDurationMsFromSpan(span) }
291
- }
292
- },
293
- )
294
- }
234
+ mutate: {
235
+ <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg): void
236
+ (
237
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
238
+ ): void
239
+ <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(
240
+ options: { label?: string; skipRefresh?: boolean },
241
+ ...list: TMutationArg
242
+ ): void
243
+ (
244
+ options: { label?: string; skipRefresh?: boolean },
245
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
246
+ ): void
247
+ } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
248
+ let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
249
+ let options: { label?: string; skipRefresh?: boolean } | undefined
250
+
251
+ if (typeof firstMutationOrTxnFnOrOptions === 'function') {
252
+ mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
253
+ } else if (
254
+ firstMutationOrTxnFnOrOptions?.label !== undefined ||
255
+ firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined
256
+ ) {
257
+ options = firstMutationOrTxnFnOrOptions
258
+ mutationsEvents = restMutations
259
+ } else {
260
+ mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
261
+ }
295
262
 
296
- /**
297
- * Apply multiple write events to the store, and refresh all queries in response.
298
- * This is faster than calling applyEvent many times in quick succession because
299
- * we can do a single refresh after all the events.
300
- */
301
- applyEvents = (
302
- // TODO make args type-safe in polymorphic array case
303
- events: Iterable<{ eventType: string; args: any }>,
304
- options?: { label?: string; skipRefresh?: boolean },
305
- ): { durationMs: number } => {
306
- const label = options?.label ?? 'applyEvents'
263
+ const label = options?.label ?? 'mutate'
307
264
  const skipRefresh = options?.skipRefresh ?? false
308
265
 
309
- const applyEventsSpan = otel.trace.getSpan(this.otel.applyEventsSpanContext)!
310
- applyEventsSpan.addEvent('applyEvents')
266
+ const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
267
+ mutationsSpan.addEvent('mutate')
268
+
269
+ // console.debug('LiveStore.mutate', { skipRefresh, events: [...events] })
311
270
 
312
- // console.log('applyEvents', { skipRefresh, events: [...events] })
313
271
  return this.otel.tracer.startActiveSpan(
314
- 'LiveStore:applyEvents',
315
- { attributes: { 'livestore.applyEventsLabel': label } },
316
- this.otel.applyEventsSpanContext,
272
+ 'LiveStore:mutate',
273
+ { attributes: { 'livestore.mutateLabel': label } },
274
+ this.otel.mutationsSpanContext,
317
275
  (span) => {
318
276
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
319
277
 
@@ -322,29 +280,31 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
322
280
 
323
281
  this.otel.tracer.startActiveSpan(
324
282
  'LiveStore:processWrites',
325
- { attributes: { 'livestore.applyEventsLabel': label } },
283
+ { attributes: { 'livestore.mutateLabel': label } },
326
284
  otel.trace.setSpan(otel.context.active(), span),
327
285
  (span) => {
328
286
  try {
329
287
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
330
288
 
331
- // TODO: what to do about storage transaction here?
332
- this.inMemoryDB.txn(() => {
333
- for (const event of events) {
289
+ const applyMutations = () => {
290
+ for (const mutationEvent of mutationsEvents) {
334
291
  try {
335
- const { writeTables: writeTablesForEvent } = this.applyEventWithoutRefresh(
336
- event.eventType,
337
- event.args,
338
- otelContext,
339
- )
292
+ const { writeTables: writeTablesForEvent } = this.mutateWithoutRefresh(mutationEvent, otelContext)
340
293
  for (const tableName of writeTablesForEvent) {
341
294
  writeTables.add(tableName)
342
295
  }
343
296
  } catch (e: any) {
344
- console.error(e, event)
297
+ console.error(e, mutationEvent)
345
298
  }
346
299
  }
347
- })
300
+ }
301
+
302
+ if (mutationsEvents.length > 1) {
303
+ // TODO: what to do about storage transaction here?
304
+ this.inMemoryDB.txn(applyMutations)
305
+ } else {
306
+ applyMutations()
307
+ }
348
308
  } catch (e: any) {
349
309
  console.error(e)
350
310
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
@@ -362,10 +322,11 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
362
322
  }
363
323
 
364
324
  const debugRefreshReason = {
365
- _tag: 'applyEvents' as const,
366
- events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
367
- writeTables: [...writeTables],
325
+ _tag: 'mutate' as const,
326
+ mutations: mutationsEvents,
327
+ writeTables: Array.from(writeTables),
368
328
  }
329
+
369
330
  // Update all table refs together in a batch, to only trigger one reactive update
370
331
  this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
371
332
  } catch (e: any) {
@@ -380,7 +341,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
380
341
  }
381
342
 
382
343
  /**
383
- * This can be used in combination with `skipRefresh` when applying events.
344
+ * This can be used in combination with `skipRefresh` when applying mutations.
384
345
  * We might need a better solution for this. Let's see.
385
346
  */
386
347
  manualRefresh = (options?: { label?: string }) => {
@@ -388,7 +349,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
388
349
  this.otel.tracer.startActiveSpan(
389
350
  'LiveStore:manualRefresh',
390
351
  { attributes: { 'livestore.manualRefreshLabel': label } },
391
- this.otel.applyEventsSpanContext,
352
+ this.otel.mutationsSpanContext,
392
353
  (span) => {
393
354
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
394
355
  this.graph.runDeferredEffects({ otelContext })
@@ -398,98 +359,64 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
398
359
  }
399
360
 
400
361
  /**
401
- * Apply an event to the store.
362
+ * Apply a mutation to the store.
402
363
  * Returns the tables that were affected by the event.
403
364
  * This is an internal method that doesn't trigger a refresh;
404
365
  * the caller must refresh queries after calling this method.
405
366
  */
406
- private applyEventWithoutRefresh = (
407
- eventType: string,
408
- args: any = {},
367
+ private mutateWithoutRefresh = (
368
+ mutationEvent: MutationEvent.ForSchema<TSchema>,
409
369
  otelContext: otel.Context,
410
- ): { writeTables: ReadonlyArray<string>; durationMs: number } => {
370
+ ): { writeTables: ReadonlySet<string>; durationMs: number } => {
411
371
  return this.otel.tracer.startActiveSpan(
412
- 'LiveStore:applyEventWithoutRefresh',
372
+ 'LiveStore:mutatetWithoutRefresh',
413
373
  {
414
374
  attributes: {
415
- 'livestore.actionType': eventType,
416
- 'livestore.args': JSON.stringify(args, null, 2),
375
+ 'livestore.mutation': mutationEvent.mutation,
376
+ 'livestore.args': JSON.stringify(mutationEvent.args, null, 2),
417
377
  },
418
378
  },
419
379
  otelContext,
420
380
  (span) => {
421
381
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
422
382
 
423
- const actionDefinitions: { [key: string]: ActionDefinition } = {
424
- ...this.schema.actions,
425
-
426
- // Special LiveStore:defined actions
427
- 'livestore.UpdateComponentState': {
428
- statement: ({
429
- id,
430
- columnNames,
431
- tableName,
432
- }: {
433
- id?: string
434
- columnNames: ReadonlyArray<string>
435
- tableName: string
436
- }) => {
437
- const whereClause = id === undefined ? '' : `where id = '${id}'`
438
- const updateClause = columnNames.map((columnName) => `${columnName} = $${columnName}`).join(', ')
439
- const stmt = sql`update ${tableName} set ${updateClause} ${whereClause}`
440
-
441
- return {
442
- sql: stmt,
443
- writeTables: [tableName],
444
- }
445
- },
446
- prepareBindValues: ({ bindValues }) => bindValues ?? {},
447
- },
448
-
449
- 'livestore.RawSql': {
450
- statement: ({ sql, writeTables }: { sql: string; writeTables: ReadonlyArray<string> }) => ({
451
- sql,
452
- writeTables,
453
- argsAlreadyBound: false,
454
- }),
455
- prepareBindValues: ({ bindValues }) => bindValues ?? {},
456
- },
457
- }
383
+ const mutationDef =
384
+ this.schema.mutations.get(mutationEvent.mutation) ??
385
+ shouldNeverHappen(`Unknown mutation type: ${mutationEvent.mutation}`)
386
+
387
+ const statementRes =
388
+ typeof mutationDef.sql === 'function' ? mutationDef.sql(mutationEvent.args) : mutationDef.sql
458
389
 
459
- const actionDefinition = actionDefinitions[eventType] ?? shouldNeverHappen(`Unknown event type: ${eventType}`)
390
+ const statementSql = typeof statementRes === 'string' ? statementRes : statementRes.sql
391
+ const writeTables =
392
+ typeof statementRes === 'string'
393
+ ? this.inMemoryDB.getTablesUsed(statementSql)
394
+ : statementRes.writeTables ?? this.inMemoryDB.getTablesUsed(statementSql)
460
395
 
461
- // Generate a fresh ID for the event
462
- const eventWithId: LiveStoreEvent = { id: uuid(), type: eventType, args }
396
+ const bindValues =
397
+ typeof statementRes === 'string'
398
+ ? Schema.encodeUnknownSync(mutationDef.schema)(mutationEvent.args)
399
+ : statementRes.bindValues
463
400
 
464
- // Synchronously apply the event to the in-memory database
465
- // const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
466
- const { statement, bindValues } = eventToSql(eventWithId, actionDefinition)
467
401
  const { durationMs } = this.inMemoryDB.execute(
468
- statement.sql,
469
- prepareBindValues(bindValues, statement.sql),
470
- statement.writeTables,
471
- {
472
- otelContext,
473
- },
402
+ statementSql,
403
+ prepareBindValues(bindValues ?? {}, statementSql),
404
+ writeTables,
405
+ { otelContext },
474
406
  )
475
407
 
476
- // Asynchronously apply the event to a persistent storage (we're not awaiting this promise here)
408
+ // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
477
409
  if (this.storage !== undefined) {
478
- // this.storage.applyEvent(eventWithId, actionDefinition, span)
479
- this.storage.execute(statement.sql, prepareBindValues(bindValues, statement.sql), span)
410
+ const mutationEventEncoded = Schema.encodeUnknownSync(this.mutationArgsSchema)(mutationEvent)
411
+ this.storage.mutate(mutationEventEncoded, span)
480
412
  }
481
413
 
482
414
  // Uncomment to print a list of queries currently registered on the store
483
- // console.log(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
484
-
485
- // const statement =
486
- // typeof actionDefinition.statement === 'function'
487
- // ? actionDefinition.statement(args)
488
- // : actionDefinition.statement
415
+ // console.debug(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
489
416
 
490
417
  span.end()
491
418
 
492
- return { writeTables: statement.writeTables, durationMs }
419
+ return { writeTables, durationMs }
493
420
  },
494
421
  )
495
422
  }
@@ -497,12 +424,12 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
497
424
  /**
498
425
  * Directly execute a SQL query on the Store.
499
426
  * This should only be used for framework-internal purposes;
500
- * all app writes should go through applyEvent.
427
+ * all app writes should go through mutate.
501
428
  */
502
429
  execute = (
503
430
  query: string,
504
431
  params: ParamsObject = {},
505
- writeTables?: ReadonlyArray<string>,
432
+ writeTables?: ReadonlySet<string>,
506
433
  otelContext?: otel.Context,
507
434
  ) => {
508
435
  this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
@@ -523,10 +450,28 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
523
450
  label: `tableRef:${tableName}`,
524
451
  meta: { liveStoreRefType: 'table' },
525
452
  })
453
+
454
+ __devDownloadDb = () => {
455
+ const data = this.inMemoryDB.export()
456
+ downloadBlob(data, `livestore-${Date.now()}.db`)
457
+ }
458
+
459
+ __devDownloadMutationLogDb = async () => {
460
+ const data = await this.storage?.getMutationLogData()
461
+ if (data !== undefined) {
462
+ downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
463
+ }
464
+ }
465
+
466
+ // TODO allow for graceful store reset without requiring a full page reload
467
+ dangerouslyResetStorage = () => this.storage?.dangerouslyReset()
526
468
  }
527
469
 
528
470
  /** Create a new LiveStore Store */
529
- export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
471
+ export const createStore = async <
472
+ TGraphQLContext extends BaseGraphQLContext,
473
+ TSchema extends LiveStoreSchema = LiveStoreSchema,
474
+ >({
530
475
  schema,
531
476
  loadStorage,
532
477
  graphQLOptions,
@@ -536,7 +481,7 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
536
481
  boot,
537
482
  dbGraph,
538
483
  }: {
539
- schema: LiveStoreSchema
484
+ schema: TSchema
540
485
  loadStorage: () => StorageInit | Promise<StorageInit>
541
486
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
542
487
  otelTracer?: otel.Tracer
@@ -544,7 +489,7 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
544
489
  sqlite3: Sqlite.Sqlite3Static
545
490
  boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
546
491
  dbGraph?: DbGraph
547
- }): Promise<Store<TGraphQLContext>> => {
492
+ }): Promise<Store<TGraphQLContext, TSchema>> => {
548
493
  return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
549
494
  try {
550
495
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -622,7 +567,7 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
622
567
  // TODO: we can't apply the schema at this point, we've already loaded persisted data!
623
568
  // Think about what to do about this case.
624
569
  // await applySchema(db, schema)
625
- return Store.createStore<TGraphQLContext>(
570
+ return Store.createStore<TGraphQLContext, TSchema>(
626
571
  { db, dbProxy, schema, storage, graphQLOptions, otelTracer, otelRootSpanContext, dbGraph },
627
572
  span,
628
573
  )
@@ -632,21 +577,6 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
632
577
  })
633
578
  }
634
579
 
635
- const eventToSql = (
636
- event: LiveStoreEvent,
637
- eventDefinition: ActionDefinition,
638
- ): { statement: SQLWriteStatement; bindValues: ParamsObject } => {
639
- const statement =
640
- typeof eventDefinition.statement === 'function' ? eventDefinition.statement(event.args) : eventDefinition.statement
641
-
642
- const prepareBindValues = eventDefinition.prepareBindValues ?? identity
643
-
644
- const bindValues =
645
- typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound ? {} : prepareBindValues(event.args)
646
-
647
- return { statement, bindValues }
648
- }
649
-
650
580
  class ReferenceCountedSet<T> {
651
581
  private map: Map<T, number>
652
582
 
@@ -0,0 +1,23 @@
1
+ export const downloadBlob = (
2
+ data: Uint8Array | Blob | string,
3
+ fileName: string,
4
+ mimeType = 'application/octet-stream',
5
+ ) => {
6
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mimeType })
7
+
8
+ const url = window.URL.createObjectURL(blob)
9
+
10
+ downloadURL(url, fileName)
11
+
12
+ setTimeout(() => window.URL.revokeObjectURL(url), 1000)
13
+ }
14
+
15
+ export const downloadURL = (data: string, fileName: string) => {
16
+ const a = document.createElement('a')
17
+ a.href = data
18
+ a.download = fileName
19
+ document.body.append(a)
20
+ a.style.display = 'none'
21
+ a.click()
22
+ a.remove()
23
+ }
package/src/utils/util.ts CHANGED
@@ -69,3 +69,7 @@ export const objectToString = (error: any): string => {
69
69
  }
70
70
 
71
71
  export const isPromise = (value: any): value is Promise<unknown> => typeof value?.then === 'function'
72
+
73
+ export const isReadonlyArray = <I, T>(value: ReadonlyArray<I> | T): value is ReadonlyArray<I> => Array.isArray(value)
74
+
75
+ export const isIterable = <T>(value: any): value is Iterable<T> => typeof value?.[Symbol.iterator] === 'function'
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=mutations.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mutations.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mutations.test.ts"],"names":[],"mappings":""}
@@ -1,40 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { makeMutations } from '../mutations.js';
3
- import { schema } from './react/fixture.js';
4
- describe('mutations', () => {
5
- const mutations = makeMutations(schema);
6
- test('basic', () => {
7
- expect(mutations.todos.insert({ id: 't1', completed: true, text: 'Task 1' })).toMatchInlineSnapshot(`
8
- {
9
- "args": {
10
- "bindValues": {
11
- "completed": 1,
12
- "id": "t1",
13
- "text": "Task 1",
14
- },
15
- "sql": "INSERT INTO todos (id, text, completed) VALUES ($id, $text, $completed)",
16
- "writeTables": Set {
17
- "todos",
18
- },
19
- },
20
- "eventType": "livestore.RawSql",
21
- }
22
- `);
23
- expect(mutations.todos.update({ where: { id: 't1' }, values: { text: 'Task 1 - fixed' } })).toMatchInlineSnapshot(`
24
- {
25
- "args": {
26
- "bindValues": {
27
- "update_text": "Task 1 - fixed",
28
- "where_id": "t1",
29
- },
30
- "sql": "UPDATE todos SET text = $update_text WHERE id = $where_id",
31
- "writeTables": Set {
32
- "todos",
33
- },
34
- },
35
- "eventType": "livestore.RawSql",
36
- }
37
- `);
38
- });
39
- });
40
- //# sourceMappingURL=mutations.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mutations.test.js","sourceRoot":"","sources":["../../src/__tests__/mutations.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAE3C,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IAEvC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;QACjB,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC;;;;;;;;;;;;;;;KAenG,CAAC,CAAA;QAEF,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC;;;;;;;;;;;;;;KAcjH,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"useQuery.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/react/useQuery.test.tsx"],"names":[],"mappings":""}