bunderstack 0.20.0 → 0.21.1

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 (115) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +17 -5
  3. package/dist/api/crud-router.d.ts +4 -4
  4. package/dist/api/realtime-router.d.ts +1 -1
  5. package/dist/client/client.d.ts +53 -0
  6. package/dist/client/client.d.ts.map +1 -0
  7. package/dist/client/client.js +111 -0
  8. package/dist/client/client.js.map +1 -0
  9. package/dist/client/index.d.ts +4 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +4 -0
  12. package/dist/client/index.js.map +1 -0
  13. package/dist/client/live-view.d.ts +49 -0
  14. package/dist/client/live-view.d.ts.map +1 -0
  15. package/dist/client/live-view.js +184 -0
  16. package/dist/client/live-view.js.map +1 -0
  17. package/dist/client/react.d.ts +4 -0
  18. package/dist/client/react.d.ts.map +1 -0
  19. package/dist/client/react.js +6 -0
  20. package/dist/client/react.js.map +1 -0
  21. package/dist/client/realtime.d.ts +54 -0
  22. package/dist/client/realtime.d.ts.map +1 -0
  23. package/dist/client/realtime.js +105 -0
  24. package/dist/client/realtime.js.map +1 -0
  25. package/dist/client/rest.d.ts +2 -0
  26. package/dist/client/rest.d.ts.map +1 -0
  27. package/dist/client/rest.js +2 -0
  28. package/dist/client/rest.js.map +1 -0
  29. package/dist/client/rpc-client.d.ts +38 -0
  30. package/dist/client/rpc-client.d.ts.map +1 -0
  31. package/dist/client/rpc-client.js +59 -0
  32. package/dist/client/rpc-client.js.map +1 -0
  33. package/dist/client/solid.d.ts +7 -0
  34. package/dist/client/solid.d.ts.map +1 -0
  35. package/dist/client/solid.js +21 -0
  36. package/dist/client/solid.js.map +1 -0
  37. package/dist/client/svelte.d.ts +6 -0
  38. package/dist/client/svelte.d.ts.map +1 -0
  39. package/dist/client/svelte.js +9 -0
  40. package/dist/client/svelte.js.map +1 -0
  41. package/dist/client/vue.d.ts +5 -0
  42. package/dist/client/vue.d.ts.map +1 -0
  43. package/dist/client/vue.js +11 -0
  44. package/dist/client/vue.js.map +1 -0
  45. package/dist/query/api.d.ts +10 -0
  46. package/dist/query/api.d.ts.map +1 -0
  47. package/dist/query/api.js +7 -0
  48. package/dist/query/api.js.map +1 -0
  49. package/dist/query/client.d.ts +33 -0
  50. package/dist/query/client.d.ts.map +1 -0
  51. package/dist/query/client.js +98 -0
  52. package/dist/query/client.js.map +1 -0
  53. package/dist/query/fetch.d.ts +3 -0
  54. package/dist/query/fetch.d.ts.map +1 -0
  55. package/dist/query/fetch.js +4 -0
  56. package/dist/query/fetch.js.map +1 -0
  57. package/dist/query/index.d.ts +8 -0
  58. package/dist/query/index.d.ts.map +1 -0
  59. package/dist/query/index.js +4 -0
  60. package/dist/query/index.js.map +1 -0
  61. package/dist/query/infer.d.ts +25 -0
  62. package/dist/query/infer.d.ts.map +1 -0
  63. package/dist/query/infer.js +2 -0
  64. package/dist/query/infer.js.map +1 -0
  65. package/dist/query/react.d.ts +3 -0
  66. package/dist/query/react.d.ts.map +1 -0
  67. package/dist/query/react.js +2 -0
  68. package/dist/query/react.js.map +1 -0
  69. package/dist/query/realtime-flush.d.ts +26 -0
  70. package/dist/query/realtime-flush.d.ts.map +1 -0
  71. package/dist/query/realtime-flush.js +45 -0
  72. package/dist/query/realtime-flush.js.map +1 -0
  73. package/dist/query/realtime-stream.d.ts +3 -0
  74. package/dist/query/realtime-stream.d.ts.map +1 -0
  75. package/dist/query/realtime-stream.js +3 -0
  76. package/dist/query/realtime-stream.js.map +1 -0
  77. package/dist/query/realtime.d.ts +88 -0
  78. package/dist/query/realtime.d.ts.map +1 -0
  79. package/dist/query/realtime.js +255 -0
  80. package/dist/query/realtime.js.map +1 -0
  81. package/dist/start/auth-client.d.ts +11 -0
  82. package/dist/start/auth-client.d.ts.map +1 -0
  83. package/dist/start/auth-client.js +15 -0
  84. package/dist/start/auth-client.js.map +1 -0
  85. package/dist/start/index.d.ts +66 -0
  86. package/dist/start/index.d.ts.map +1 -0
  87. package/dist/start/index.js +49 -0
  88. package/dist/start/index.js.map +1 -0
  89. package/dist/start/isomorphic-fetch.d.ts +13 -0
  90. package/dist/start/isomorphic-fetch.d.ts.map +1 -0
  91. package/dist/start/isomorphic-fetch.js +33 -0
  92. package/dist/start/isomorphic-fetch.js.map +1 -0
  93. package/dist/sync/collection.d.ts +106 -0
  94. package/dist/sync/collection.d.ts.map +1 -0
  95. package/dist/sync/collection.js +258 -0
  96. package/dist/sync/collection.js.map +1 -0
  97. package/dist/sync/index.d.ts +8 -0
  98. package/dist/sync/index.d.ts.map +1 -0
  99. package/dist/sync/index.js +4 -0
  100. package/dist/sync/index.js.map +1 -0
  101. package/dist/sync/realtime-sync.d.ts +31 -0
  102. package/dist/sync/realtime-sync.d.ts.map +1 -0
  103. package/dist/sync/realtime-sync.js +46 -0
  104. package/dist/sync/realtime-sync.js.map +1 -0
  105. package/dist/sync/sync-client.d.ts +28 -0
  106. package/dist/sync/sync-client.d.ts.map +1 -0
  107. package/dist/sync/sync-client.js +52 -0
  108. package/dist/sync/sync-client.js.map +1 -0
  109. package/dist/sync/update-queue.d.ts +27 -0
  110. package/dist/sync/update-queue.d.ts.map +1 -0
  111. package/dist/sync/update-queue.js +66 -0
  112. package/dist/sync/update-queue.js.map +1 -0
  113. package/package.json +103 -3
  114. package/skills/creating-bunderstack-apps/references/runtime-integrations.md +1 -1
  115. package/skills/migrating-to-bunderstack/references/runtime-replacements.md +2 -2
@@ -0,0 +1,8 @@
1
+ export { createClient } from './client.js';
2
+ export type { BunderstackClient, ClientOptions, FileBucketHelpers, FileTransformOptions, UploadedFile, } from './client.js';
3
+ export { createApiClient } from './api.js';
4
+ export type { ApiClientOptions, ApiQueryUtils } from './api.js';
5
+ export type { AnyBunderstackApp, ClientCarrier, ExposedTables, InferApiRouter, InferBuckets, InferInsert, InferSelect, InferSchema, InferTables, } from './infer.js';
6
+ export { syncRealtime } from './realtime.js';
7
+ export type { NotifyScheduler, RealtimeApplyStrategy, RealtimeChange, RealtimeEvent, RealtimeHeartbeat, RealtimeProcedure, RealtimeQueryApi, RealtimeSyncHandle, RealtimeSyncOptions, RealtimeClock, } from './realtime.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,GACb,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAC5D,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,cAAc,EACd,YAAY,EACZ,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,GACZ,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,GACd,MAAM,YAAY,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { createClient } from './client.js';
2
+ export { createApiClient } from './api.js';
3
+ export { syncRealtime } from './realtime.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAQvC,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAavC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA","sourcesContent":["export { createClient } from './client'\nexport type {\n BunderstackClient,\n ClientOptions,\n FileBucketHelpers,\n FileTransformOptions,\n UploadedFile,\n} from './client'\nexport { createApiClient } from './api'\nexport type { ApiClientOptions, ApiQueryUtils } from './api'\nexport type {\n AnyBunderstackApp,\n ClientCarrier,\n ExposedTables,\n InferApiRouter,\n InferBuckets,\n InferInsert,\n InferSelect,\n InferSchema,\n InferTables,\n} from './infer'\nexport { syncRealtime } from './realtime'\nexport type {\n NotifyScheduler,\n RealtimeApplyStrategy,\n RealtimeChange,\n RealtimeEvent,\n RealtimeHeartbeat,\n RealtimeProcedure,\n RealtimeQueryApi,\n RealtimeSyncHandle,\n RealtimeSyncOptions,\n RealtimeClock,\n} from './realtime'\n"]}
@@ -0,0 +1,25 @@
1
+ import type { AnyRouter } from '@orpc/server';
2
+ import type { ExposedApiTables } from '../api/types.js';
3
+ /** Shape of the `$inferClient` phantom `createBunderstack` puts on the app. */
4
+ export type ClientCarrier = {
5
+ schema: Record<string, unknown>;
6
+ access: unknown;
7
+ buckets: string;
8
+ api: AnyRouter;
9
+ };
10
+ export type AnyBunderstackApp = {
11
+ $inferClient?: ClientCarrier | undefined;
12
+ };
13
+ export type InferCarrier<TApp extends AnyBunderstackApp> = NonNullable<TApp['$inferClient']>;
14
+ export type InferSchema<TApp extends AnyBunderstackApp> = InferCarrier<TApp>['schema'];
15
+ export type InferBuckets<TApp extends AnyBunderstackApp> = InferCarrier<TApp>['buckets'];
16
+ export type InferSelect<T> = T extends {
17
+ $inferSelect: infer R;
18
+ } ? R : never;
19
+ export type InferInsert<T> = T extends {
20
+ $inferInsert: infer R;
21
+ } ? R : never;
22
+ export type InferApiRouter<TApp extends AnyBunderstackApp> = InferCarrier<TApp>['api'];
23
+ export type ExposedTables<TSchema extends Record<string, unknown>, TAccess = undefined> = ExposedApiTables<TSchema, TAccess>;
24
+ export type InferTables<TApp extends AnyBunderstackApp> = ExposedTables<InferSchema<TApp>, InferCarrier<TApp>['access']> & keyof InferSchema<TApp> & string;
25
+ //# sourceMappingURL=infer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infer.d.ts","sourceRoot":"","sources":["../../src/query/infer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,SAAS,CAAA;CACf,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,YAAY,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CAAA;AAE5E,MAAM,MAAM,YAAY,CAAC,IAAI,SAAS,iBAAiB,IAAI,WAAW,CACpE,IAAI,CAAC,cAAc,CAAC,CACrB,CAAA;AACD,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,iBAAiB,IACpD,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAA;AAC9B,MAAM,MAAM,YAAY,CAAC,IAAI,SAAS,iBAAiB,IACrD,YAAY,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAA;AAE/B,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,YAAY,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAA;AAC5E,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,YAAY,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAA;AAE5E,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,iBAAiB,IACvD,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAA;AAE3B,MAAM,MAAM,aAAa,CACvB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,GAAG,SAAS,IACjB,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AAEtC,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,iBAAiB,IAAI,aAAa,CACrE,WAAW,CAAC,IAAI,CAAC,EACjB,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAC7B,GACC,MAAM,WAAW,CAAC,IAAI,CAAC,GACvB,MAAM,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=infer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infer.js","sourceRoot":"","sources":["../../src/query/infer.ts"],"names":[],"mappings":"","sourcesContent":["import type { AnyRouter } from '@orpc/server'\n\nimport type { ExposedApiTables } from '../api/types'\n\n/** Shape of the `$inferClient` phantom `createBunderstack` puts on the app. */\nexport type ClientCarrier = {\n schema: Record<string, unknown>\n access: unknown\n buckets: string\n api: AnyRouter\n}\n\nexport type AnyBunderstackApp = { $inferClient?: ClientCarrier | undefined }\n\nexport type InferCarrier<TApp extends AnyBunderstackApp> = NonNullable<\n TApp['$inferClient']\n>\nexport type InferSchema<TApp extends AnyBunderstackApp> =\n InferCarrier<TApp>['schema']\nexport type InferBuckets<TApp extends AnyBunderstackApp> =\n InferCarrier<TApp>['buckets']\n\nexport type InferSelect<T> = T extends { $inferSelect: infer R } ? R : never\nexport type InferInsert<T> = T extends { $inferInsert: infer R } ? R : never\n\nexport type InferApiRouter<TApp extends AnyBunderstackApp> =\n InferCarrier<TApp>['api']\n\nexport type ExposedTables<\n TSchema extends Record<string, unknown>,\n TAccess = undefined,\n> = ExposedApiTables<TSchema, TAccess>\n\nexport type InferTables<TApp extends AnyBunderstackApp> = ExposedTables<\n InferSchema<TApp>,\n InferCarrier<TApp>['access']\n> &\n keyof InferSchema<TApp> &\n string\n"]}
@@ -0,0 +1,3 @@
1
+ export { createClient } from './client.js';
2
+ export type { BunderstackClient, ClientOptions } from './client.js';
3
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/query/react.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { createClient } from './client.js';
2
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/query/react.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA","sourcesContent":["export { createClient } from './client'\nexport type { BunderstackClient, ClientOptions } from './client'\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * When buffered realtime work runs.
3
+ *
4
+ * `'frame'` coalesces a burst of events into one cache write per animation
5
+ * frame. `'sync'` restores the older behaviour of writing as each event
6
+ * arrives. A number debounces by that many milliseconds.
7
+ */
8
+ export type NotifyScheduler = 'sync' | 'frame' | number;
9
+ /** The timer functions the scheduler needs, injectable so tests control time. */
10
+ export type RealtimeClock = {
11
+ setTimeout: (fn: () => void, ms: number) => unknown;
12
+ clearTimeout: (handle: unknown) => void;
13
+ };
14
+ export type FlushScheduler = {
15
+ /**
16
+ * Ask for `flush` to run. Calls made while a flush is already pending are
17
+ * absorbed into it, so the flush runs once per window however many events
18
+ * arrived.
19
+ */
20
+ schedule: (flush: () => void) => void;
21
+ /** Drop a pending flush without running it. */
22
+ cancel: () => void;
23
+ };
24
+ export declare const systemClock: RealtimeClock;
25
+ export declare function createFlushScheduler(mode: NotifyScheduler, clock?: RealtimeClock): FlushScheduler;
26
+ //# sourceMappingURL=realtime-flush.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-flush.d.ts","sourceRoot":"","sources":["../../src/query/realtime-flush.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AAEvD,iFAAiF;AACjF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;OAIG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;IACrC,+CAA+C;IAC/C,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,aAIzB,CAAA;AAiBD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,eAAe,EACrB,KAAK,CAAC,EAAE,aAAa,GACpB,cAAc,CAuBhB"}
@@ -0,0 +1,45 @@
1
+ export const systemClock = {
2
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
3
+ clearTimeout: (handle) => clearTimeout(handle),
4
+ };
5
+ /**
6
+ * A frame is `requestAnimationFrame` in a browser and a zero-delay timer
7
+ * everywhere else, so the same code paces correctly under SSR and in tests.
8
+ * An injected clock always wins — a test that supplies one is asking for its
9
+ * timers to be the only ones.
10
+ */
11
+ function frameClock(clock) {
12
+ if (clock)
13
+ return clock;
14
+ if (typeof requestAnimationFrame !== 'function')
15
+ return systemClock;
16
+ return {
17
+ setTimeout: (fn) => requestAnimationFrame(fn),
18
+ clearTimeout: (handle) => cancelAnimationFrame(handle),
19
+ };
20
+ }
21
+ export function createFlushScheduler(mode, clock) {
22
+ if (mode === 'sync') {
23
+ return { schedule: (flush) => flush(), cancel: () => { } };
24
+ }
25
+ const timers = mode === 'frame' ? frameClock(clock) : (clock ?? systemClock);
26
+ const delayMs = mode === 'frame' ? 0 : Math.max(0, mode);
27
+ let pending;
28
+ return {
29
+ schedule(flush) {
30
+ if (pending !== undefined)
31
+ return;
32
+ pending = timers.setTimeout(() => {
33
+ pending = undefined;
34
+ flush();
35
+ }, delayMs);
36
+ },
37
+ cancel() {
38
+ if (pending === undefined)
39
+ return;
40
+ timers.clearTimeout(pending);
41
+ pending = undefined;
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=realtime-flush.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-flush.js","sourceRoot":"","sources":["../../src/query/realtime-flush.ts"],"names":[],"mappings":"AA0BA,MAAM,CAAC,MAAM,WAAW,GAAkB;IACxC,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CACvB,YAAY,CAAC,MAAuC,CAAC;CACxD,CAAA;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,KAAgC;IAClD,IAAI,KAAK;QAAE,OAAO,KAAK,CAAA;IACvB,IAAI,OAAO,qBAAqB,KAAK,UAAU;QAAE,OAAO,WAAW,CAAA;IACnE,OAAO;QACL,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,qBAAqB,CAAC,EAAE,CAAC;QAC7C,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,oBAAoB,CAAC,MAAgB,CAAC;KACjE,CAAA;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAqB,EACrB,KAAqB;IAErB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAA;IAC3D,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,WAAW,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IACxD,IAAI,OAAgB,CAAA;IAEpB,OAAO;QACL,QAAQ,CAAC,KAAK;YACZ,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAM;YACjC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;gBAC/B,OAAO,GAAG,SAAS,CAAA;gBACnB,KAAK,EAAE,CAAA;YACT,CAAC,EAAE,OAAO,CAAC,CAAA;QACb,CAAC;QACD,MAAM;YACJ,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAM;YACjC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;YAC5B,OAAO,GAAG,SAAS,CAAA;QACrB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * When buffered realtime work runs.\n *\n * `'frame'` coalesces a burst of events into one cache write per animation\n * frame. `'sync'` restores the older behaviour of writing as each event\n * arrives. A number debounces by that many milliseconds.\n */\nexport type NotifyScheduler = 'sync' | 'frame' | number\n\n/** The timer functions the scheduler needs, injectable so tests control time. */\nexport type RealtimeClock = {\n setTimeout: (fn: () => void, ms: number) => unknown\n clearTimeout: (handle: unknown) => void\n}\n\nexport type FlushScheduler = {\n /**\n * Ask for `flush` to run. Calls made while a flush is already pending are\n * absorbed into it, so the flush runs once per window however many events\n * arrived.\n */\n schedule: (flush: () => void) => void\n /** Drop a pending flush without running it. */\n cancel: () => void\n}\n\nexport const systemClock: RealtimeClock = {\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (handle) =>\n clearTimeout(handle as ReturnType<typeof setTimeout>),\n}\n\n/**\n * A frame is `requestAnimationFrame` in a browser and a zero-delay timer\n * everywhere else, so the same code paces correctly under SSR and in tests.\n * An injected clock always wins — a test that supplies one is asking for its\n * timers to be the only ones.\n */\nfunction frameClock(clock: RealtimeClock | undefined): RealtimeClock {\n if (clock) return clock\n if (typeof requestAnimationFrame !== 'function') return systemClock\n return {\n setTimeout: (fn) => requestAnimationFrame(fn),\n clearTimeout: (handle) => cancelAnimationFrame(handle as number),\n }\n}\n\nexport function createFlushScheduler(\n mode: NotifyScheduler,\n clock?: RealtimeClock,\n): FlushScheduler {\n if (mode === 'sync') {\n return { schedule: (flush) => flush(), cancel: () => {} }\n }\n\n const timers = mode === 'frame' ? frameClock(clock) : (clock ?? systemClock)\n const delayMs = mode === 'frame' ? 0 : Math.max(0, mode)\n let pending: unknown\n\n return {\n schedule(flush) {\n if (pending !== undefined) return\n pending = timers.setTimeout(() => {\n pending = undefined\n flush()\n }, delayMs)\n },\n cancel() {\n if (pending === undefined) return\n timers.clearTimeout(pending)\n pending = undefined\n },\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** @deprecated Import transport primitives from `bunderstack/client`. */
2
+ export { DEFAULT_KEEPALIVE_MS, DEFAULT_LIVENESS_FACTOR, isRealtimeHeartbeat, openRealtimeStream, type RealtimeAction, type RealtimeChange, type RealtimeEvent, type RealtimeHeartbeat, type RealtimeProcedure, type RealtimeStreamOptions, type RealtimeSyncHandle, } from '../client/realtime.js';
3
+ //# sourceMappingURL=realtime-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-stream.d.ts","sourceRoot":"","sources":["../../src/query/realtime-stream.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** @deprecated Import transport primitives from `bunderstack/client`. */
2
+ export { DEFAULT_KEEPALIVE_MS, DEFAULT_LIVENESS_FACTOR, isRealtimeHeartbeat, openRealtimeStream, } from '../client/realtime.js';
3
+ //# sourceMappingURL=realtime-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-stream.js","sourceRoot":"","sources":["../../src/query/realtime-stream.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,GAQnB,MAAM,oBAAoB,CAAA","sourcesContent":["/** @deprecated Import transport primitives from `bunderstack/client`. */\nexport {\n DEFAULT_KEEPALIVE_MS,\n DEFAULT_LIVENESS_FACTOR,\n isRealtimeHeartbeat,\n openRealtimeStream,\n type RealtimeAction,\n type RealtimeChange,\n type RealtimeEvent,\n type RealtimeHeartbeat,\n type RealtimeProcedure,\n type RealtimeStreamOptions,\n type RealtimeSyncHandle,\n} from '../client/realtime'\n"]}
@@ -0,0 +1,88 @@
1
+ import type { QueryClient } from '@tanstack/query-core';
2
+ import type { AnyBunderstackApp, InferSchema, InferSelect, InferTables } from './infer.js';
3
+ import type { NotifyScheduler, RealtimeClock } from './realtime-flush.js';
4
+ import type { RealtimeAction, RealtimeChange, RealtimeProcedure, RealtimeSyncHandle } from './realtime-stream.js';
5
+ export type { NotifyScheduler, RealtimeClock } from './realtime-flush.js';
6
+ export type { RealtimeAction, RealtimeChange, RealtimeEvent, RealtimeHeartbeat, RealtimeProcedure, RealtimeSyncHandle, } from './realtime-stream.js';
7
+ /**
8
+ * The change for one table, with `record` typed as that table's row.
9
+ *
10
+ * Passing the app type to `syncRealtime` turns `tables` into a checked list of
11
+ * exposed table names and `onChange` into a union discriminated by `table`, so
12
+ * `change.table === 'todos'` narrows `change.record` to the todo row:
13
+ *
14
+ * ```ts
15
+ * syncRealtime<App>({
16
+ * api,
17
+ * queryClient,
18
+ * tables: ['todos'],
19
+ * onChange: (change) => {
20
+ * if (change.table === 'todos') console.log(change.record.done)
21
+ * },
22
+ * })
23
+ * ```
24
+ */
25
+ export type RealtimeChangeFor<TApp extends AnyBunderstackApp, TTable extends InferTables<TApp>> = {
26
+ table: TTable;
27
+ action: RealtimeAction;
28
+ record: InferSelect<InferSchema<TApp>[TTable]>;
29
+ operationId?: string;
30
+ };
31
+ /**
32
+ * Untyped when no app type is supplied, so existing callers — and consumers
33
+ * that only have a structural client — keep the loose `RealtimeChange`.
34
+ */
35
+ export type RealtimeChangeOf<TApp, TTable extends string> = [TApp] extends [
36
+ never
37
+ ] ? RealtimeChange : TApp extends AnyBunderstackApp ? TTable extends InferTables<TApp> ? RealtimeChangeFor<TApp, TTable> : RealtimeChange : RealtimeChange;
38
+ /** Every exposed table name, or any string when no app type is supplied. */
39
+ export type RealtimeTableName<TApp> = [TApp] extends [never] ? string : TApp extends AnyBunderstackApp ? InferTables<TApp> : string;
40
+ export type RealtimeQueryApi = {
41
+ realtime: {
42
+ changes: RealtimeProcedure;
43
+ };
44
+ [table: string]: any;
45
+ };
46
+ /**
47
+ * How a change reaches the cache.
48
+ *
49
+ * `invalidate` (default) marks the table's queries stale and lets TanStack
50
+ * Query refetch them — always correct, one request per change.
51
+ *
52
+ * `patch` writes the change into cached list results instead, so a write costs
53
+ * no extra request. The event carries the action and the full row, and
54
+ * Bunderstack's list contract is narrow enough to decide membership locally:
55
+ * filters are `=`, `IN`, or `IS NULL`, and ordering is a single column. Any
56
+ * list where that is not decidable — a text search, or a page that is not the
57
+ * complete result — falls back to invalidation.
58
+ */
59
+ export type RealtimeApplyStrategy = 'invalidate' | 'patch';
60
+ export type RealtimeSyncOptions<TApp = never, TTable extends string = RealtimeTableName<TApp>> = {
61
+ api: RealtimeQueryApi;
62
+ queryClient: QueryClient;
63
+ tables: TTable[];
64
+ signal?: AbortSignal;
65
+ /** Defaults to `'invalidate'`. */
66
+ apply?: RealtimeApplyStrategy;
67
+ /**
68
+ * When buffered changes reach the cache. Defaults to `'frame'`, which
69
+ * collapses a burst into one cache write and one invalidation per table.
70
+ * Pass `'sync'` to write as each change arrives, as versions before 0.18 did.
71
+ */
72
+ notifyScheduler?: NotifyScheduler;
73
+ /** Timer functions, injectable for tests. */
74
+ clock?: RealtimeClock;
75
+ /** Initial reconnect delay before jitter. Defaults to 1 second. */
76
+ retryMs?: number;
77
+ /** Maximum reconnect delay before jitter. Defaults to 30 seconds. */
78
+ maxRetryMs?: number;
79
+ onChange?: (change: RealtimeChangeOf<TApp, TTable>) => void;
80
+ onReconnect?: () => void | Promise<void>;
81
+ onError?: (error: unknown) => void;
82
+ onRetry?: (retry: {
83
+ attempt: number;
84
+ delayMs: number;
85
+ }) => void;
86
+ };
87
+ export declare function syncRealtime<TApp = never, TTable extends string = RealtimeTableName<TApp>>(options: RealtimeSyncOptions<TApp, TTable>): RealtimeSyncHandle;
88
+ //# sourceMappingURL=realtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../../src/query/realtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,255 @@
1
+ import { hashKey, notifyManager } from '@tanstack/query-core';
2
+ import { createFlushScheduler } from './realtime-flush.js';
3
+ import { openRealtimeStream } from './realtime-stream.js';
4
+ function tableQueryKey(api, table) {
5
+ return api[table]?.key?.({ type: 'query' }) ?? [[table], { type: 'query' }];
6
+ }
7
+ function detailQueryKey(api, table, id) {
8
+ return (api[table]?.get?.queryKey?.({ input: { id } }) ?? [
9
+ [table, 'get'],
10
+ { type: 'query', input: { id } },
11
+ ]);
12
+ }
13
+ function listQueryKey(api, table) {
14
+ return (api[table]?.list?.key?.({ type: 'query' }) ?? [
15
+ [table, 'list'],
16
+ { type: 'query' },
17
+ ]);
18
+ }
19
+ function isListSnapshot(value) {
20
+ return (typeof value === 'object' &&
21
+ value !== null &&
22
+ Array.isArray(value.items));
23
+ }
24
+ /** Dates arrive as Dates over the wire, so identity is not enough. */
25
+ function sameValue(a, b) {
26
+ if (a instanceof Date && b instanceof Date)
27
+ return a.getTime() === b.getTime();
28
+ return a === b;
29
+ }
30
+ /**
31
+ * Bunderstack's filter contract is narrow by design — a scalar is `=`, an
32
+ * array is `IN`, and `null` is `IS NULL` — so membership is decidable against
33
+ * a single record without asking the server.
34
+ */
35
+ function matchesFilters(record, filters) {
36
+ for (const [column, expected] of Object.entries(filters ?? {})) {
37
+ if (expected === undefined)
38
+ continue;
39
+ const actual = record[column];
40
+ if (expected === null) {
41
+ if (actual !== null && actual !== undefined)
42
+ return false;
43
+ continue;
44
+ }
45
+ if (Array.isArray(expected)) {
46
+ if (!expected.some((value) => sameValue(value, actual)))
47
+ return false;
48
+ continue;
49
+ }
50
+ if (!sameValue(expected, actual))
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+ function compareValues(a, b) {
56
+ if (a instanceof Date && b instanceof Date)
57
+ return a.getTime() - b.getTime();
58
+ if (typeof a === 'number' && typeof b === 'number')
59
+ return a - b;
60
+ if (typeof a === 'boolean' && typeof b === 'boolean')
61
+ return Number(a) - Number(b);
62
+ return String(a).localeCompare(String(b));
63
+ }
64
+ /**
65
+ * Where the row goes in an already-sorted page.
66
+ *
67
+ * Ties are common rather than exotic: a `timestamp` column stores seconds, so
68
+ * rows written in the same second share a sort value. SQL does not define an
69
+ * order within a tie, so the new row is placed as the most recent of its
70
+ * equals — first under `desc`, last under `asc` — which is where a reader
71
+ * expects a just-written row to appear.
72
+ */
73
+ function insertionIndex(items, record, sort, order) {
74
+ const value = record[sort];
75
+ const index = items.findIndex((item) => {
76
+ const comparison = compareValues(value, item[sort]);
77
+ return order === 'desc' ? comparison >= 0 : comparison < 0;
78
+ });
79
+ return index === -1 ? items.length : index;
80
+ }
81
+ /**
82
+ * A cached page can only absorb an insert when it already holds every row the
83
+ * query would return. A partial page cannot: the new row may belong to another
84
+ * page, and inserting would shift the boundary the server drew.
85
+ */
86
+ function holdsCompleteResult(snapshot) {
87
+ return (snapshot.hasMore === false &&
88
+ !snapshot.cursor &&
89
+ (snapshot.offset === undefined || snapshot.offset === 0));
90
+ }
91
+ function readInput(queryKey) {
92
+ const meta = queryKey[1];
93
+ const input = meta?.input;
94
+ return typeof input === 'object' && input !== null
95
+ ? input
96
+ : {};
97
+ }
98
+ export function syncRealtime(options) {
99
+ const scheduler = createFlushScheduler(options.notifyScheduler ?? 'frame', options.clock);
100
+ /** Changes waiting for the next flush, in arrival order. */
101
+ const buffered = [];
102
+ /**
103
+ * Query keys the flush must invalidate, deduplicated by their hash. Keyed
104
+ * this way rather than by table so a list that cannot absorb a change still
105
+ * invalidates only itself, while a burst against one key collapses to one
106
+ * call.
107
+ */
108
+ const stale = new Map();
109
+ const invalidateAll = async () => {
110
+ await Promise.all(options.tables.map((table) => options.queryClient.invalidateQueries({
111
+ queryKey: tableQueryKey(options.api, table),
112
+ })));
113
+ };
114
+ /**
115
+ * Write the change into every cached list for the table, marking the table
116
+ * stale for any list whose membership or ordering cannot be settled locally.
117
+ * A list is only patched when the answer is certain.
118
+ */
119
+ const patchLists = (change) => {
120
+ const prefix = listQueryKey(options.api, change.table);
121
+ const queries = options.queryClient
122
+ .getQueryCache()
123
+ .findAll({ queryKey: prefix });
124
+ const id = change.record['id'];
125
+ for (const query of queries) {
126
+ const snapshot = query.state.data;
127
+ if (!isListSnapshot(snapshot))
128
+ continue;
129
+ const queryKey = query.queryKey;
130
+ const items = snapshot.items;
131
+ const present = items.some((item) => sameValue(item['id'], id));
132
+ if (change.action === 'delete') {
133
+ if (!present)
134
+ continue;
135
+ options.queryClient.setQueryData(queryKey, {
136
+ ...snapshot,
137
+ items: items.filter((item) => !sameValue(item['id'], id)),
138
+ ...(typeof snapshot.total === 'number'
139
+ ? { total: Math.max(0, snapshot.total - 1) }
140
+ : {}),
141
+ });
142
+ continue;
143
+ }
144
+ const input = readInput(queryKey);
145
+ const belongs = matchesFilters(change.record, input['filters']);
146
+ // An update can move a row out of a filtered list.
147
+ if (!belongs) {
148
+ if (!present)
149
+ continue;
150
+ options.queryClient.setQueryData(queryKey, {
151
+ ...snapshot,
152
+ items: items.filter((item) => !sameValue(item['id'], id)),
153
+ ...(typeof snapshot.total === 'number'
154
+ ? { total: Math.max(0, snapshot.total - 1) }
155
+ : {}),
156
+ });
157
+ continue;
158
+ }
159
+ // Already here: replace in place and keep the row where it is.
160
+ if (present) {
161
+ options.queryClient.setQueryData(queryKey, {
162
+ ...snapshot,
163
+ items: items.map((item) => sameValue(item['id'], id) ? change.record : item),
164
+ });
165
+ continue;
166
+ }
167
+ // The row joins this list. Only safe on a page that holds everything,
168
+ // and only when `q` is absent — a text match needs the server's
169
+ // searchable columns to evaluate.
170
+ const searched = snapshot.q ?? input['q'];
171
+ const sort = snapshot.sort ?? input['sort'];
172
+ const order = snapshot.order ?? input['order'];
173
+ if (searched || !sort || !order || !holdsCompleteResult(snapshot)) {
174
+ stale.set(hashKey(queryKey), queryKey);
175
+ continue;
176
+ }
177
+ const next = items.slice();
178
+ next.splice(insertionIndex(items, change.record, sort, order), 0, change.record);
179
+ options.queryClient.setQueryData(queryKey, {
180
+ ...snapshot,
181
+ items: next,
182
+ ...(typeof snapshot.total === 'number'
183
+ ? { total: snapshot.total + 1 }
184
+ : {}),
185
+ });
186
+ }
187
+ };
188
+ const applyOne = (change) => {
189
+ // The runtime shape is the same either way; the generic only narrows what
190
+ // callers see.
191
+ options.onChange?.(change);
192
+ const id = change.record['id'];
193
+ if (id !== undefined) {
194
+ const queryKey = detailQueryKey(options.api, change.table, id);
195
+ if (change.action === 'delete')
196
+ options.queryClient.removeQueries({ queryKey });
197
+ else
198
+ options.queryClient.setQueryData(queryKey, change.record);
199
+ }
200
+ if (options.apply === 'patch') {
201
+ patchLists(change);
202
+ return;
203
+ }
204
+ const tableKey = tableQueryKey(options.api, change.table);
205
+ stale.set(hashKey(tableKey), tableKey);
206
+ };
207
+ /**
208
+ * One pass over the buffer, then one invalidation per stale table. The
209
+ * second part is the point: a burst of changes to a table used to issue one
210
+ * `invalidateQueries` each, and now issues one in total.
211
+ */
212
+ const flush = () => {
213
+ if (buffered.length === 0)
214
+ return;
215
+ const batch = buffered.splice(0, buffered.length);
216
+ notifyManager.batch(() => {
217
+ for (const change of batch)
218
+ applyOne(change);
219
+ for (const queryKey of stale.values())
220
+ void options.queryClient.invalidateQueries({ queryKey });
221
+ stale.clear();
222
+ });
223
+ };
224
+ const stream = openRealtimeStream({
225
+ subscribe: ({ signal, lastEventId }) => options.api.realtime.changes.call({ tables: options.tables }, { signal, lastEventId }),
226
+ onChange: (change) => {
227
+ buffered.push(change);
228
+ scheduler.schedule(flush);
229
+ },
230
+ onReconnect: async () => {
231
+ // A refetch of everything supersedes whatever was still buffered.
232
+ scheduler.cancel();
233
+ buffered.length = 0;
234
+ stale.clear();
235
+ await invalidateAll();
236
+ await options.onReconnect?.();
237
+ },
238
+ onError: options.onError,
239
+ onRetry: options.onRetry,
240
+ signal: options.signal ?? new AbortController().signal,
241
+ retryMs: options.retryMs,
242
+ maxRetryMs: options.maxRetryMs,
243
+ clock: options.clock,
244
+ });
245
+ return {
246
+ close: () => {
247
+ stream.close();
248
+ // Anything already delivered is still owed to the cache.
249
+ scheduler.cancel();
250
+ flush();
251
+ },
252
+ done: stream.done,
253
+ };
254
+ }
255
+ //# sourceMappingURL=realtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.js","sourceRoot":"","sources":["../../src/query/realtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type { QueryClient, QueryKey } from '@tanstack/query-core'\n\nimport { hashKey, notifyManager } from '@tanstack/query-core'\n\nimport type {\n AnyBunderstackApp,\n InferSchema,\n InferSelect,\n InferTables,\n} from './infer'\nimport type { NotifyScheduler, RealtimeClock } from './realtime-flush'\nimport type {\n RealtimeAction,\n RealtimeChange,\n RealtimeProcedure,\n RealtimeSyncHandle,\n} from './realtime-stream'\n\nimport { createFlushScheduler } from './realtime-flush'\nimport { openRealtimeStream } from './realtime-stream'\n\n// The event types live with the stream that produces them; re-exported here so\n// the package's public surface is unchanged.\nexport type { NotifyScheduler, RealtimeClock } from './realtime-flush'\nexport type {\n RealtimeAction,\n RealtimeChange,\n RealtimeEvent,\n RealtimeHeartbeat,\n RealtimeProcedure,\n RealtimeSyncHandle,\n} from './realtime-stream'\n\n/**\n * The change for one table, with `record` typed as that table's row.\n *\n * Passing the app type to `syncRealtime` turns `tables` into a checked list of\n * exposed table names and `onChange` into a union discriminated by `table`, so\n * `change.table === 'todos'` narrows `change.record` to the todo row:\n *\n * ```ts\n * syncRealtime<App>({\n * api,\n * queryClient,\n * tables: ['todos'],\n * onChange: (change) => {\n * if (change.table === 'todos') console.log(change.record.done)\n * },\n * })\n * ```\n */\nexport type RealtimeChangeFor<\n TApp extends AnyBunderstackApp,\n TTable extends InferTables<TApp>,\n> = {\n table: TTable\n action: RealtimeAction\n record: InferSelect<InferSchema<TApp>[TTable]>\n operationId?: string\n}\n\n/**\n * Untyped when no app type is supplied, so existing callers — and consumers\n * that only have a structural client — keep the loose `RealtimeChange`.\n */\nexport type RealtimeChangeOf<TApp, TTable extends string> = [TApp] extends [\n never,\n]\n ? RealtimeChange\n : TApp extends AnyBunderstackApp\n ? TTable extends InferTables<TApp>\n ? RealtimeChangeFor<TApp, TTable>\n : RealtimeChange\n : RealtimeChange\n\n/** Every exposed table name, or any string when no app type is supplied. */\nexport type RealtimeTableName<TApp> = [TApp] extends [never]\n ? string\n : TApp extends AnyBunderstackApp\n ? InferTables<TApp>\n : string\n\nexport type RealtimeQueryApi = {\n realtime: { changes: RealtimeProcedure }\n [table: string]: any\n}\n\n/**\n * How a change reaches the cache.\n *\n * `invalidate` (default) marks the table's queries stale and lets TanStack\n * Query refetch them — always correct, one request per change.\n *\n * `patch` writes the change into cached list results instead, so a write costs\n * no extra request. The event carries the action and the full row, and\n * Bunderstack's list contract is narrow enough to decide membership locally:\n * filters are `=`, `IN`, or `IS NULL`, and ordering is a single column. Any\n * list where that is not decidable — a text search, or a page that is not the\n * complete result — falls back to invalidation.\n */\nexport type RealtimeApplyStrategy = 'invalidate' | 'patch'\n\nexport type RealtimeSyncOptions<\n TApp = never,\n TTable extends string = RealtimeTableName<TApp>,\n> = {\n api: RealtimeQueryApi\n queryClient: QueryClient\n tables: TTable[]\n signal?: AbortSignal\n /** Defaults to `'invalidate'`. */\n apply?: RealtimeApplyStrategy\n /**\n * When buffered changes reach the cache. Defaults to `'frame'`, which\n * collapses a burst into one cache write and one invalidation per table.\n * Pass `'sync'` to write as each change arrives, as versions before 0.18 did.\n */\n notifyScheduler?: NotifyScheduler\n /** Timer functions, injectable for tests. */\n clock?: RealtimeClock\n /** Initial reconnect delay before jitter. Defaults to 1 second. */\n retryMs?: number\n /** Maximum reconnect delay before jitter. Defaults to 30 seconds. */\n maxRetryMs?: number\n onChange?: (change: RealtimeChangeOf<TApp, TTable>) => void\n onReconnect?: () => void | Promise<void>\n onError?: (error: unknown) => void\n onRetry?: (retry: { attempt: number; delayMs: number }) => void\n}\n\nfunction tableQueryKey(api: RealtimeQueryApi, table: string): QueryKey {\n return api[table]?.key?.({ type: 'query' }) ?? [[table], { type: 'query' }]\n}\n\nfunction detailQueryKey(\n api: RealtimeQueryApi,\n table: string,\n id: unknown,\n): QueryKey {\n return (\n api[table]?.get?.queryKey?.({ input: { id } }) ?? [\n [table, 'get'],\n { type: 'query', input: { id } },\n ]\n )\n}\n\nfunction listQueryKey(api: RealtimeQueryApi, table: string): QueryKey {\n return (\n api[table]?.list?.key?.({ type: 'query' }) ?? [\n [table, 'list'],\n { type: 'query' },\n ]\n )\n}\n\n/** The shape every generated list endpoint returns, as far as patching cares. */\ntype ListSnapshot = {\n items: Record<string, unknown>[]\n hasMore?: boolean\n total?: number\n offset?: number\n cursor?: string\n q?: string\n sort?: string\n order?: 'asc' | 'desc'\n}\n\nfunction isListSnapshot(value: unknown): value is ListSnapshot {\n return (\n typeof value === 'object' &&\n value !== null &&\n Array.isArray((value as ListSnapshot).items)\n )\n}\n\n/** Dates arrive as Dates over the wire, so identity is not enough. */\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()\n return a === b\n}\n\n/**\n * Bunderstack's filter contract is narrow by design — a scalar is `=`, an\n * array is `IN`, and `null` is `IS NULL` — so membership is decidable against\n * a single record without asking the server.\n */\nfunction matchesFilters(\n record: Record<string, unknown>,\n filters: Record<string, unknown> | undefined,\n): boolean {\n for (const [column, expected] of Object.entries(filters ?? {})) {\n if (expected === undefined) continue\n const actual = record[column]\n if (expected === null) {\n if (actual !== null && actual !== undefined) return false\n continue\n }\n if (Array.isArray(expected)) {\n if (!expected.some((value) => sameValue(value, actual))) return false\n continue\n }\n if (!sameValue(expected, actual)) return false\n }\n return true\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (typeof a === 'boolean' && typeof b === 'boolean')\n return Number(a) - Number(b)\n return String(a).localeCompare(String(b))\n}\n\n/**\n * Where the row goes in an already-sorted page.\n *\n * Ties are common rather than exotic: a `timestamp` column stores seconds, so\n * rows written in the same second share a sort value. SQL does not define an\n * order within a tie, so the new row is placed as the most recent of its\n * equals — first under `desc`, last under `asc` — which is where a reader\n * expects a just-written row to appear.\n */\nfunction insertionIndex(\n items: Record<string, unknown>[],\n record: Record<string, unknown>,\n sort: string,\n order: 'asc' | 'desc',\n): number {\n const value = record[sort]\n const index = items.findIndex((item) => {\n const comparison = compareValues(value, item[sort])\n return order === 'desc' ? comparison >= 0 : comparison < 0\n })\n return index === -1 ? items.length : index\n}\n\n/**\n * A cached page can only absorb an insert when it already holds every row the\n * query would return. A partial page cannot: the new row may belong to another\n * page, and inserting would shift the boundary the server drew.\n */\nfunction holdsCompleteResult(snapshot: ListSnapshot): boolean {\n return (\n snapshot.hasMore === false &&\n !snapshot.cursor &&\n (snapshot.offset === undefined || snapshot.offset === 0)\n )\n}\n\nfunction readInput(queryKey: QueryKey): Record<string, unknown> {\n const meta = queryKey[1] as { input?: unknown } | undefined\n const input = meta?.input\n return typeof input === 'object' && input !== null\n ? (input as Record<string, unknown>)\n : {}\n}\n\nexport function syncRealtime<\n TApp = never,\n TTable extends string = RealtimeTableName<TApp>,\n>(options: RealtimeSyncOptions<TApp, TTable>): RealtimeSyncHandle {\n const scheduler = createFlushScheduler(\n options.notifyScheduler ?? 'frame',\n options.clock,\n )\n /** Changes waiting for the next flush, in arrival order. */\n const buffered: RealtimeChange[] = []\n /**\n * Query keys the flush must invalidate, deduplicated by their hash. Keyed\n * this way rather than by table so a list that cannot absorb a change still\n * invalidates only itself, while a burst against one key collapses to one\n * call.\n */\n const stale = new Map<string, QueryKey>()\n\n const invalidateAll = async () => {\n await Promise.all(\n options.tables.map((table) =>\n options.queryClient.invalidateQueries({\n queryKey: tableQueryKey(options.api, table),\n }),\n ),\n )\n }\n\n /**\n * Write the change into every cached list for the table, marking the table\n * stale for any list whose membership or ordering cannot be settled locally.\n * A list is only patched when the answer is certain.\n */\n const patchLists = (change: RealtimeChange) => {\n const prefix = listQueryKey(options.api, change.table)\n const queries = options.queryClient\n .getQueryCache()\n .findAll({ queryKey: prefix })\n const id = change.record['id']\n\n for (const query of queries) {\n const snapshot = query.state.data\n if (!isListSnapshot(snapshot)) continue\n\n const queryKey = query.queryKey\n const items = snapshot.items\n const present = items.some((item) => sameValue(item['id'], id))\n\n if (change.action === 'delete') {\n if (!present) continue\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: items.filter((item) => !sameValue(item['id'], id)),\n ...(typeof snapshot.total === 'number'\n ? { total: Math.max(0, snapshot.total - 1) }\n : {}),\n })\n continue\n }\n\n const input = readInput(queryKey)\n const belongs = matchesFilters(\n change.record,\n input['filters'] as Record<string, unknown> | undefined,\n )\n\n // An update can move a row out of a filtered list.\n if (!belongs) {\n if (!present) continue\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: items.filter((item) => !sameValue(item['id'], id)),\n ...(typeof snapshot.total === 'number'\n ? { total: Math.max(0, snapshot.total - 1) }\n : {}),\n })\n continue\n }\n\n // Already here: replace in place and keep the row where it is.\n if (present) {\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: items.map((item) =>\n sameValue(item['id'], id) ? change.record : item,\n ),\n })\n continue\n }\n\n // The row joins this list. Only safe on a page that holds everything,\n // and only when `q` is absent — a text match needs the server's\n // searchable columns to evaluate.\n const searched = snapshot.q ?? (input['q'] as string | undefined)\n const sort = snapshot.sort ?? (input['sort'] as string | undefined)\n const order =\n snapshot.order ?? (input['order'] as 'asc' | 'desc' | undefined)\n\n if (searched || !sort || !order || !holdsCompleteResult(snapshot)) {\n stale.set(hashKey(queryKey), queryKey)\n continue\n }\n\n const next = items.slice()\n next.splice(\n insertionIndex(items, change.record, sort, order),\n 0,\n change.record,\n )\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: next,\n ...(typeof snapshot.total === 'number'\n ? { total: snapshot.total + 1 }\n : {}),\n })\n }\n }\n\n const applyOne = (change: RealtimeChange) => {\n // The runtime shape is the same either way; the generic only narrows what\n // callers see.\n options.onChange?.(change as RealtimeChangeOf<TApp, TTable>)\n const id = change.record['id']\n if (id !== undefined) {\n const queryKey = detailQueryKey(options.api, change.table, id)\n if (change.action === 'delete')\n options.queryClient.removeQueries({ queryKey })\n else options.queryClient.setQueryData(queryKey, change.record)\n }\n\n if (options.apply === 'patch') {\n patchLists(change)\n return\n }\n\n const tableKey = tableQueryKey(options.api, change.table)\n stale.set(hashKey(tableKey), tableKey)\n }\n\n /**\n * One pass over the buffer, then one invalidation per stale table. The\n * second part is the point: a burst of changes to a table used to issue one\n * `invalidateQueries` each, and now issues one in total.\n */\n const flush = () => {\n if (buffered.length === 0) return\n const batch = buffered.splice(0, buffered.length)\n notifyManager.batch(() => {\n for (const change of batch) applyOne(change)\n for (const queryKey of stale.values())\n void options.queryClient.invalidateQueries({ queryKey })\n stale.clear()\n })\n }\n\n const stream = openRealtimeStream({\n subscribe: ({ signal, lastEventId }) =>\n options.api.realtime.changes.call(\n { tables: options.tables },\n { signal, lastEventId },\n ),\n onChange: (change) => {\n buffered.push(change)\n scheduler.schedule(flush)\n },\n onReconnect: async () => {\n // A refetch of everything supersedes whatever was still buffered.\n scheduler.cancel()\n buffered.length = 0\n stale.clear()\n await invalidateAll()\n await options.onReconnect?.()\n },\n onError: options.onError,\n onRetry: options.onRetry,\n signal: options.signal ?? new AbortController().signal,\n retryMs: options.retryMs,\n maxRetryMs: options.maxRetryMs,\n clock: options.clock,\n })\n\n return {\n close: () => {\n stream.close()\n // Anything already delivered is still owed to the cache.\n scheduler.cancel()\n flush()\n },\n done: stream.done,\n }\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * BetterAuth browser client pointed at the Bunderstack handler's
3
+ * /api/auth/* routes. Defaults to the current origin in the browser and
4
+ * APP_URL (or localhost:3000) during SSR.
5
+ */
6
+ export declare function createStartAuthClient(options?: {
7
+ baseURL?: string;
8
+ }): import("better-auth/react").ReactAuthClient<{
9
+ baseURL: string;
10
+ }>;
11
+ //# sourceMappingURL=auth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../src/start/auth-client.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO;;GAQvE"}
@@ -0,0 +1,15 @@
1
+ import { createAuthClient } from 'better-auth/react';
2
+ /**
3
+ * BetterAuth browser client pointed at the Bunderstack handler's
4
+ * /api/auth/* routes. Defaults to the current origin in the browser and
5
+ * APP_URL (or localhost:3000) during SSR.
6
+ */
7
+ export function createStartAuthClient(options = {}) {
8
+ return createAuthClient({
9
+ baseURL: options.baseURL ??
10
+ (typeof window !== 'undefined'
11
+ ? window.location.origin
12
+ : (process.env.APP_URL ?? 'http://localhost:3000')),
13
+ });
14
+ }
15
+ //# sourceMappingURL=auth-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-client.js","sourceRoot":"","sources":["../../src/start/auth-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAAgC,EAAE;IACtE,OAAO,gBAAgB,CAAC;QACtB,OAAO,EACL,OAAO,CAAC,OAAO;YACf,CAAC,OAAO,MAAM,KAAK,WAAW;gBAC5B,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM;gBACxB,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,uBAAuB,CAAC,CAAC;KACxD,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { createAuthClient } from 'better-auth/react'\n\n/**\n * BetterAuth browser client pointed at the Bunderstack handler's\n * /api/auth/* routes. Defaults to the current origin in the browser and\n * APP_URL (or localhost:3000) during SSR.\n */\nexport function createStartAuthClient(options: { baseURL?: string } = {}) {\n return createAuthClient({\n baseURL:\n options.baseURL ??\n (typeof window !== 'undefined'\n ? window.location.origin\n : (process.env.APP_URL ?? 'http://localhost:3000')),\n })\n}\n"]}