on-zero 0.7.1 → 0.7.2

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 (55) hide show
  1. package/dist/cjs/createZeroSQLiteServer.test.cjs +42 -0
  2. package/dist/cjs/createZeroSQLiteServer.test.native.js +45 -0
  3. package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -1
  4. package/dist/cjs/httpPull/transport.test.cjs +109 -0
  5. package/dist/cjs/httpPull/transport.test.native.js +148 -0
  6. package/dist/cjs/httpPull/transport.test.native.js.map +1 -1
  7. package/dist/cjs/httpPullTransport.cjs +41 -24
  8. package/dist/cjs/httpPullTransport.native.js +47 -26
  9. package/dist/cjs/httpPullTransport.native.js.map +1 -1
  10. package/dist/cjs/mutations.cjs +5 -1
  11. package/dist/cjs/mutations.native.js +5 -1
  12. package/dist/cjs/mutations.native.js.map +1 -1
  13. package/dist/cjs/mutations.test.cjs +28 -0
  14. package/dist/cjs/mutations.test.native.js +40 -0
  15. package/dist/cjs/mutations.test.native.js.map +1 -0
  16. package/dist/cjs/sqliteRuntime.cjs +2 -2
  17. package/dist/cjs/sqliteRuntime.native.js +4 -4
  18. package/dist/cjs/sqliteRuntime.native.js.map +1 -1
  19. package/dist/esm/createZeroSQLiteServer.test.mjs +42 -0
  20. package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -1
  21. package/dist/esm/createZeroSQLiteServer.test.native.js +45 -0
  22. package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -1
  23. package/dist/esm/httpPull/transport.test.mjs +109 -0
  24. package/dist/esm/httpPull/transport.test.mjs.map +1 -1
  25. package/dist/esm/httpPull/transport.test.native.js +148 -0
  26. package/dist/esm/httpPull/transport.test.native.js.map +1 -1
  27. package/dist/esm/httpPullTransport.mjs +41 -24
  28. package/dist/esm/httpPullTransport.mjs.map +1 -1
  29. package/dist/esm/httpPullTransport.native.js +47 -26
  30. package/dist/esm/httpPullTransport.native.js.map +1 -1
  31. package/dist/esm/mutations.mjs +5 -1
  32. package/dist/esm/mutations.mjs.map +1 -1
  33. package/dist/esm/mutations.native.js +5 -1
  34. package/dist/esm/mutations.native.js.map +1 -1
  35. package/dist/esm/mutations.test.mjs +29 -0
  36. package/dist/esm/mutations.test.mjs.map +1 -0
  37. package/dist/esm/mutations.test.native.js +38 -0
  38. package/dist/esm/mutations.test.native.js.map +1 -0
  39. package/dist/esm/sqliteRuntime.mjs +2 -2
  40. package/dist/esm/sqliteRuntime.mjs.map +1 -1
  41. package/dist/esm/sqliteRuntime.native.js +4 -4
  42. package/dist/esm/sqliteRuntime.native.js.map +1 -1
  43. package/package.json +2 -2
  44. package/readme.md +2 -2
  45. package/src/createZeroSQLiteServer.test.ts +43 -0
  46. package/src/httpPull/transport.test.ts +132 -0
  47. package/src/httpPullTransport.ts +74 -42
  48. package/src/mutations.test.ts +38 -0
  49. package/src/mutations.ts +10 -2
  50. package/src/sqliteRuntime.ts +4 -4
  51. package/types/httpPullTransport.d.ts +14 -1
  52. package/types/httpPullTransport.d.ts.map +1 -1
  53. package/types/mutations.d.ts.map +1 -1
  54. package/types/mutations.test.d.ts +2 -0
  55. package/types/mutations.test.d.ts.map +1 -0
@@ -249,6 +249,49 @@ describe('createZeroSQLiteServer', () => {
249
249
  expect(effectRuns).toBe(1)
250
250
  })
251
251
 
252
+ test('keeps later writes after inserting an existing primary key', async () => {
253
+ db.prepare('INSERT INTO project (id, ownerId, name) VALUES (?, ?, ?)').run(
254
+ 'p1',
255
+ 'u1',
256
+ 'original',
257
+ )
258
+ db.prepare('INSERT INTO project (id, ownerId, name) VALUES (?, ?, ?)').run(
259
+ 'p2',
260
+ 'u1',
261
+ 'before',
262
+ )
263
+ const server = createZeroSQLiteServer({
264
+ schema: zeroHttpFixtureSchema,
265
+ models: {
266
+ project: {
267
+ mutate: {
268
+ converge: async ({ tx }) => {
269
+ await tx.mutate.project.insert({
270
+ id: 'p1',
271
+ ownerId: 'u2',
272
+ name: 'replacement',
273
+ })
274
+ await tx.mutate.project.update({ id: 'p2', name: 'after' })
275
+ },
276
+ },
277
+ },
278
+ },
279
+ createServerActions: () => ({}),
280
+ transactionProvider,
281
+ })
282
+
283
+ const result = await server.handleMutationRequest({
284
+ authData: null,
285
+ request: pushRequest(1, 'project.converge', {}),
286
+ })
287
+
288
+ expect((result.response as any).mutations[0].result).toEqual({})
289
+ expect(db.prepare('SELECT * FROM project ORDER BY id').all()).toEqual([
290
+ { id: 'p1', ownerId: 'u1', name: 'original' },
291
+ { id: 'p2', ownerId: 'u1', name: 'after' },
292
+ ])
293
+ })
294
+
252
295
  test('rolls back row writes and persists an application error with its LMID', async () => {
253
296
  const server = createZeroSQLiteServer({
254
297
  schema: zeroHttpFixtureSchema,
@@ -20,10 +20,13 @@ type RequestRecord = {
20
20
  const zeros: Zero<any, any>[] = []
21
21
  const transports: Array<{ uninstall(): void }> = []
22
22
  let storageID = 0
23
+ let restoreNativeWebSocket: (() => void) | undefined
23
24
 
24
25
  afterEach(async () => {
25
26
  while (zeros.length) await zeros.pop()?.close()
26
27
  while (transports.length) transports.pop()?.uninstall()
28
+ restoreNativeWebSocket?.()
29
+ restoreNativeWebSocket = undefined
27
30
  vi.useRealTimers()
28
31
  })
29
32
 
@@ -662,6 +665,90 @@ describe('zero-http transport', () => {
662
665
  expect(errors).toEqual([])
663
666
  })
664
667
 
668
+ test('authenticated wake appends a freshly minted token when the socket opens', async () => {
669
+ const wakeSockets = useFakeNativeWebSocket()
670
+ const getToken = vi.fn(async () => 'signed token&scope=one')
671
+ const fetch = unchangedPullFetch()
672
+ const transport = installHttpPullTransport({
673
+ origin: ORIGIN,
674
+ fetch,
675
+ wake: { getToken },
676
+ })
677
+ transports.push(transport)
678
+
679
+ openRawSocketWithMessages()
680
+
681
+ await eventually(() => expect(wakeSockets).toHaveLength(1))
682
+ expect(getToken).toHaveBeenCalledTimes(1)
683
+ expect(wakeSockets[0].url).toBe(
684
+ 'wss://zero-http.local/wake?clientID=c1&wakeToken=signed%20token%26scope%3Done',
685
+ )
686
+ })
687
+
688
+ test('authenticated wake gets a fresh token for every reconnect attempt', async () => {
689
+ const wakeSockets = useFakeNativeWebSocket()
690
+ const getToken = vi
691
+ .fn<() => Promise<string>>()
692
+ .mockResolvedValueOnce('wake-token-1')
693
+ .mockResolvedValueOnce('wake-token-2')
694
+ const transport = installHttpPullTransport({
695
+ origin: ORIGIN,
696
+ fetch: unchangedPullFetch(),
697
+ wake: { getToken },
698
+ })
699
+ transports.push(transport)
700
+
701
+ openRawSocketWithMessages()
702
+ await eventually(() => expect(wakeSockets).toHaveLength(1))
703
+ wakeSockets[0].onerror?.()
704
+
705
+ await eventually(() => expect(wakeSockets).toHaveLength(2), 1_000)
706
+ expect(getToken).toHaveBeenCalledTimes(2)
707
+ expect(wakeSockets.map((socket) => socket.url)).toEqual([
708
+ 'wss://zero-http.local/wake?clientID=c1&wakeToken=wake-token-1',
709
+ 'wss://zero-http.local/wake?clientID=c1&wakeToken=wake-token-2',
710
+ ])
711
+ })
712
+
713
+ test('wake token rejection leaves pulls healthy and retries the advisory channel', async () => {
714
+ const wakeSockets = useFakeNativeWebSocket()
715
+ const getToken = vi.fn(async () => {
716
+ throw new Error('mint route unavailable')
717
+ })
718
+ const fetch = unchangedPullFetch()
719
+ const transport = installHttpPullTransport({
720
+ origin: ORIGIN,
721
+ fetch,
722
+ wake: { getToken },
723
+ })
724
+ transports.push(transport)
725
+
726
+ openRawSocketWithMessages()
727
+ await eventually(() => expect(fetch).toHaveBeenCalled())
728
+ const pullsBeforeManualPull = fetch.mock.calls.length
729
+
730
+ await expect(transport.pull()).resolves.toBeUndefined()
731
+ expect(fetch.mock.calls.length).toBeGreaterThan(pullsBeforeManualPull)
732
+ await eventually(() => expect(getToken).toHaveBeenCalledTimes(2), 1_000)
733
+ expect(wakeSockets).toHaveLength(0)
734
+ expect(transport.connections).toBe(1)
735
+ })
736
+
737
+ test('wake true preserves the bare unauthenticated socket URL', async () => {
738
+ const wakeSockets = useFakeNativeWebSocket()
739
+ const transport = installHttpPullTransport({
740
+ origin: ORIGIN,
741
+ fetch: unchangedPullFetch(),
742
+ wake: true,
743
+ })
744
+ transports.push(transport)
745
+
746
+ openRawSocketWithMessages()
747
+
748
+ await eventually(() => expect(wakeSockets).toHaveLength(1))
749
+ expect(wakeSockets[0].url).toBe('wss://zero-http.local/wake?clientID=c1')
750
+ })
751
+
665
752
  test('non-origin WebSockets pass through to the native implementation', () => {
666
753
  const previous = globalThis.WebSocket
667
754
  class NativeWebSocket {
@@ -832,6 +919,51 @@ function jsonResponse(body: unknown, init?: ResponseInit) {
832
919
  })
833
920
  }
834
921
 
922
+ function unchangedPullFetch() {
923
+ return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
924
+ const request = recordRequest(input, init)
925
+ expect(request.path).toBe('/pull')
926
+ return jsonResponse({ cookie: request.body.cookie, unchanged: true })
927
+ })
928
+ }
929
+
930
+ function useFakeNativeWebSocket() {
931
+ const previous = globalThis.WebSocket
932
+ const sockets: Array<{
933
+ url: string
934
+ onmessage: (() => void) | null
935
+ onclose: (() => void) | null
936
+ onerror: (() => void) | null
937
+ }> = []
938
+
939
+ class FakeNativeWebSocket {
940
+ static CONNECTING = 0
941
+ static OPEN = 1
942
+ static CLOSING = 2
943
+ static CLOSED = 3
944
+
945
+ readonly url: string
946
+ onmessage: (() => void) | null = null
947
+ onclose: (() => void) | null = null
948
+ onerror: (() => void) | null = null
949
+
950
+ constructor(url: string | URL) {
951
+ this.url = String(url)
952
+ sockets.push(this)
953
+ }
954
+
955
+ close() {
956
+ this.onclose?.()
957
+ }
958
+ }
959
+
960
+ globalThis.WebSocket = FakeNativeWebSocket as unknown as typeof WebSocket
961
+ restoreNativeWebSocket = () => {
962
+ globalThis.WebSocket = previous
963
+ }
964
+ return sockets
965
+ }
966
+
835
967
  async function waitForComplete<T>(view: {
836
968
  addListener(listener: (data: any, resultType: string) => void): () => void
837
969
  }): Promise<T> {
@@ -78,7 +78,7 @@ type TransportState = {
78
78
  readonly nativeWebSocket: WebSocketConstructor | undefined
79
79
  readonly sockets: Set<ZeroHttpSocket>
80
80
  readonly pullIntervalMs: number | undefined
81
- readonly wakeEnabled: boolean
81
+ readonly wake: HttpPullTransportOptions['wake']
82
82
  readonly queryTransform: QueryTransform | undefined
83
83
  readonly queryForward: boolean
84
84
  readonly queryAware: boolean
@@ -117,12 +117,18 @@ export type HttpPullTransportOptions = {
117
117
  // when set, every open connection also pulls on this interval so
118
118
  // server-initiated changes arrive without a client-side trigger
119
119
  pullIntervalMs?: number
120
- // when true, each connection also opens a notification-only wake socket to
121
- // <origin>/wake and pulls immediately on any wake, demoting the interval
122
- // poll to a safety net. the wake channel carries no data ("pull now" only)
123
- // and zero correctness weight: a lost or duplicated wake can never cause
124
- // missed or wrong data because convergence comes from the pull protocol.
125
- wake?: boolean
120
+ /**
121
+ * opens a notification-only socket to <origin>/wake and pulls immediately on
122
+ * any wake, demoting interval polling to a safety net. true preserves the
123
+ * bare unauthenticated URL. for an authenticated host, pass getToken: each
124
+ * socket attempt calls it afresh and appends the result as wakeToken because
125
+ * browser WebSockets cannot send headers. consumers should implement
126
+ * getToken by calling an authenticated edge route that mints a short-lived,
127
+ * namespace-scoped signed token; the consumer's authorizeWake callback must
128
+ * verify that token. mint failures leave this advisory channel down and retry
129
+ * with backoff; pulls remain the source of correctness.
130
+ */
131
+ wake?: boolean | { getToken(): Promise<string> }
126
132
  // when provided, the query-aware extension is on and desired queries are
127
133
  // resolved client-side to an AST before shipping (native host / trusted
128
134
  // harness). omit for the baseline dialect (client-local got-query synthesis).
@@ -132,7 +138,8 @@ export type HttpPullTransportOptions = {
132
138
  // auth. the production path for permission-transformed queries.
133
139
  queryForward?: boolean
134
140
  // receives the structured connection lifecycle. when omitted, on-zero logs
135
- // each event as one JSON line so production failures retain their owners.
141
+ // failures so production errors retain their owners without logging routine
142
+ // connection and mutation traffic.
136
143
  lifecycle?: (event: HttpPullLifecycleEvent) => void
137
144
  }
138
145
 
@@ -200,8 +207,8 @@ function getHttpPullPageRegistry(): HttpPullPageRegistry {
200
207
  }
201
208
 
202
209
  function logHttpPullLifecycle(event: HttpPullLifecycleEvent) {
203
- if (process.env.NODE_ENV === 'test') return
204
- console.info(`[on-zero:http-pull] ${JSON.stringify(event)}`)
210
+ if (process.env.NODE_ENV === 'test' || event.type !== 'failure') return
211
+ console.error(`[on-zero:http-pull] ${JSON.stringify(event)}`)
205
212
  }
206
213
 
207
214
  export function installHttpPullTransport(
@@ -233,7 +240,7 @@ export function installHttpPullTransport(
233
240
  nativeWebSocket: previousWebSocket,
234
241
  sockets: new Set(),
235
242
  pullIntervalMs: opts.pullIntervalMs,
236
- wakeEnabled: opts.wake ?? false,
243
+ wake: opts.wake ?? false,
237
244
  queryTransform: opts.queryTransform,
238
245
  queryForward: opts.queryForward === true,
239
246
  queryAware: opts.queryTransform !== undefined || opts.queryForward === true,
@@ -338,6 +345,7 @@ class ZeroHttpSocket {
338
345
  private openTimer: ReturnType<typeof setTimeout> | undefined
339
346
  private pullTimer: ReturnType<typeof setInterval> | undefined
340
347
  private wakeSocket: { close(): void } | undefined
348
+ private wakeConnecting = false
341
349
  private wakeReconnectTimer: ReturnType<typeof setTimeout> | undefined
342
350
  private readonly generation: number
343
351
  private readonly zeroInstanceID: string
@@ -491,7 +499,7 @@ class ZeroHttpSocket {
491
499
  this.run(this.pull())
492
500
  }, this.state.pullIntervalMs)
493
501
  }
494
- if (this.state.wakeEnabled) this.openWakeChannel()
502
+ if (this.state.wake) this.openWakeChannel()
495
503
  }
496
504
 
497
505
  // notification-only wake channel: a real WebSocket to <origin>/wake that
@@ -501,38 +509,62 @@ class ZeroHttpSocket {
501
509
  // interval poll remains the safety net that guarantees convergence.
502
510
  private openWakeChannel() {
503
511
  const Native = this.state.nativeWebSocket
504
- if (!Native || this.wakeSocket || this.readyState !== this.OPEN) return
505
- const wsBase = this.state.originString.replace(/^http/, 'ws')
506
- const url = `${wsBase}/wake?clientID=${encodeURIComponent(this.clientID)}`
507
- let socket: {
508
- onmessage: (() => void) | null
509
- onclose: (() => void) | null
510
- onerror: (() => void) | null
511
- close(): void
512
- }
513
- try {
514
- socket = new Native(url) as unknown as typeof socket
515
- } catch {
512
+ if (
513
+ !Native ||
514
+ this.wakeSocket ||
515
+ this.wakeConnecting ||
516
+ this.readyState !== this.OPEN
517
+ ) {
516
518
  return
517
519
  }
518
- this.wakeSocket = socket
519
- const reconnect = () => {
520
- if (this.wakeSocket !== socket) return
521
- this.wakeSocket = undefined
522
- if (this.readyState !== this.OPEN || this.wakeReconnectTimer) return
523
- this.wakeReconnectTimer = setTimeout(() => {
524
- this.wakeReconnectTimer = undefined
525
- this.openWakeChannel()
526
- }, 500)
527
- }
528
- // route through requestPullAfterCurrent, NOT pull() directly: a wake that
529
- // lands while a pull is already in flight must set pullAfterCurrent so the
530
- // in-flight pull re-runs and picks up the woken change. calling pull()
531
- // directly would return the existing promise and silently drop the wake,
532
- // leaving convergence to the safety poll (a burst-storm latency bug).
533
- socket.onmessage = () => this.requestPullAfterCurrent()
534
- socket.onclose = reconnect
535
- socket.onerror = reconnect
520
+ this.wakeConnecting = true
521
+ const wsBase = this.state.originString.replace(/^http/, 'ws')
522
+ void (async () => {
523
+ let wakeToken: string | undefined
524
+ let socket: {
525
+ onmessage: (() => void) | null
526
+ onclose: (() => void) | null
527
+ onerror: (() => void) | null
528
+ close(): void
529
+ }
530
+ try {
531
+ if (typeof this.state.wake === 'object') {
532
+ wakeToken = await this.state.wake.getToken()
533
+ }
534
+ if (this.readyState !== this.OPEN || this.wakeSocket) return
535
+ const url =
536
+ `${wsBase}/wake?clientID=${encodeURIComponent(this.clientID)}` +
537
+ (wakeToken === undefined ? '' : `&wakeToken=${encodeURIComponent(wakeToken)}`)
538
+ socket = new Native(url) as unknown as typeof socket
539
+ } catch {
540
+ this.scheduleWakeReconnect()
541
+ return
542
+ } finally {
543
+ this.wakeConnecting = false
544
+ }
545
+ this.wakeSocket = socket
546
+ const reconnect = () => {
547
+ if (this.wakeSocket !== socket) return
548
+ this.wakeSocket = undefined
549
+ this.scheduleWakeReconnect()
550
+ }
551
+ // route through requestPullAfterCurrent, NOT pull() directly: a wake that
552
+ // lands while a pull is already in flight must set pullAfterCurrent so the
553
+ // in-flight pull re-runs and picks up the woken change. calling pull()
554
+ // directly would return the existing promise and silently drop the wake,
555
+ // leaving convergence to the safety poll (a burst-storm latency bug).
556
+ socket.onmessage = () => this.requestPullAfterCurrent()
557
+ socket.onclose = reconnect
558
+ socket.onerror = reconnect
559
+ })()
560
+ }
561
+
562
+ private scheduleWakeReconnect() {
563
+ if (this.readyState !== this.OPEN || this.wakeReconnectTimer) return
564
+ this.wakeReconnectTimer = setTimeout(() => {
565
+ this.wakeReconnectTimer = undefined
566
+ this.openWakeChannel()
567
+ }, 500)
536
568
  }
537
569
 
538
570
  private closeWakeChannel() {
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { mutations } from './mutations'
4
+ import { serverWhere } from './serverWhere'
5
+
6
+ describe('mutations registry', () => {
7
+ test('two modules registering the same table keep both custom mutators', () => {
8
+ // a template commonly splits a table's mutators across files (the table's
9
+ // own mutations file plus a seed.ts firing a demo seed on that table).
10
+ // the registry is keyed by table, so a wholesale replace made whichever
11
+ // module imported LAST win: alphabetical import order silently dropped
12
+ // seed.ts's seedDemo whenever the seed file sorted before the table file.
13
+ const permissions = serverWhere('todo', () => true)
14
+
15
+ const seedModule = mutations('todo', permissions, {
16
+ seedDemo: async () => {},
17
+ })
18
+ const tableModule = mutations('todo', permissions, {})
19
+
20
+ expect(typeof seedModule.seedDemo).toBe('function')
21
+ expect(typeof tableModule.insert).toBe('function')
22
+ // both proxies read the same per-table registry: the later CRUD-only
23
+ // registration must not clobber the earlier custom mutator
24
+ expect(typeof (tableModule as Record<string, unknown>).seedDemo).toBe('function')
25
+ expect(Object.keys(tableModule)).toContain('seedDemo')
26
+ })
27
+
28
+ test('re-registering a handler replaces it per key (HMR)', () => {
29
+ const permissions = serverWhere('post', () => true)
30
+ const v1 = async () => {}
31
+ const v2 = async () => {}
32
+ mutations('post', permissions, { custom: v1 })
33
+ const proxy = mutations('post', permissions, { custom: v2 })
34
+ // per-key merge must still take the newest registration for an edited
35
+ // handler, otherwise HMR would pin the stale implementation
36
+ expect(proxy.custom).toBe(v2)
37
+ })
38
+ })
package/src/mutations.ts CHANGED
@@ -39,8 +39,16 @@ function getOrCreateMutationProxy<T extends Record<string, Function>>(
39
39
  tableName: string,
40
40
  implementations: T,
41
41
  ): T {
42
- // always update implementations (supports HMR)
43
- mutationRegistry().set(tableName, implementations)
42
+ // merge with any prior registration for this table: multiple modules may
43
+ // register mutators on the same table (a seed.ts alongside the table's own
44
+ // mutations file) and module import order is arbitrary, so replacing
45
+ // wholesale drops whichever module registered first. per-key replacement
46
+ // still supports HMR updates of edited handlers.
47
+ const prior = mutationRegistry().get(tableName)
48
+ mutationRegistry().set(
49
+ tableName,
50
+ prior ? { ...prior, ...implementations } : implementations,
51
+ )
44
52
 
45
53
  // return existing proxy if we have one (HMR case)
46
54
  const existing = proxyRegistry().get(tableName)
@@ -257,9 +257,12 @@ async function executeCrud<Schema extends ZeroSchema>(
257
257
  const insert = `INSERT INTO ${serverTableName} (${serverColumns.join(
258
258
  ', ',
259
259
  )}) VALUES (${columnNames.map(() => '?').join(', ')})`
260
+ const conflictColumns = table.primaryKey
261
+ .map((columnName) => quoteSQLiteIdentifier(serverColumn(table, columnName)))
262
+ .join(', ')
260
263
 
261
264
  if (kind === 'insert') {
262
- await executor.exec(insert, params, {
265
+ await executor.exec(`${insert} ON CONFLICT (${conflictColumns}) DO NOTHING`, params, {
263
266
  table: table.serverName ?? table.name,
264
267
  publicTable: tableName,
265
268
  kind,
@@ -271,9 +274,6 @@ async function executeCrud<Schema extends ZeroSchema>(
271
274
  const mutableColumns = columnNames.filter(
272
275
  (columnName) => !table.primaryKey.includes(columnName),
273
276
  )
274
- const conflictColumns = table.primaryKey
275
- .map((columnName) => quoteSQLiteIdentifier(serverColumn(table, columnName)))
276
- .join(', ')
277
277
  const conflictAction =
278
278
  mutableColumns.length === 0
279
279
  ? 'DO NOTHING'
@@ -13,7 +13,20 @@ export type HttpPullTransportOptions = {
13
13
  pushOrigin?: string;
14
14
  fetch?: typeof fetch;
15
15
  pullIntervalMs?: number;
16
- wake?: boolean;
16
+ /**
17
+ * opens a notification-only socket to <origin>/wake and pulls immediately on
18
+ * any wake, demoting interval polling to a safety net. true preserves the
19
+ * bare unauthenticated URL. for an authenticated host, pass getToken: each
20
+ * socket attempt calls it afresh and appends the result as wakeToken because
21
+ * browser WebSockets cannot send headers. consumers should implement
22
+ * getToken by calling an authenticated edge route that mints a short-lived,
23
+ * namespace-scoped signed token; the consumer's authorizeWake callback must
24
+ * verify that token. mint failures leave this advisory channel down and retry
25
+ * with backoff; pulls remain the source of correctness.
26
+ */
27
+ wake?: boolean | {
28
+ getToken(): Promise<string>;
29
+ };
17
30
  queryTransform?: QueryTransform;
18
31
  queryForward?: boolean;
19
32
  lifecycle?: (event: HttpPullLifecycleEvent) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"httpPullTransport.d.ts","sourceRoot":"","sources":["../src/httpPullTransport.ts"],"names":[],"mappings":"AAUA,KAAK,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AA+B7D,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAA;AA4DhF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,SAAS,IAAI,IAAI,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAA;IAGd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IAGpB,cAAc,CAAC,EAAE,MAAM,CAAA;IAMvB,IAAI,CAAC,EAAE,OAAO,CAAA;IAId,cAAc,CAAC,EAAE,cAAc,CAAA;IAI/B,YAAY,CAAC,EAAE,OAAO,CAAA;IAGtB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;CACpD,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EACA,SAAS,GACT,UAAU,GACV,MAAM,GACN,OAAO,GACP,SAAS,GACT,YAAY,GACZ,SAAS,GACT,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAwCD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,wBAAwB,GAC7B,iBAAiB,CA6EnB;AAKD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,wBAAwB,GAC7B,iBAAiB,CAQnB"}
1
+ {"version":3,"file":"httpPullTransport.d.ts","sourceRoot":"","sources":["../src/httpPullTransport.ts"],"names":[],"mappings":"AAUA,KAAK,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AA+B7D,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAA;AA4DhF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,SAAS,IAAI,IAAI,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAA;IAGd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IAGpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAIhD,cAAc,CAAC,EAAE,cAAc,CAAA;IAI/B,YAAY,CAAC,EAAE,OAAO,CAAA;IAItB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;CACpD,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EACA,SAAS,GACT,UAAU,GACV,MAAM,GACN,OAAO,GACP,SAAS,GACT,YAAY,GACZ,SAAS,GACT,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAwCD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,wBAAwB,GAC7B,iBAAiB,CA6EnB;AAKD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,wBAAwB,GAC7B,iBAAiB,CAQnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../src/mutations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,KAAK,EACV,cAAc,EACd,MAAM,EACN,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACN,MAAM,SAAS,CAAA;AAGhB,KAAK,kBAAkB,CAAC,KAAK,SAAS,SAAS,IAAI,uBAAuB,CACxE,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CACxB,CAAA;AA2DD,KAAK,eAAe,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AACnF,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAevD,KAAK,YAAY,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAA;AAEhD,KAAK,aAAa,CAAC,KAAK,SAAS,YAAY,IAAI;IAC/C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;CAC/C,CAAA;AAED,KAAK,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE1D,KAAK,iBAAiB,CAAC,KAAK,SAAS,YAAY,EAAE,SAAS,SAAS,gBAAgB,IAAI;KACtF,GAAG,IAAI,SAAS,GAAG,MAAM,SAAS,GAAG,GAAG,SAAS,MAAM,SAAS,GAC7D,SAAS,CAAC,GAAG,CAAC,GACd,GAAG,SAAS,MAAM,aAAa,CAAC,GAAG,CAAC,GAClC,aAAa,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GACzB,KAAK;CACZ,CAAA;AAED,wBAAgB,SAAS,CAAC,SAAS,SAAS,gBAAgB,EAC1D,SAAS,EAAE,SAAS,GACnB,SAAS,CAAA;AACZ,wBAAgB,SAAS,CAAC,KAAK,SAAS,YAAY,EAAE,WAAW,SAAS,KAAK,EAC7E,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,GACvB,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC/B,wBAAgB,SAAS,CACvB,KAAK,SAAS,YAAY,EAC1B,WAAW,SAAS,KAAK,EACzB,SAAS,SAAS,gBAAgB,EAElC,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,GACnB,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;AAEtC,wBAAgB,SAAS,CAAC,KAAK,SAAS,SAAS,EAAE,WAAW,SAAS,KAAK,EAC1E,SAAS,EAAE,KAAK,EAChB,WAAW,EAAE,WAAW,GACvB,iBAAiB,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;AACnD,wBAAgB,SAAS,CACvB,KAAK,SAAS,SAAS,EACvB,WAAW,SAAS,KAAK,EACzB,SAAS,SAAS,gBAAgB,EAElC,SAAS,EAAE,KAAK,EAChB,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,GACnB,iBAAiB,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAA"}
1
+ {"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../src/mutations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,KAAK,EACV,cAAc,EACd,MAAM,EACN,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACN,MAAM,SAAS,CAAA;AAGhB,KAAK,kBAAkB,CAAC,KAAK,SAAS,SAAS,IAAI,uBAAuB,CACxE,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CACxB,CAAA;AAmED,KAAK,eAAe,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AACnF,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAevD,KAAK,YAAY,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAA;AAEhD,KAAK,aAAa,CAAC,KAAK,SAAS,YAAY,IAAI;IAC/C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC9C,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;CAC/C,CAAA;AAED,KAAK,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE1D,KAAK,iBAAiB,CAAC,KAAK,SAAS,YAAY,EAAE,SAAS,SAAS,gBAAgB,IAAI;KACtF,GAAG,IAAI,SAAS,GAAG,MAAM,SAAS,GAAG,GAAG,SAAS,MAAM,SAAS,GAC7D,SAAS,CAAC,GAAG,CAAC,GACd,GAAG,SAAS,MAAM,aAAa,CAAC,GAAG,CAAC,GAClC,aAAa,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GACzB,KAAK;CACZ,CAAA;AAED,wBAAgB,SAAS,CAAC,SAAS,SAAS,gBAAgB,EAC1D,SAAS,EAAE,SAAS,GACnB,SAAS,CAAA;AACZ,wBAAgB,SAAS,CAAC,KAAK,SAAS,YAAY,EAAE,WAAW,SAAS,KAAK,EAC7E,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,GACvB,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC/B,wBAAgB,SAAS,CACvB,KAAK,SAAS,YAAY,EAC1B,WAAW,SAAS,KAAK,EACzB,SAAS,SAAS,gBAAgB,EAElC,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,GACnB,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;AAEtC,wBAAgB,SAAS,CAAC,KAAK,SAAS,SAAS,EAAE,WAAW,SAAS,KAAK,EAC1E,SAAS,EAAE,KAAK,EAChB,WAAW,EAAE,WAAW,GACvB,iBAAiB,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;AACnD,wBAAgB,SAAS,CACvB,KAAK,SAAS,SAAS,EACvB,WAAW,SAAS,KAAK,EACzB,SAAS,SAAS,gBAAgB,EAElC,SAAS,EAAE,KAAK,EAChB,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,GACnB,iBAAiB,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mutations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutations.test.d.ts","sourceRoot":"","sources":["../src/mutations.test.ts"],"names":[],"mappings":""}