on-zero 0.6.14 → 0.6.16

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 (163) hide show
  1. package/dist/cjs/asyncTasks.cjs +51 -0
  2. package/dist/cjs/asyncTasks.native.js +84 -0
  3. package/dist/cjs/asyncTasks.native.js.map +1 -0
  4. package/dist/cjs/cli.cjs +8 -1
  5. package/dist/cjs/cli.native.js +8 -1
  6. package/dist/cjs/cli.native.js.map +1 -1
  7. package/dist/cjs/createSchemaFromDrizzle.cjs +4 -4
  8. package/dist/cjs/createSchemaFromDrizzle.native.js +4 -4
  9. package/dist/cjs/createSchemaFromDrizzle.native.js.map +1 -1
  10. package/dist/cjs/createZeroDirectServer.cjs +269 -0
  11. package/dist/cjs/createZeroDirectServer.native.js +287 -0
  12. package/dist/cjs/createZeroDirectServer.native.js.map +1 -0
  13. package/dist/cjs/createZeroSQLiteServer.cjs +8 -277
  14. package/dist/cjs/createZeroSQLiteServer.native.js +8 -390
  15. package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -1
  16. package/dist/cjs/createZeroSQLiteServer.test.cjs +36 -9
  17. package/dist/cjs/createZeroSQLiteServer.test.native.js +37 -9
  18. package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -1
  19. package/dist/cjs/createZeroServer.test.cjs +79 -1
  20. package/dist/cjs/createZeroServer.test.native.js +85 -1
  21. package/dist/cjs/createZeroServer.test.native.js.map +1 -1
  22. package/dist/cjs/createZeroServerCore.cjs +28 -208
  23. package/dist/cjs/createZeroServerCore.native.js +33 -218
  24. package/dist/cjs/createZeroServerCore.native.js.map +1 -1
  25. package/dist/cjs/generate.cjs +15 -5
  26. package/dist/cjs/generate.native.js +15 -5
  27. package/dist/cjs/generate.native.js.map +1 -1
  28. package/dist/cjs/generate.test.cjs +55 -0
  29. package/dist/cjs/generate.test.native.js +55 -0
  30. package/dist/cjs/generate.test.native.js.map +1 -1
  31. package/dist/cjs/helpers/createMutators.cjs +2 -2
  32. package/dist/cjs/helpers/createMutators.native.js +2 -2
  33. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  34. package/dist/cjs/index.cjs +1 -0
  35. package/dist/cjs/index.native.js +1 -0
  36. package/dist/cjs/index.native.js.map +1 -1
  37. package/dist/cjs/server.cjs +2 -1
  38. package/dist/cjs/sqliteRuntime.cjs +318 -0
  39. package/dist/cjs/sqliteRuntime.native.js +438 -0
  40. package/dist/cjs/sqliteRuntime.native.js.map +1 -0
  41. package/dist/cjs/syncHost.cjs +26 -12
  42. package/dist/cjs/syncHost.native.js +32 -16
  43. package/dist/cjs/syncHost.native.js.map +1 -1
  44. package/dist/cjs/syncHost.test.cjs +15 -4
  45. package/dist/cjs/syncHost.test.native.js +16 -4
  46. package/dist/cjs/syncHost.test.native.js.map +1 -1
  47. package/dist/cjs/vite-plugin.cjs +2 -1
  48. package/dist/cjs/vite-plugin.native.js +2 -1
  49. package/dist/cjs/vite-plugin.native.js.map +1 -1
  50. package/dist/esm/asyncTasks.mjs +26 -0
  51. package/dist/esm/asyncTasks.mjs.map +1 -0
  52. package/dist/esm/asyncTasks.native.js +56 -0
  53. package/dist/esm/asyncTasks.native.js.map +1 -0
  54. package/dist/esm/cli.mjs +8 -1
  55. package/dist/esm/cli.mjs.map +1 -1
  56. package/dist/esm/cli.native.js +8 -1
  57. package/dist/esm/cli.native.js.map +1 -1
  58. package/dist/esm/createSchemaFromDrizzle.mjs +2 -2
  59. package/dist/esm/createSchemaFromDrizzle.native.js +2 -2
  60. package/dist/esm/createZeroDirectServer.mjs +241 -0
  61. package/dist/esm/createZeroDirectServer.mjs.map +1 -0
  62. package/dist/esm/createZeroDirectServer.native.js +256 -0
  63. package/dist/esm/createZeroDirectServer.native.js.map +1 -0
  64. package/dist/esm/createZeroSQLiteServer.mjs +3 -272
  65. package/dist/esm/createZeroSQLiteServer.mjs.map +1 -1
  66. package/dist/esm/createZeroSQLiteServer.native.js +3 -385
  67. package/dist/esm/createZeroSQLiteServer.native.js.map +1 -1
  68. package/dist/esm/createZeroSQLiteServer.test.mjs +36 -9
  69. package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -1
  70. package/dist/esm/createZeroSQLiteServer.test.native.js +37 -9
  71. package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -1
  72. package/dist/esm/createZeroServer.test.mjs +80 -2
  73. package/dist/esm/createZeroServer.test.mjs.map +1 -1
  74. package/dist/esm/createZeroServer.test.native.js +86 -2
  75. package/dist/esm/createZeroServer.test.native.js.map +1 -1
  76. package/dist/esm/createZeroServerCore.mjs +26 -205
  77. package/dist/esm/createZeroServerCore.mjs.map +1 -1
  78. package/dist/esm/createZeroServerCore.native.js +32 -216
  79. package/dist/esm/createZeroServerCore.native.js.map +1 -1
  80. package/dist/esm/generate.mjs +15 -5
  81. package/dist/esm/generate.mjs.map +1 -1
  82. package/dist/esm/generate.native.js +15 -5
  83. package/dist/esm/generate.native.js.map +1 -1
  84. package/dist/esm/generate.test.mjs +55 -1
  85. package/dist/esm/generate.test.mjs.map +1 -1
  86. package/dist/esm/generate.test.native.js +55 -1
  87. package/dist/esm/generate.test.native.js.map +1 -1
  88. package/dist/esm/helpers/createMutators.mjs +2 -2
  89. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  90. package/dist/esm/helpers/createMutators.native.js +2 -2
  91. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  92. package/dist/esm/index.js +1 -0
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/index.mjs +1 -0
  95. package/dist/esm/index.mjs.map +1 -1
  96. package/dist/esm/index.native.js +1 -0
  97. package/dist/esm/index.native.js.map +1 -1
  98. package/dist/esm/server.mjs +1 -0
  99. package/dist/esm/server.mjs.map +1 -1
  100. package/dist/esm/sqliteRuntime.mjs +289 -0
  101. package/dist/esm/sqliteRuntime.mjs.map +1 -0
  102. package/dist/esm/sqliteRuntime.native.js +406 -0
  103. package/dist/esm/sqliteRuntime.native.js.map +1 -0
  104. package/dist/esm/syncHost.mjs +24 -10
  105. package/dist/esm/syncHost.mjs.map +1 -1
  106. package/dist/esm/syncHost.native.js +30 -14
  107. package/dist/esm/syncHost.native.js.map +1 -1
  108. package/dist/esm/syncHost.test.mjs +15 -4
  109. package/dist/esm/syncHost.test.mjs.map +1 -1
  110. package/dist/esm/syncHost.test.native.js +16 -4
  111. package/dist/esm/syncHost.test.native.js.map +1 -1
  112. package/dist/esm/vite-plugin.mjs +2 -1
  113. package/dist/esm/vite-plugin.mjs.map +1 -1
  114. package/dist/esm/vite-plugin.native.js +2 -1
  115. package/dist/esm/vite-plugin.native.js.map +1 -1
  116. package/lint/ssr-guards.js +8 -7
  117. package/package.json +5 -6
  118. package/readme.md +13 -4
  119. package/src/asyncTasks.ts +33 -0
  120. package/src/cli.ts +7 -1
  121. package/src/createSchemaFromDrizzle.ts +2 -2
  122. package/src/createZeroDirectServer.ts +411 -0
  123. package/src/createZeroSQLiteServer.test.ts +28 -12
  124. package/src/createZeroSQLiteServer.ts +14 -481
  125. package/src/createZeroServer.test.ts +86 -2
  126. package/src/createZeroServerCore.ts +39 -343
  127. package/src/generate.test.ts +75 -1
  128. package/src/generate.ts +30 -4
  129. package/src/helpers/createMutators.ts +3 -3
  130. package/src/helpers/mutatorContext.ts +1 -1
  131. package/src/index.ts +1 -0
  132. package/src/server.ts +2 -0
  133. package/src/sqliteRuntime.ts +512 -0
  134. package/src/syncHost.test.ts +12 -5
  135. package/src/syncHost.ts +55 -16
  136. package/src/types.ts +21 -3
  137. package/src/vite-plugin.ts +1 -0
  138. package/test-fixtures/server-return-types.ts +20 -2
  139. package/types/asyncTasks.d.ts +7 -0
  140. package/types/asyncTasks.d.ts.map +1 -0
  141. package/types/createSchemaFromDrizzle.d.ts +2 -2
  142. package/types/createSchemaFromDrizzle.d.ts.map +1 -1
  143. package/types/createZeroDirectServer.d.ts +80 -0
  144. package/types/createZeroDirectServer.d.ts.map +1 -0
  145. package/types/createZeroSQLiteServer.d.ts +5 -30
  146. package/types/createZeroSQLiteServer.d.ts.map +1 -1
  147. package/types/createZeroServerCore.d.ts +30 -87
  148. package/types/createZeroServerCore.d.ts.map +1 -1
  149. package/types/generate.d.ts +2 -0
  150. package/types/generate.d.ts.map +1 -1
  151. package/types/helpers/createMutators.d.ts +3 -3
  152. package/types/helpers/createMutators.d.ts.map +1 -1
  153. package/types/index.d.ts +1 -0
  154. package/types/index.d.ts.map +1 -1
  155. package/types/server.d.ts +2 -0
  156. package/types/server.d.ts.map +1 -1
  157. package/types/sqliteRuntime.d.ts +71 -0
  158. package/types/sqliteRuntime.d.ts.map +1 -0
  159. package/types/syncHost.d.ts +10 -19
  160. package/types/syncHost.d.ts.map +1 -1
  161. package/types/types.d.ts +18 -2
  162. package/types/types.d.ts.map +1 -1
  163. package/types/vite-plugin.d.ts.map +1 -1
@@ -1,60 +1,17 @@
1
- import { createBuilder } from '@rocicorp/zero'
2
- import { asQueryInternals } from '@rocicorp/zero/bindings'
3
-
1
+ import { createZeroServerCore } from './createZeroServerCore'
4
2
  import {
5
- createZeroServerCore,
6
- type PersistAlreadyProcessedArgs,
7
- type ZeroServer,
8
- type ZeroServerCommonOptions,
9
- type ZeroServerDatabase,
10
- } from './createZeroServerCore'
11
-
12
- import type { GenericModels, Transaction } from './types'
13
- import type { AST, Schema as ZeroSchema } from '@rocicorp/zero'
14
- import type {
15
- TransactionProviderHooks,
16
- TransactionProviderInput,
17
- } from '@rocicorp/zero/server'
18
-
19
- export type SQLiteTransactionExecutor = {
20
- exec(sql: string, params?: readonly unknown[]): void | Promise<void>
21
- query<Row extends Record<string, unknown> = Record<string, unknown>>(
22
- sql: string,
23
- params?: readonly unknown[],
24
- ): Row[] | Promise<Row[]>
25
- queryAst<Result = unknown>(
26
- ast: AST,
27
- format: SQLiteQueryFormat,
28
- queryName?: string,
29
- ): Result | Promise<Result>
30
- }
31
-
32
- export type SQLiteQueryFormat = {
33
- relationships: Record<string, SQLiteQueryFormat>
34
- singular: boolean
35
- }
36
-
37
- export type SQLiteTransactionProvider = <R>(
38
- callback: (tx: SQLiteTransactionExecutor) => Promise<R>,
39
- ) => Promise<R>
40
-
41
- type SQLiteColumn = ZeroSchema['tables'][string]['columns'][string]
42
-
43
- export type SQLiteValueContext = {
44
- column: SQLiteColumn
45
- columnName: string
46
- tableName: string
47
- }
48
-
49
- export type ZeroSQLiteTransactionOptions<Schema extends ZeroSchema> = {
50
- schema: Schema
51
- encodeValue?: (value: unknown, context: SQLiteValueContext) => unknown
52
- }
53
-
54
- export type SQLiteLedgerTables = {
55
- clients: string
56
- mutations: string
57
- }
3
+ createSQLiteDatabase,
4
+ defaultLedgerTables,
5
+ encodeSQLiteValue,
6
+ persistAlreadyProcessedMutationResults,
7
+ type SQLiteLedgerTables,
8
+ type SQLiteTransactionProvider,
9
+ type SQLiteValueContext,
10
+ } from './sqliteRuntime'
11
+
12
+ import type { ZeroServer, ZeroServerCommonOptions } from './createZeroServerCore'
13
+ import type { GenericModels } from './types'
14
+ import type { Schema as ZeroSchema } from '@rocicorp/zero'
58
15
 
59
16
  export type CreateZeroSQLiteServerOptions<
60
17
  Schema extends ZeroSchema,
@@ -96,428 +53,4 @@ export function createZeroSQLiteServer<
96
53
  })
97
54
  }
98
55
 
99
- type SQLiteTransactionOptions<Schema extends ZeroSchema> = {
100
- encodeValue: (value: unknown, context: SQLiteValueContext) => unknown
101
- schema: Schema
102
- }
103
-
104
- type SQLiteDatabaseOptions<Schema extends ZeroSchema> =
105
- SQLiteTransactionOptions<Schema> & {
106
- initializedLedgers: Set<string>
107
- ledgerTables: (upstreamSchema: string) => SQLiteLedgerTables
108
- transactionProvider: SQLiteTransactionProvider
109
- }
110
-
111
- function createSQLiteDatabase<Schema extends ZeroSchema>(
112
- options: SQLiteDatabaseOptions<Schema>,
113
- ): ZeroServerDatabase {
114
- return {
115
- async transaction<R>(
116
- callback: (
117
- tx: Transaction,
118
- transactionHooks: TransactionProviderHooks,
119
- ) => R | Promise<R>,
120
- input?: TransactionProviderInput,
121
- ) {
122
- let committedLedgers = new Set<string>()
123
- const value = await options.transactionProvider(async (executor) => {
124
- const attemptLedgers = new Set<string>()
125
- const ensureLedger = async (upstreamSchema: string) => {
126
- const tables = options.ledgerTables(upstreamSchema)
127
- const key = ledgerKey(tables)
128
- if (options.initializedLedgers.has(key) || attemptLedgers.has(key)) {
129
- return
130
- }
131
- await createLedgerTables(executor, tables)
132
- attemptLedgers.add(key)
133
- }
134
- const transaction = createZeroSQLiteTransaction(options, executor, input)
135
- const result = await callback(
136
- transaction,
137
- createLedgerHooks(options, executor, ensureLedger, input),
138
- )
139
- committedLedgers = attemptLedgers
140
- return result
141
- })
142
- for (const key of committedLedgers) options.initializedLedgers.add(key)
143
- return value
144
- },
145
- }
146
- }
147
-
148
- export function createZeroSQLiteTransaction<Schema extends ZeroSchema>(
149
- options: ZeroSQLiteTransactionOptions<Schema>,
150
- executor: SQLiteTransactionExecutor,
151
- input?: TransactionProviderInput,
152
- ): Transaction {
153
- const transactionOptions = {
154
- encodeValue: options.encodeValue ?? encodeSQLiteValue,
155
- schema: options.schema,
156
- }
157
- const clientID = input?.clientID ?? ''
158
- const mutationID = input?.mutationID ?? 0
159
- const mutate = new Proxy(
160
- {},
161
- {
162
- get(_target, tableKey) {
163
- const tableName = String(tableKey)
164
- if (!options.schema.tables[tableName]) {
165
- throw new Error(`unknown SQLite mutation table ${tableName}`)
166
- }
167
- return new Proxy(
168
- {},
169
- {
170
- get(_innerTarget, kindKey) {
171
- const kind = String(kindKey)
172
- if (!isCrudKind(kind)) return
173
- return (value: unknown) =>
174
- executeCrud(transactionOptions, executor, tableName, kind, value)
175
- },
176
- },
177
- )
178
- },
179
- },
180
- )
181
-
182
- const runQuery = (
183
- ast: AST,
184
- format: SQLiteQueryFormat,
185
- queryName?: string,
186
- ): Promise<unknown> => Promise.resolve(executor.queryAst(ast, format, queryName))
187
- const dbTransaction = {
188
- wrappedTransaction: executor,
189
- query: (sql: string, args: unknown[]) => executor.query(sql, args),
190
- runQuery,
191
- }
192
-
193
- return {
194
- clientID,
195
- dbTransaction,
196
- location: 'server',
197
- mutate,
198
- mutationID,
199
- query: transactionOptions.schema.enableLegacyQueries
200
- ? createLegacyQueryBuilder(transactionOptions.schema, (query) => {
201
- const internals = asQueryInternals(query as never)
202
- return runQuery(internals.ast, internals.format, internals.customQueryID?.name)
203
- })
204
- : undefined,
205
- reason: 'authoritative',
206
- async run(query: unknown) {
207
- const internals = asQueryInternals(query as never)
208
- return runQuery(internals.ast, internals.format, internals.customQueryID?.name)
209
- },
210
- } as unknown as Transaction
211
- }
212
-
213
- type CrudKind = 'delete' | 'insert' | 'update' | 'upsert'
214
-
215
- function isCrudKind(value: string): value is CrudKind {
216
- return (
217
- value === 'delete' || value === 'insert' || value === 'update' || value === 'upsert'
218
- )
219
- }
220
-
221
- async function executeCrud<Schema extends ZeroSchema>(
222
- options: SQLiteTransactionOptions<Schema>,
223
- executor: SQLiteTransactionExecutor,
224
- tableName: string,
225
- kind: CrudKind,
226
- raw: unknown,
227
- ) {
228
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
229
- throw new Error(`${tableName}.${kind} expects an object`)
230
- }
231
-
232
- const table = options.schema.tables[tableName]!
233
- const values = Object.fromEntries(
234
- Object.entries(raw as Record<string, unknown>).filter(
235
- ([, value]) => value !== undefined,
236
- ),
237
- )
238
- for (const columnName of Object.keys(values)) {
239
- if (!table.columns[columnName]) {
240
- throw new Error(`unknown SQLite column ${tableName}.${columnName}`)
241
- }
242
- }
243
-
244
- const primaryValues = table.primaryKey.map((columnName) => {
245
- if (!(columnName in values)) {
246
- throw new Error(`${tableName}.${kind} is missing primary key ${columnName}`)
247
- }
248
- return encodeColumn(options, tableName, columnName, values[columnName])
249
- })
250
- const primaryWhere = table.primaryKey
251
- .map((columnName) => `${quoteSQLiteIdentifier(serverColumn(table, columnName))} = ?`)
252
- .join(' AND ')
253
- const serverTableName = quoteSQLiteIdentifier(table.serverName ?? table.name)
254
-
255
- if (kind === 'delete') {
256
- await executor.exec(
257
- `DELETE FROM ${serverTableName} WHERE ${primaryWhere}`,
258
- primaryValues,
259
- )
260
- return
261
- }
262
-
263
- const columnNames = Object.keys(values)
264
- if (columnNames.length === 0) {
265
- throw new Error(`${tableName}.${kind} has no values`)
266
- }
267
-
268
- const serverColumns = columnNames.map((columnName) =>
269
- quoteSQLiteIdentifier(serverColumn(table, columnName)),
270
- )
271
- const params = columnNames.map((columnName) =>
272
- encodeColumn(options, tableName, columnName, values[columnName]),
273
- )
274
- const insert = `INSERT INTO ${serverTableName} (${serverColumns.join(
275
- ', ',
276
- )}) VALUES (${columnNames.map(() => '?').join(', ')})`
277
-
278
- if (kind === 'insert') {
279
- await executor.exec(insert, params)
280
- return
281
- }
282
-
283
- if (kind === 'upsert') {
284
- const mutableColumns = columnNames.filter(
285
- (columnName) => !table.primaryKey.includes(columnName),
286
- )
287
- const conflictColumns = table.primaryKey
288
- .map((columnName) => quoteSQLiteIdentifier(serverColumn(table, columnName)))
289
- .join(', ')
290
- const conflictAction =
291
- mutableColumns.length === 0
292
- ? 'DO NOTHING'
293
- : `DO UPDATE SET ${mutableColumns
294
- .map((columnName) => {
295
- const column = quoteSQLiteIdentifier(serverColumn(table, columnName))
296
- return `${column} = excluded.${column}`
297
- })
298
- .join(', ')}`
299
- await executor.exec(
300
- `${insert} ON CONFLICT (${conflictColumns}) ${conflictAction}`,
301
- params,
302
- )
303
- return
304
- }
305
-
306
- const mutableColumns = columnNames.filter(
307
- (columnName) => !table.primaryKey.includes(columnName),
308
- )
309
- if (mutableColumns.length === 0) return
310
- await executor.exec(
311
- `UPDATE ${serverTableName} SET ${mutableColumns
312
- .map(
313
- (columnName) => `${quoteSQLiteIdentifier(serverColumn(table, columnName))} = ?`,
314
- )
315
- .join(', ')} WHERE ${primaryWhere}`,
316
- [
317
- ...mutableColumns.map((columnName) =>
318
- encodeColumn(options, tableName, columnName, values[columnName]),
319
- ),
320
- ...primaryValues,
321
- ],
322
- )
323
- }
324
-
325
- function serverColumn(table: ZeroSchema['tables'][string], columnName: string) {
326
- const column = table.columns[columnName]
327
- if (!column) throw new Error(`unknown SQLite column ${table.name}.${columnName}`)
328
- return column.serverName ?? columnName
329
- }
330
-
331
- function encodeColumn<Schema extends ZeroSchema>(
332
- options: SQLiteTransactionOptions<Schema>,
333
- tableName: string,
334
- columnName: string,
335
- value: unknown,
336
- ) {
337
- return options.encodeValue(value, {
338
- column: options.schema.tables[tableName]!.columns[columnName]!,
339
- columnName,
340
- tableName,
341
- })
342
- }
343
-
344
- function createLegacyQueryBuilder<Schema extends ZeroSchema>(
345
- schema: Schema,
346
- run: (query: unknown) => Promise<unknown>,
347
- ) {
348
- const builder = createBuilder(schema)
349
- return new Proxy(builder, {
350
- get(target, property) {
351
- return makeRunnableQuery(Reflect.get(target, property, target), run)
352
- },
353
- })
354
- }
355
-
356
- function makeRunnableQuery(
357
- query: unknown,
358
- run: (query: unknown) => Promise<unknown>,
359
- ): unknown {
360
- if (!isQueryBuilder(query)) return query
361
- return new Proxy(query as object, {
362
- get(target, property) {
363
- if (property === 'run') return () => run(target)
364
- if (property === 'materialize' || property === 'preload') {
365
- return () => {
366
- throw new Error(`${String(property)}() is not supported in server transactions`)
367
- }
368
- }
369
- const value = Reflect.get(target, property, target)
370
- if (typeof value !== 'function') return value
371
- return (...args: unknown[]) =>
372
- makeRunnableQuery(Reflect.apply(value, target, args), run)
373
- },
374
- })
375
- }
376
-
377
- function isQueryBuilder(value: unknown) {
378
- if (!value || typeof value !== 'object') return false
379
- try {
380
- asQueryInternals(value as never)
381
- return true
382
- } catch {
383
- return false
384
- }
385
- }
386
-
387
- function createLedgerHooks<Schema extends ZeroSchema>(
388
- options: SQLiteDatabaseOptions<Schema>,
389
- executor: SQLiteTransactionExecutor,
390
- ensureLedger: (upstreamSchema: string) => Promise<void>,
391
- input?: TransactionProviderInput,
392
- ): TransactionProviderHooks {
393
- const upstreamSchema = input?.upstreamSchema ?? ''
394
- const clientGroupID = input?.clientGroupID ?? ''
395
- const clientID = input?.clientID ?? ''
396
- const tables = options.ledgerTables(upstreamSchema)
397
- const clientsTable = quoteSQLiteIdentifier(tables.clients)
398
- const mutationsTable = quoteSQLiteIdentifier(tables.mutations)
399
-
400
- return {
401
- async updateClientMutationID() {
402
- await ensureLedger(upstreamSchema)
403
- const rows = await executor.query<{ lastMutationID: number | bigint }>(
404
- `INSERT INTO ${clientsTable}
405
- (clientGroupID, clientID, lastMutationID)
406
- VALUES (?, ?, 1)
407
- ON CONFLICT (clientGroupID, clientID)
408
- DO UPDATE SET lastMutationID = lastMutationID + 1
409
- RETURNING lastMutationID`,
410
- [clientGroupID, clientID],
411
- )
412
- const lastMutationID = rows[0]?.lastMutationID
413
- if (typeof lastMutationID !== 'number' && typeof lastMutationID !== 'bigint') {
414
- throw new Error('SQLite mutation ledger returned no lastMutationID')
415
- }
416
- return { lastMutationID }
417
- },
418
- async writeMutationResult(result) {
419
- await ensureLedger(upstreamSchema)
420
- await executor.exec(
421
- `INSERT INTO ${mutationsTable}
422
- (clientGroupID, clientID, mutationID, result)
423
- VALUES (?, ?, ?, ?)`,
424
- [clientGroupID, result.id.clientID, result.id.id, JSON.stringify(result.result)],
425
- )
426
- },
427
- async deleteMutationResults(args) {
428
- await ensureLedger(upstreamSchema)
429
- if ('type' in args && args.type === 'bulk') {
430
- if (args.clientIDs.length === 0) return
431
- await executor.exec(
432
- `DELETE FROM ${mutationsTable}
433
- WHERE clientGroupID = ?
434
- AND clientID IN (${args.clientIDs.map(() => '?').join(', ')})`,
435
- [args.clientGroupID, ...args.clientIDs],
436
- )
437
- return
438
- }
439
- await executor.exec(
440
- `DELETE FROM ${mutationsTable}
441
- WHERE clientGroupID = ?
442
- AND clientID = ?
443
- AND mutationID <= ?`,
444
- [args.clientGroupID, args.clientID, args.upToMutationID],
445
- )
446
- },
447
- }
448
- }
449
-
450
- async function persistAlreadyProcessedMutationResults<Schema extends ZeroSchema>(
451
- options: SQLiteDatabaseOptions<Schema>,
452
- { clientGroupID, mutations, upstreamSchema }: PersistAlreadyProcessedArgs,
453
- ) {
454
- const tables = options.ledgerTables(upstreamSchema)
455
- const table = quoteSQLiteIdentifier(tables.mutations)
456
- const key = ledgerKey(tables)
457
- await options.transactionProvider(async (executor) => {
458
- if (!options.initializedLedgers.has(key)) {
459
- await createLedgerTables(executor, tables)
460
- }
461
- for (const mutation of mutations) {
462
- await executor.exec(
463
- `INSERT INTO ${table}
464
- (clientGroupID, clientID, mutationID, result)
465
- VALUES (?, ?, ?, ?)
466
- ON CONFLICT (clientGroupID, clientID, mutationID) DO NOTHING`,
467
- [
468
- clientGroupID,
469
- mutation.id.clientID,
470
- mutation.id.id,
471
- JSON.stringify(mutation.result),
472
- ],
473
- )
474
- }
475
- })
476
- options.initializedLedgers.add(key)
477
- }
478
-
479
- async function createLedgerTables(
480
- executor: SQLiteTransactionExecutor,
481
- tables: SQLiteLedgerTables,
482
- ) {
483
- await executor.exec(
484
- `CREATE TABLE IF NOT EXISTS ${quoteSQLiteIdentifier(tables.clients)} (
485
- clientGroupID TEXT NOT NULL,
486
- clientID TEXT NOT NULL,
487
- lastMutationID INTEGER NOT NULL,
488
- PRIMARY KEY (clientGroupID, clientID)
489
- )`,
490
- )
491
- await executor.exec(
492
- `CREATE TABLE IF NOT EXISTS ${quoteSQLiteIdentifier(tables.mutations)} (
493
- clientGroupID TEXT NOT NULL,
494
- clientID TEXT NOT NULL,
495
- mutationID INTEGER NOT NULL,
496
- result TEXT NOT NULL,
497
- PRIMARY KEY (clientGroupID, clientID, mutationID)
498
- )`,
499
- )
500
- }
501
-
502
- function ledgerKey(tables: SQLiteLedgerTables) {
503
- return `${tables.clients}\0${tables.mutations}`
504
- }
505
-
506
- function defaultLedgerTables(upstreamSchema: string): SQLiteLedgerTables {
507
- const prefix = upstreamSchema ? `${upstreamSchema}_` : ''
508
- return {
509
- clients: `${prefix}clients`,
510
- mutations: `${prefix}mutations`,
511
- }
512
- }
513
-
514
- function encodeSQLiteValue(value: unknown, { column }: SQLiteValueContext) {
515
- if (value === null) return null
516
- if (column.type === 'boolean') return value ? 1 : 0
517
- if (column.type === 'json') return JSON.stringify(value)
518
- return value
519
- }
520
-
521
- function quoteSQLiteIdentifier(value: string) {
522
- return `"${value.replaceAll('"', '""')}"`
523
- }
56
+ export * from './sqliteRuntime'
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test, vi } from 'vitest'
2
2
 
3
3
  import { createZeroServer } from './createZeroServer'
4
4
  import { zeroHttpFixtureSchema } from './httpPull/fixtureSchema'
@@ -103,7 +103,7 @@ describe('createZeroServer mutation recovery', () => {
103
103
  project: {
104
104
  mutate: {
105
105
  touch: async ({ server }) => {
106
- server?.asyncTasks.push(async () => {
106
+ server?.enqueueTask(async () => {
107
107
  effectRuns++
108
108
  })
109
109
  throw new Error('rejected')
@@ -129,6 +129,90 @@ describe('createZeroServer mutation recovery', () => {
129
129
  expect(effectRuns).toBe(0)
130
130
  })
131
131
 
132
+ test('awaits barrier tasks before resolving the push response', async () => {
133
+ const pg = new FakePgClient()
134
+ let releaseBarrier!: () => void
135
+ let markBarrierStarted!: () => void
136
+ const barrierStarted = new Promise<void>((resolve) => {
137
+ markBarrierStarted = resolve
138
+ })
139
+ const barrier = new Promise<void>((resolve) => {
140
+ releaseBarrier = resolve
141
+ })
142
+ const zeroServer = createZeroServer({
143
+ schema: zeroHttpFixtureSchema,
144
+ models: {
145
+ project: {
146
+ mutate: {
147
+ touch: async ({ server }) => {
148
+ server?.enqueueTask(
149
+ async () => {
150
+ markBarrierStarted()
151
+ await barrier
152
+ },
153
+ { barrier: true },
154
+ )
155
+ },
156
+ },
157
+ },
158
+ },
159
+ createServerActions: () => ({}),
160
+ pool: pg as any,
161
+ })
162
+
163
+ let responseResolved = false
164
+ const resultPromise = zeroServer
165
+ .handleMutationRequest({
166
+ authData: null,
167
+ request: pushRequest(1),
168
+ })
169
+ .then((result) => {
170
+ responseResolved = true
171
+ return result
172
+ })
173
+
174
+ await barrierStarted
175
+ await Promise.resolve()
176
+ expect(responseResolved).toBe(false)
177
+
178
+ releaseBarrier()
179
+ const result = await resultPromise
180
+ expect((result.response as any).mutations[0].result).toEqual({})
181
+ })
182
+
183
+ test('does not fail the push when a background task rejects', async () => {
184
+ const pg = new FakePgClient()
185
+ const error = new Error('background failed')
186
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
187
+ const zeroServer = createZeroServer({
188
+ schema: zeroHttpFixtureSchema,
189
+ models: {
190
+ project: {
191
+ mutate: {
192
+ touch: async ({ server }) => {
193
+ server?.enqueueTask(async () => {
194
+ throw error
195
+ })
196
+ },
197
+ },
198
+ },
199
+ },
200
+ createServerActions: () => ({}),
201
+ pool: pg as any,
202
+ })
203
+
204
+ const result = await zeroServer.handleMutationRequest({
205
+ authData: null,
206
+ request: pushRequest(1),
207
+ })
208
+
209
+ expect((result.response as any).mutations[0].result).toEqual({})
210
+ await vi.waitFor(() => {
211
+ expect(consoleError).toHaveBeenCalledWith('[on-zero] async task failed', error)
212
+ })
213
+ consoleError.mockRestore()
214
+ })
215
+
132
216
  test('persists alreadyProcessed duplicate results for zero-cache recovery rows', async () => {
133
217
  const pg = new FakePgClient()
134
218
  const zeroServer = createZeroServer({