bunderstack 0.20.0 → 0.21.0

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 (114) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/api/crud-router.d.ts +4 -4
  3. package/dist/api/realtime-router.d.ts +1 -1
  4. package/dist/client/client.d.ts +53 -0
  5. package/dist/client/client.d.ts.map +1 -0
  6. package/dist/client/client.js +111 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +4 -0
  9. package/dist/client/index.d.ts.map +1 -0
  10. package/dist/client/index.js +4 -0
  11. package/dist/client/index.js.map +1 -0
  12. package/dist/client/live-view.d.ts +49 -0
  13. package/dist/client/live-view.d.ts.map +1 -0
  14. package/dist/client/live-view.js +184 -0
  15. package/dist/client/live-view.js.map +1 -0
  16. package/dist/client/react.d.ts +4 -0
  17. package/dist/client/react.d.ts.map +1 -0
  18. package/dist/client/react.js +6 -0
  19. package/dist/client/react.js.map +1 -0
  20. package/dist/client/realtime.d.ts +54 -0
  21. package/dist/client/realtime.d.ts.map +1 -0
  22. package/dist/client/realtime.js +105 -0
  23. package/dist/client/realtime.js.map +1 -0
  24. package/dist/client/rest.d.ts +2 -0
  25. package/dist/client/rest.d.ts.map +1 -0
  26. package/dist/client/rest.js +2 -0
  27. package/dist/client/rest.js.map +1 -0
  28. package/dist/client/rpc-client.d.ts +38 -0
  29. package/dist/client/rpc-client.d.ts.map +1 -0
  30. package/dist/client/rpc-client.js +59 -0
  31. package/dist/client/rpc-client.js.map +1 -0
  32. package/dist/client/solid.d.ts +7 -0
  33. package/dist/client/solid.d.ts.map +1 -0
  34. package/dist/client/solid.js +21 -0
  35. package/dist/client/solid.js.map +1 -0
  36. package/dist/client/svelte.d.ts +6 -0
  37. package/dist/client/svelte.d.ts.map +1 -0
  38. package/dist/client/svelte.js +9 -0
  39. package/dist/client/svelte.js.map +1 -0
  40. package/dist/client/vue.d.ts +5 -0
  41. package/dist/client/vue.d.ts.map +1 -0
  42. package/dist/client/vue.js +11 -0
  43. package/dist/client/vue.js.map +1 -0
  44. package/dist/query/api.d.ts +10 -0
  45. package/dist/query/api.d.ts.map +1 -0
  46. package/dist/query/api.js +7 -0
  47. package/dist/query/api.js.map +1 -0
  48. package/dist/query/client.d.ts +33 -0
  49. package/dist/query/client.d.ts.map +1 -0
  50. package/dist/query/client.js +98 -0
  51. package/dist/query/client.js.map +1 -0
  52. package/dist/query/fetch.d.ts +3 -0
  53. package/dist/query/fetch.d.ts.map +1 -0
  54. package/dist/query/fetch.js +4 -0
  55. package/dist/query/fetch.js.map +1 -0
  56. package/dist/query/index.d.ts +8 -0
  57. package/dist/query/index.d.ts.map +1 -0
  58. package/dist/query/index.js +4 -0
  59. package/dist/query/index.js.map +1 -0
  60. package/dist/query/infer.d.ts +25 -0
  61. package/dist/query/infer.d.ts.map +1 -0
  62. package/dist/query/infer.js +2 -0
  63. package/dist/query/infer.js.map +1 -0
  64. package/dist/query/react.d.ts +3 -0
  65. package/dist/query/react.d.ts.map +1 -0
  66. package/dist/query/react.js +2 -0
  67. package/dist/query/react.js.map +1 -0
  68. package/dist/query/realtime-flush.d.ts +26 -0
  69. package/dist/query/realtime-flush.d.ts.map +1 -0
  70. package/dist/query/realtime-flush.js +45 -0
  71. package/dist/query/realtime-flush.js.map +1 -0
  72. package/dist/query/realtime-stream.d.ts +3 -0
  73. package/dist/query/realtime-stream.d.ts.map +1 -0
  74. package/dist/query/realtime-stream.js +3 -0
  75. package/dist/query/realtime-stream.js.map +1 -0
  76. package/dist/query/realtime.d.ts +88 -0
  77. package/dist/query/realtime.d.ts.map +1 -0
  78. package/dist/query/realtime.js +255 -0
  79. package/dist/query/realtime.js.map +1 -0
  80. package/dist/start/auth-client.d.ts +11 -0
  81. package/dist/start/auth-client.d.ts.map +1 -0
  82. package/dist/start/auth-client.js +15 -0
  83. package/dist/start/auth-client.js.map +1 -0
  84. package/dist/start/index.d.ts +66 -0
  85. package/dist/start/index.d.ts.map +1 -0
  86. package/dist/start/index.js +49 -0
  87. package/dist/start/index.js.map +1 -0
  88. package/dist/start/isomorphic-fetch.d.ts +13 -0
  89. package/dist/start/isomorphic-fetch.d.ts.map +1 -0
  90. package/dist/start/isomorphic-fetch.js +33 -0
  91. package/dist/start/isomorphic-fetch.js.map +1 -0
  92. package/dist/sync/collection.d.ts +106 -0
  93. package/dist/sync/collection.d.ts.map +1 -0
  94. package/dist/sync/collection.js +258 -0
  95. package/dist/sync/collection.js.map +1 -0
  96. package/dist/sync/index.d.ts +8 -0
  97. package/dist/sync/index.d.ts.map +1 -0
  98. package/dist/sync/index.js +4 -0
  99. package/dist/sync/index.js.map +1 -0
  100. package/dist/sync/realtime-sync.d.ts +31 -0
  101. package/dist/sync/realtime-sync.d.ts.map +1 -0
  102. package/dist/sync/realtime-sync.js +46 -0
  103. package/dist/sync/realtime-sync.js.map +1 -0
  104. package/dist/sync/sync-client.d.ts +28 -0
  105. package/dist/sync/sync-client.d.ts.map +1 -0
  106. package/dist/sync/sync-client.js +52 -0
  107. package/dist/sync/sync-client.js.map +1 -0
  108. package/dist/sync/update-queue.d.ts +27 -0
  109. package/dist/sync/update-queue.d.ts.map +1 -0
  110. package/dist/sync/update-queue.js +66 -0
  111. package/dist/sync/update-queue.js.map +1 -0
  112. package/package.json +103 -3
  113. package/skills/creating-bunderstack-apps/references/runtime-integrations.md +1 -1
  114. package/skills/migrating-to-bunderstack/references/runtime-replacements.md +2 -2
package/CHANGELOG.md CHANGED
@@ -2,7 +2,23 @@
2
2
 
3
3
  All notable changes to `bunderstack` will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.21.0] - 2026-08-25
6
+
7
+ ### Changed
8
+
9
+ - **Consolidated package architecture.** Merged `bunderstack-client`,
10
+ `bunderstack-query`, `bunderstack-sync`, and `bunderstack-start` into the single
11
+ `bunderstack` npm package. Consumers now install only `bunderstack` and import
12
+ modules via clean subpath exports:
13
+ - `bunderstack/client` (core RPC client & framework-neutral `LiveView`)
14
+ - `bunderstack/client/rest` (type-safe REST client)
15
+ - `bunderstack/client/react`, `bunderstack/client/solid`, `bunderstack/client/svelte`, `bunderstack/client/vue` (UI framework live view bindings)
16
+ - `bunderstack/query` & `bunderstack/query/react` (TanStack Query client and hooks)
17
+ - `bunderstack/sync` (TanStack DB client with realtime collections)
18
+ - `bunderstack/start` & `bunderstack/start/auth` (TanStack Start full-stack integration)
19
+ - Framework integrations (`react`, `solid-js`, `vue`, `@tanstack/react-query`,
20
+ `@tanstack/react-start`, `@tanstack/db`, etc.) and database drivers remain
21
+ optional peer dependencies.
6
22
 
7
23
  ## [0.20.0] - 2026-08-25
8
24
 
@@ -189,7 +189,7 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
189
189
  readonly code: "TOO_MANY_REQUESTS";
190
190
  }>;
191
191
  };
192
- }, Extract<TTable["$inferSelect"], import("@orpc/server").AnyORPCError>>;
192
+ }, Extract<TTable["$inferSelect"], import("@orpc/client").AnyORPCError>>;
193
193
  create: import("@orpc/server").DecoratedProcedure<import("./context.js").ApiContext<TSchema, Record<string, unknown>> & object, object, v.GenericSchema<Partial<TTable["$inferInsert"]>, Partial<TTable["$inferInsert"]>>, v.GenericSchema<TTable["$inferSelect"], TTable["$inferSelect"]>, {
194
194
  readonly BAD_REQUEST: {
195
195
  readonly data: v.OptionalSchema<v.StrictObjectSchema<{
@@ -247,7 +247,7 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
247
247
  readonly code: "TOO_MANY_REQUESTS";
248
248
  }>;
249
249
  };
250
- }, Extract<TTable["$inferSelect"], import("@orpc/server").AnyORPCError>>;
250
+ }, Extract<TTable["$inferSelect"], import("@orpc/client").AnyORPCError>>;
251
251
  update: import("@orpc/server").DecoratedProcedure<import("./context.js").ApiContext<TSchema, Record<string, unknown>> & object, object, v.GenericSchema<{
252
252
  id: string;
253
253
  } & Partial<Omit<TTable["$inferInsert"], "id">>, {
@@ -309,7 +309,7 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
309
309
  readonly code: "TOO_MANY_REQUESTS";
310
310
  }>;
311
311
  };
312
- }, Extract<TTable["$inferSelect"], import("@orpc/server").AnyORPCError>>;
312
+ }, Extract<TTable["$inferSelect"], import("@orpc/client").AnyORPCError>>;
313
313
  delete: import("@orpc/server").DecoratedProcedure<import("./context.js").ApiContext<TSchema, Record<string, unknown>> & object, object, v.StrictObjectSchema<{
314
314
  readonly id: v.StringSchema<undefined>;
315
315
  }, undefined>, v.UndefinedSchema<undefined>, {
@@ -390,7 +390,7 @@ export declare function buildTableCrudProcedures<TSchema extends Record<string,
390
390
  } | {
391
391
  type: "heartbeat";
392
392
  intervalMs: number;
393
- }, unknown, void>, import("@orpc/server").AsyncIteratorClass<{
393
+ }, unknown, void>, import("@orpc/client").AsyncIteratorClass<{
394
394
  type: "snapshot";
395
395
  items: TTable["$inferSelect"][];
396
396
  sort: string;
@@ -19,7 +19,7 @@ export declare function buildRealtimeApiRouter(publisher: RealtimePublisher | un
19
19
  } | {
20
20
  type: "heartbeat";
21
21
  intervalMs: number;
22
- }, unknown, void>, import("@orpc/server").AsyncIteratorClass<{
22
+ }, unknown, void>, import("@orpc/client").AsyncIteratorClass<{
23
23
  table: string;
24
24
  action: "create" | "update" | "delete";
25
25
  record: {
@@ -0,0 +1,53 @@
1
+ import { type CallOptions, type Fetch } from './rpc-client.js';
2
+ export type RouteOperation<TMethod extends string, TPath extends string, TQuery = undefined, TBody = undefined, TOutput = undefined, TStream extends boolean = false> = {
3
+ method: TMethod;
4
+ path: TPath;
5
+ query?: TQuery;
6
+ body?: TBody;
7
+ output?: TOutput;
8
+ stream?: TStream;
9
+ };
10
+ type AnyOperation = {
11
+ method: string;
12
+ path: string;
13
+ query?: unknown;
14
+ body?: unknown;
15
+ output?: unknown;
16
+ stream?: boolean;
17
+ };
18
+ type QueryOf<O extends AnyOperation> = NonNullable<O['query']>;
19
+ type BodyOf<O extends AnyOperation> = NonNullable<O['body']>;
20
+ type OutputOf<O extends AnyOperation> = NonNullable<O['output']>;
21
+ export type RouteCallOptions = CallOptions;
22
+ type PathParamNames<TPath extends string> = TPath extends `${string}{${infer TParam}}${infer TRest}` ? TParam extends `+${infer TName}` ? TName | PathParamNames<TRest> : TParam | PathParamNames<TRest> : never;
23
+ type PathArgs<O extends AnyOperation> = [PathParamNames<O['path']>] extends [
24
+ never
25
+ ] ? object : {
26
+ params: Record<PathParamNames<O['path']>, string>;
27
+ };
28
+ type QueryArgs<O extends AnyOperation> = [O['query']] extends [undefined] ? object : {
29
+ query?: QueryOf<O>;
30
+ };
31
+ type BodyArgs<O extends AnyOperation> = [O['body']] extends [undefined] ? object : {
32
+ body: BodyOf<O>;
33
+ };
34
+ type CallArgs<O extends AnyOperation> = PathArgs<O> & QueryArgs<O> & BodyArgs<O>;
35
+ type HasRequiredArgs<O extends AnyOperation> = [
36
+ PathParamNames<O['path']>
37
+ ] extends [never] ? [O['body']] extends [undefined] ? false : true : true;
38
+ export type RouteMethod<O extends AnyOperation> = (...args: true extends HasRequiredArgs<O> ? [args: CallArgs<O>, options?: RouteCallOptions] : [args?: CallArgs<O>, options?: RouteCallOptions]) => true extends O['stream'] ? AsyncIterable<OutputOf<O>> : Promise<OutputOf<O>>;
39
+ export type ClientOf<TRoutes> = {
40
+ [K in keyof TRoutes]: TRoutes[K] extends AnyOperation ? RouteMethod<TRoutes[K]> : never;
41
+ };
42
+ export type RestClientOptions = {
43
+ baseUrl?: string;
44
+ fetch?: Fetch;
45
+ };
46
+ type RouteDescriptor = {
47
+ method: string;
48
+ path: string;
49
+ stream?: boolean;
50
+ };
51
+ export declare function createRestClient<TRoutes>(routes: Record<string, RouteDescriptor>, options?: RestClientOptions): ClientOf<TRoutes>;
52
+ export {};
53
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { OPERATION_ID_HEADER } from './rpc-client.js';
2
+ function joinUrl(baseUrl, path) {
3
+ if (!baseUrl)
4
+ return path;
5
+ return `${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
6
+ }
7
+ function fillPath(path, params) {
8
+ return path.replace(/\{(\+?)([^}]+)\}/g, (_, catchAll, name) => {
9
+ const value = params?.[name];
10
+ if (value === undefined)
11
+ throw new Error(`Missing path parameter: ${name}`);
12
+ if (catchAll)
13
+ return value.split('/').map(encodeURIComponent).join('/');
14
+ return encodeURIComponent(value);
15
+ });
16
+ }
17
+ function buildUrl(baseUrl, path, query) {
18
+ const url = joinUrl(baseUrl, path);
19
+ if (!query)
20
+ return url;
21
+ const params = new URLSearchParams();
22
+ for (const [key, value] of Object.entries(query)) {
23
+ if (value === undefined)
24
+ continue;
25
+ params.set(key, typeof value === 'string' ? value : JSON.stringify(value));
26
+ }
27
+ return params.size ? `${url}?${params}` : url;
28
+ }
29
+ async function requestJson(fetcher, url, init) {
30
+ const response = await fetcher(url, init);
31
+ if (!response.ok)
32
+ throw new Error(`${response.status} ${await response.text()}`);
33
+ if (response.status === 204)
34
+ return undefined;
35
+ return response.json();
36
+ }
37
+ async function* streamJsonEvents(fetcher, url, init) {
38
+ const response = await fetcher(url, init);
39
+ if (!response.ok || !response.body) {
40
+ throw new Error(`Stream request failed (${response.status})`);
41
+ }
42
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
43
+ let buffer = '';
44
+ let data = [];
45
+ const flush = () => {
46
+ if (!data.length)
47
+ return undefined;
48
+ const value = JSON.parse(data.join('\n'));
49
+ data = [];
50
+ return value;
51
+ };
52
+ try {
53
+ while (true) {
54
+ const { done, value } = await reader.read();
55
+ if (done)
56
+ break;
57
+ buffer += value;
58
+ let newline = buffer.indexOf('\n');
59
+ while (newline !== -1) {
60
+ const line = buffer.slice(0, newline).replace(/\r$/, '');
61
+ buffer = buffer.slice(newline + 1);
62
+ if (line === '') {
63
+ const event = flush();
64
+ if (event !== undefined)
65
+ yield event;
66
+ }
67
+ else if (line.startsWith('data:')) {
68
+ data.push(line.slice(5).replace(/^ /, ''));
69
+ }
70
+ newline = buffer.indexOf('\n');
71
+ }
72
+ }
73
+ const event = flush();
74
+ if (event !== undefined)
75
+ yield event;
76
+ }
77
+ finally {
78
+ await reader.cancel().catch(() => { });
79
+ }
80
+ }
81
+ export function createRestClient(routes, options = {}) {
82
+ const fetcher = options.fetch ?? globalThis.fetch;
83
+ const client = {};
84
+ for (const [name, route] of Object.entries(routes)) {
85
+ client[name] = (args = {}, callOptions = {}) => {
86
+ const path = fillPath(route.path, args.params);
87
+ const url = buildUrl(options.baseUrl, path, args.query);
88
+ const headers = new Headers(callOptions.headers);
89
+ if (callOptions.operationId)
90
+ headers.set(OPERATION_ID_HEADER, callOptions.operationId);
91
+ if (route.stream) {
92
+ headers.set('accept', 'text/event-stream');
93
+ return streamJsonEvents(fetcher, url, {
94
+ method: route.method,
95
+ signal: callOptions.signal,
96
+ headers,
97
+ });
98
+ }
99
+ if (args.body !== undefined)
100
+ headers.set('content-type', 'application/json');
101
+ return requestJson(fetcher, url, {
102
+ method: route.method,
103
+ signal: callOptions.signal,
104
+ headers,
105
+ ...(args.body === undefined ? {} : { body: JSON.stringify(args.body) }),
106
+ });
107
+ };
108
+ }
109
+ return client;
110
+ }
111
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgC,MAAM,cAAc,CAAA;AAqFhF,SAAS,OAAO,CAAC,OAA2B,EAAE,IAAY;IACxD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IACzB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAA;AACnE,CAAC;AAED,SAAS,QAAQ,CACf,IAAY,EACZ,MAA0C;IAE1C,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAY,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,CAAA;QAC5B,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAA;QAC3E,IAAI,QAAQ;YAAE,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACvE,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,QAAQ,CACf,OAA2B,EAC3B,IAAY,EACZ,KAA0C;IAE1C,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAClC,IAAI,CAAC,KAAK;QAAE,OAAO,GAAG,CAAA;IACtB,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QACjC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;IAC5E,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAA;AAC/C,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,OAAc,EACd,GAAW,EACX,IAAiB;IAEjB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACzC,IAAI,CAAC,QAAQ,CAAC,EAAE;QACd,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IAChE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;AACxB,CAAC;AAED,KAAK,SAAS,CAAC,CAAC,gBAAgB,CAC9B,OAAc,EACd,GAAW,EACX,IAAiB;IAEjB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACzC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAA;IAC/D,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,iBAAiB,EAAE,CAAC,CAAC,SAAS,EAAE,CAAA;IAC7E,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,IAAI,IAAI,GAAa,EAAE,CAAA;IACvB,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAA;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAY,CAAA;QACpD,IAAI,GAAG,EAAE,CAAA;QACT,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;IACD,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;YAC3C,IAAI,IAAI;gBAAE,MAAK;YACf,MAAM,IAAI,KAAK,CAAA;YACf,IAAI,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YAClC,OAAO,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC;gBACtB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;gBACxD,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;gBAClC,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;oBAChB,MAAM,KAAK,GAAG,KAAK,EAAE,CAAA;oBACrB,IAAI,KAAK,KAAK,SAAS;wBAAE,MAAM,KAAK,CAAA;gBACtC,CAAC;qBAAM,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;oBACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAA;gBAC5C,CAAC;gBACD,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,EAAE,CAAA;QACrB,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,KAAK,CAAA;IACtC,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACvC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAuC,EACvC,UAA6B,EAAE;IAE/B,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAA;IACjD,MAAM,MAAM,GAA4B,EAAE,CAAA;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,MAAM,CAAC,IAAI,CAAC,GAAG,CACb,OAII,EAAE,EACN,cAAgC,EAAE,EAClC,EAAE;YACF,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;YAC9C,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YACvD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;YAChD,IAAI,WAAW,CAAC,WAAW;gBACzB,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,WAAW,CAAC,WAAW,CAAC,CAAA;YAC3D,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAA;gBAC1C,OAAO,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;oBACpC,MAAM,EAAE,KAAK,CAAC,MAAM;oBACpB,MAAM,EAAE,WAAW,CAAC,MAAM;oBAC1B,OAAO;iBACR,CAAC,CAAA;YACJ,CAAC;YACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;gBACzB,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;YACjD,OAAO,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE;gBAC/B,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,OAAO;gBACP,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;aACxE,CAAC,CAAA;QACJ,CAAC,CAAA;IACH,CAAC;IACD,OAAO,MAA2B,CAAA;AACpC,CAAC","sourcesContent":["import { OPERATION_ID_HEADER, type CallOptions, type Fetch } from './rpc-client'\n\nexport type RouteOperation<\n TMethod extends string,\n TPath extends string,\n TQuery = undefined,\n TBody = undefined,\n TOutput = undefined,\n TStream extends boolean = false,\n> = {\n method: TMethod\n path: TPath\n query?: TQuery\n body?: TBody\n output?: TOutput\n stream?: TStream\n}\n\ntype AnyOperation = {\n method: string\n path: string\n query?: unknown\n body?: unknown\n output?: unknown\n stream?: boolean\n}\n\ntype QueryOf<O extends AnyOperation> = NonNullable<O['query']>\ntype BodyOf<O extends AnyOperation> = NonNullable<O['body']>\ntype OutputOf<O extends AnyOperation> = NonNullable<O['output']>\n\nexport type RouteCallOptions = CallOptions\n\ntype PathParamNames<TPath extends string> =\n TPath extends `${string}{${infer TParam}}${infer TRest}`\n ? TParam extends `+${infer TName}`\n ? TName | PathParamNames<TRest>\n : TParam | PathParamNames<TRest>\n : never\n\ntype PathArgs<O extends AnyOperation> = [PathParamNames<O['path']>] extends [\n never,\n]\n ? object\n : { params: Record<PathParamNames<O['path']>, string> }\n\ntype QueryArgs<O extends AnyOperation> = [O['query']] extends [undefined]\n ? object\n : { query?: QueryOf<O> }\n\ntype BodyArgs<O extends AnyOperation> = [O['body']] extends [undefined]\n ? object\n : { body: BodyOf<O> }\n\ntype CallArgs<O extends AnyOperation> = PathArgs<O> & QueryArgs<O> & BodyArgs<O>\n\ntype HasRequiredArgs<O extends AnyOperation> = [\n PathParamNames<O['path']>,\n] extends [never]\n ? [O['body']] extends [undefined]\n ? false\n : true\n : true\n\nexport type RouteMethod<O extends AnyOperation> = (\n ...args: true extends HasRequiredArgs<O>\n ? [args: CallArgs<O>, options?: RouteCallOptions]\n : [args?: CallArgs<O>, options?: RouteCallOptions]\n) => true extends O['stream']\n ? AsyncIterable<OutputOf<O>>\n : Promise<OutputOf<O>>\n\nexport type ClientOf<TRoutes> = {\n [K in keyof TRoutes]: TRoutes[K] extends AnyOperation\n ? RouteMethod<TRoutes[K]>\n : never\n}\n\nexport type RestClientOptions = {\n baseUrl?: string\n fetch?: Fetch\n}\n\ntype RouteDescriptor = { method: string; path: string; stream?: boolean }\n\nfunction joinUrl(baseUrl: string | undefined, path: string): string {\n if (!baseUrl) return path\n return `${baseUrl.replace(/\\/$/, '')}/${path.replace(/^\\//, '')}`\n}\n\nfunction fillPath(\n path: string,\n params: Record<string, string> | undefined,\n): string {\n return path.replace(/\\{(\\+?)([^}]+)\\}/g, (_, catchAll, name: string) => {\n const value = params?.[name]\n if (value === undefined) throw new Error(`Missing path parameter: ${name}`)\n if (catchAll) return value.split('/').map(encodeURIComponent).join('/')\n return encodeURIComponent(value)\n })\n}\n\nfunction buildUrl(\n baseUrl: string | undefined,\n path: string,\n query: Record<string, unknown> | undefined,\n): string {\n const url = joinUrl(baseUrl, path)\n if (!query) return url\n const params = new URLSearchParams()\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue\n params.set(key, typeof value === 'string' ? value : JSON.stringify(value))\n }\n return params.size ? `${url}?${params}` : url\n}\n\nasync function requestJson(\n fetcher: Fetch,\n url: string,\n init: RequestInit,\n): Promise<unknown> {\n const response = await fetcher(url, init)\n if (!response.ok)\n throw new Error(`${response.status} ${await response.text()}`)\n if (response.status === 204) return undefined\n return response.json()\n}\n\nasync function* streamJsonEvents(\n fetcher: Fetch,\n url: string,\n init: RequestInit,\n): AsyncGenerator<unknown> {\n const response = await fetcher(url, init)\n if (!response.ok || !response.body) {\n throw new Error(`Stream request failed (${response.status})`)\n }\n const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()\n let buffer = ''\n let data: string[] = []\n const flush = () => {\n if (!data.length) return undefined\n const value = JSON.parse(data.join('\\n')) as unknown\n data = []\n return value\n }\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += value\n let newline = buffer.indexOf('\\n')\n while (newline !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '')\n buffer = buffer.slice(newline + 1)\n if (line === '') {\n const event = flush()\n if (event !== undefined) yield event\n } else if (line.startsWith('data:')) {\n data.push(line.slice(5).replace(/^ /, ''))\n }\n newline = buffer.indexOf('\\n')\n }\n }\n const event = flush()\n if (event !== undefined) yield event\n } finally {\n await reader.cancel().catch(() => {})\n }\n}\n\nexport function createRestClient<TRoutes>(\n routes: Record<string, RouteDescriptor>,\n options: RestClientOptions = {},\n): ClientOf<TRoutes> {\n const fetcher = options.fetch ?? globalThis.fetch\n const client: Record<string, unknown> = {}\n for (const [name, route] of Object.entries(routes)) {\n client[name] = (\n args: {\n params?: Record<string, string>\n query?: Record<string, unknown>\n body?: unknown\n } = {},\n callOptions: RouteCallOptions = {},\n ) => {\n const path = fillPath(route.path, args.params)\n const url = buildUrl(options.baseUrl, path, args.query)\n const headers = new Headers(callOptions.headers)\n if (callOptions.operationId)\n headers.set(OPERATION_ID_HEADER, callOptions.operationId)\n if (route.stream) {\n headers.set('accept', 'text/event-stream')\n return streamJsonEvents(fetcher, url, {\n method: route.method,\n signal: callOptions.signal,\n headers,\n })\n }\n if (args.body !== undefined)\n headers.set('content-type', 'application/json')\n return requestJson(fetcher, url, {\n method: route.method,\n signal: callOptions.signal,\n headers,\n ...(args.body === undefined ? {} : { body: JSON.stringify(args.body) }),\n })\n }\n }\n return client as ClientOf<TRoutes>\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { createClient, createRpcClient, OPERATION_ID_HEADER, type AnyBunderstackApp, type BunderstackClient, type CallOptions, type ClientOptions, type ClientCarrier, type DirectClient, type Fetch, type InferApiRouter, type RpcClient, } from './rpc-client.js';
2
+ export { createLiveView, type LiveView, type LiveViewFrame, type LiveViewOptions, type LiveViewSnapshot, type LiveViewStatus, type MutationMethod, } from './live-view.js';
3
+ export { DEFAULT_KEEPALIVE_MS, DEFAULT_LIVENESS_FACTOR, isRealtimeHeartbeat, openRealtimeStream, type RealtimeAction, type RealtimeChange, type RealtimeClock, type RealtimeEvent, type RealtimeHeartbeat, type RealtimeProcedure, type RealtimeStreamOptions, type RealtimeSyncHandle, } from './realtime.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,SAAS,GACf,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,GACxB,MAAM,YAAY,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { createClient, createRpcClient, OPERATION_ID_HEADER, } from './rpc-client.js';
2
+ export { createLiveView, } from './live-view.js';
3
+ export { DEFAULT_KEEPALIVE_MS, DEFAULT_LIVENESS_FACTOR, isRealtimeHeartbeat, openRealtimeStream, } from './realtime.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,eAAe,EACf,mBAAmB,GAUpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,cAAc,GAOf,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,GASnB,MAAM,YAAY,CAAA","sourcesContent":["export {\n createClient,\n createRpcClient,\n OPERATION_ID_HEADER,\n type AnyBunderstackApp,\n type BunderstackClient,\n type CallOptions,\n type ClientOptions,\n type ClientCarrier,\n type DirectClient,\n type Fetch,\n type InferApiRouter,\n type RpcClient,\n} from './rpc-client'\nexport {\n createLiveView,\n type LiveView,\n type LiveViewFrame,\n type LiveViewOptions,\n type LiveViewSnapshot,\n type LiveViewStatus,\n type MutationMethod,\n} from './live-view'\nexport {\n DEFAULT_KEEPALIVE_MS,\n DEFAULT_LIVENESS_FACTOR,\n isRealtimeHeartbeat,\n openRealtimeStream,\n type RealtimeAction,\n type RealtimeChange,\n type RealtimeClock,\n type RealtimeEvent,\n type RealtimeHeartbeat,\n type RealtimeProcedure,\n type RealtimeStreamOptions,\n type RealtimeSyncHandle,\n} from './realtime'\n"]}
@@ -0,0 +1,49 @@
1
+ import type { CallOptions } from './rpc-client.js';
2
+ export type LiveViewFrame<T> = {
3
+ type: 'snapshot';
4
+ items: T[];
5
+ operationId?: string;
6
+ } | {
7
+ type: 'upsert';
8
+ record: T;
9
+ afterId?: string | null;
10
+ operationId?: string;
11
+ } | {
12
+ type: 'remove';
13
+ id: string;
14
+ operationId?: string;
15
+ } | {
16
+ type: 'heartbeat';
17
+ intervalMs?: number;
18
+ };
19
+ export type LiveViewStatus = 'connecting' | 'ready' | 'reconnecting' | 'error' | 'closed';
20
+ export type LiveViewSnapshot<T> = {
21
+ readonly items: readonly T[];
22
+ readonly status: LiveViewStatus;
23
+ readonly error: unknown;
24
+ };
25
+ export type LiveViewOptions<T> = {
26
+ subscribe: (options: {
27
+ signal: AbortSignal;
28
+ }) => AsyncIterable<LiveViewFrame<T>> | Promise<AsyncIterable<LiveViewFrame<T>>>;
29
+ getKey?: (record: T) => string;
30
+ createOperationId?: () => string;
31
+ retryMs?: number;
32
+ /** Reconnect after this multiple of the server heartbeat interval is silent. */
33
+ livenessFactor?: number;
34
+ /** Maximum wait after HTTP success for the matching live frame. */
35
+ ackTimeoutMs?: number;
36
+ };
37
+ export type MutationMethod<TArgs, TResult> = (args: TArgs, options: CallOptions & {
38
+ operationId: string;
39
+ }) => Promise<TResult>;
40
+ export type LiveView<T> = {
41
+ getSnapshot(): LiveViewSnapshot<T>;
42
+ subscribe(listener: () => void): () => void;
43
+ mutate<TArgs, TResult>(method: MutationMethod<TArgs, TResult>, args: TArgs, options?: Omit<CallOptions, 'operationId'>): Promise<TResult>;
44
+ resync(): void;
45
+ close(): void;
46
+ readonly done: Promise<void>;
47
+ };
48
+ export declare function createLiveView<T>(options: LiveViewOptions<T>): LiveView<T>;
49
+ //# sourceMappingURL=live-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-view.d.ts","sourceRoot":"","sources":["../../src/client/live-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,CAAC,EAAE,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD;IACE,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,CAAC,CAAA;IACT,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,OAAO,GACP,cAAc,GACd,OAAO,GACP,QAAQ,CAAA;AAEZ,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI;IAC/B,SAAS,EAAE,CAAC,OAAO,EAAE;QACnB,MAAM,EAAE,WAAW,CAAA;KACpB,KACG,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAA;IAC9B,iBAAiB,CAAC,EAAE,MAAM,MAAM,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,cAAc,CAAC,KAAK,EAAE,OAAO,IAAI,CAC3C,IAAI,EAAE,KAAK,EACX,OAAO,EAAE,WAAW,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,KAC3C,OAAO,CAAC,OAAO,CAAC,CAAA;AAErB,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IACxB,WAAW,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAAA;IAClC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;IAC3C,MAAM,CAAC,KAAK,EAAE,OAAO,EACnB,MAAM,EAAE,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,EACtC,IAAI,EAAE,KAAK,EACX,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACzC,OAAO,CAAC,OAAO,CAAC,CAAA;IACnB,MAAM,IAAI,IAAI,CAAA;IACd,KAAK,IAAI,IAAI,CAAA;IACb,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B,CAAA;AAQD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAqL1E"}
@@ -0,0 +1,184 @@
1
+ export function createLiveView(options) {
2
+ const controller = new AbortController();
3
+ const listeners = new Set();
4
+ const waiters = new Map();
5
+ const getKey = options.getKey ?? ((record) => String(record.id));
6
+ let current;
7
+ let forceReconnect = false;
8
+ let hasSnapshot = false;
9
+ let snapshot = {
10
+ items: [],
11
+ status: 'connecting',
12
+ error: undefined,
13
+ };
14
+ const publish = (next) => {
15
+ snapshot = next;
16
+ for (const listener of listeners)
17
+ listener();
18
+ };
19
+ const apply = (frame) => {
20
+ if (frame.type !== 'heartbeat') {
21
+ let items;
22
+ if (frame.type === 'snapshot') {
23
+ hasSnapshot = true;
24
+ items = [...frame.items];
25
+ }
26
+ else if (frame.type === 'remove') {
27
+ items = snapshot.items.filter((record) => getKey(record) !== String(frame.id));
28
+ }
29
+ else {
30
+ const key = getKey(frame.record);
31
+ const index = snapshot.items.findIndex((record) => getKey(record) === key);
32
+ const next = snapshot.items.filter((record) => getKey(record) !== key);
33
+ if (frame.afterId === undefined) {
34
+ if (index === -1)
35
+ next.push(frame.record);
36
+ else
37
+ next.splice(index, 0, frame.record);
38
+ }
39
+ else {
40
+ const anchor = frame.afterId === null
41
+ ? -1
42
+ : next.findIndex((record) => getKey(record) === String(frame.afterId));
43
+ const placement = frame.afterId !== null && anchor === -1 ? next.length : anchor + 1;
44
+ next.splice(placement, 0, frame.record);
45
+ }
46
+ items = next;
47
+ }
48
+ publish({ items, status: 'ready', error: undefined });
49
+ if (frame.type === 'snapshot') {
50
+ // A fresh snapshot is authoritative. Once the write request succeeded,
51
+ // it safely settles operations whose matching event was lost while the
52
+ // live connection was down.
53
+ for (const [operationId, waiter] of waiters) {
54
+ if (!waiter.httpSucceeded)
55
+ continue;
56
+ waiter.resolve();
57
+ waiters.delete(operationId);
58
+ }
59
+ }
60
+ }
61
+ if ('operationId' in frame && frame.operationId) {
62
+ waiters.get(frame.operationId)?.resolve();
63
+ waiters.delete(frame.operationId);
64
+ }
65
+ };
66
+ const done = (async () => {
67
+ let attempt = 0;
68
+ while (!controller.signal.aborted) {
69
+ current = new AbortController();
70
+ const signal = AbortSignal.any([controller.signal, current.signal]);
71
+ let heartbeatIntervalMs;
72
+ let livenessTimer;
73
+ const armLivenessTimer = () => {
74
+ if (livenessTimer)
75
+ clearTimeout(livenessTimer);
76
+ if (!heartbeatIntervalMs)
77
+ return;
78
+ livenessTimer = setTimeout(() => current?.abort(), heartbeatIntervalMs * (options.livenessFactor ?? 2.5));
79
+ };
80
+ try {
81
+ const frames = await options.subscribe({ signal });
82
+ for await (const frame of frames) {
83
+ if (signal.aborted)
84
+ break;
85
+ attempt = 0;
86
+ apply(frame);
87
+ if (frame.type === 'heartbeat' && frame.intervalMs) {
88
+ heartbeatIntervalMs = frame.intervalMs;
89
+ }
90
+ armLivenessTimer();
91
+ }
92
+ if (!signal.aborted)
93
+ throw new Error('Live view closed');
94
+ }
95
+ catch (error) {
96
+ if (controller.signal.aborted)
97
+ break;
98
+ publish({
99
+ ...snapshot,
100
+ status: hasSnapshot ? 'reconnecting' : 'error',
101
+ error: hasSnapshot ? undefined : error,
102
+ });
103
+ }
104
+ finally {
105
+ if (livenessTimer)
106
+ clearTimeout(livenessTimer);
107
+ }
108
+ if (!controller.signal.aborted) {
109
+ const delayMs = forceReconnect
110
+ ? 0
111
+ : Math.min(30_000, (options.retryMs ?? 1_000) * 2 ** attempt++);
112
+ forceReconnect = false;
113
+ if (delayMs > 0) {
114
+ await new Promise((resolve) => {
115
+ const timer = setTimeout(done, delayMs);
116
+ function done() {
117
+ clearTimeout(timer);
118
+ controller.signal.removeEventListener('abort', done);
119
+ resolve();
120
+ }
121
+ controller.signal.addEventListener('abort', done, { once: true });
122
+ });
123
+ }
124
+ }
125
+ }
126
+ if (controller.signal.aborted) {
127
+ publish({ ...snapshot, status: 'closed' });
128
+ }
129
+ })();
130
+ return {
131
+ getSnapshot: () => snapshot,
132
+ subscribe(listener) {
133
+ listeners.add(listener);
134
+ return () => listeners.delete(listener);
135
+ },
136
+ async mutate(method, args, callOptions = {}) {
137
+ const operationId = options.createOperationId?.() ?? crypto.randomUUID();
138
+ let waiter;
139
+ const acknowledged = new Promise((resolve, reject) => {
140
+ waiter = { httpSucceeded: false, resolve, reject };
141
+ });
142
+ // The HTTP request may fail before this promise is awaited. Attaching a
143
+ // handler now prevents that rejection from surfacing as unhandled.
144
+ void acknowledged.catch(() => { });
145
+ waiters.set(operationId, waiter);
146
+ let timer;
147
+ try {
148
+ const result = await method(args, { ...callOptions, operationId });
149
+ waiter.httpSucceeded = true;
150
+ const timedOut = new Promise((_resolve, reject) => {
151
+ timer = setTimeout(() => {
152
+ const error = new Error(`Operation ${operationId} acknowledgement timed out`);
153
+ waiters.delete(operationId);
154
+ reject(error);
155
+ }, options.ackTimeoutMs ?? 30_000);
156
+ });
157
+ await Promise.race([acknowledged, timedOut]);
158
+ return result;
159
+ }
160
+ catch (error) {
161
+ waiters.delete(operationId);
162
+ waiter.reject(error);
163
+ throw error;
164
+ }
165
+ finally {
166
+ if (timer)
167
+ clearTimeout(timer);
168
+ }
169
+ },
170
+ resync() {
171
+ forceReconnect = true;
172
+ current?.abort();
173
+ },
174
+ close() {
175
+ controller.abort();
176
+ const error = new DOMException('Live view closed', 'AbortError');
177
+ for (const waiter of waiters.values())
178
+ waiter.reject(error);
179
+ waiters.clear();
180
+ },
181
+ done,
182
+ };
183
+ }
184
+ //# sourceMappingURL=live-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-view.js","sourceRoot":"","sources":["../../src/client/live-view.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CallOptions } from './rpc-client'\n\nexport type LiveViewFrame<T> =\n | { type: 'snapshot'; items: T[]; operationId?: string }\n | {\n type: 'upsert'\n record: T\n afterId?: string | null\n operationId?: string\n }\n | { type: 'remove'; id: string; operationId?: string }\n | { type: 'heartbeat'; intervalMs?: number }\n\nexport type LiveViewStatus =\n | 'connecting'\n | 'ready'\n | 'reconnecting'\n | 'error'\n | 'closed'\n\nexport type LiveViewSnapshot<T> = {\n readonly items: readonly T[]\n readonly status: LiveViewStatus\n readonly error: unknown\n}\n\nexport type LiveViewOptions<T> = {\n subscribe: (options: {\n signal: AbortSignal\n }) =>\n | AsyncIterable<LiveViewFrame<T>>\n | Promise<AsyncIterable<LiveViewFrame<T>>>\n getKey?: (record: T) => string\n createOperationId?: () => string\n retryMs?: number\n /** Reconnect after this multiple of the server heartbeat interval is silent. */\n livenessFactor?: number\n /** Maximum wait after HTTP success for the matching live frame. */\n ackTimeoutMs?: number\n}\n\nexport type MutationMethod<TArgs, TResult> = (\n args: TArgs,\n options: CallOptions & { operationId: string },\n) => Promise<TResult>\n\nexport type LiveView<T> = {\n getSnapshot(): LiveViewSnapshot<T>\n subscribe(listener: () => void): () => void\n mutate<TArgs, TResult>(\n method: MutationMethod<TArgs, TResult>,\n args: TArgs,\n options?: Omit<CallOptions, 'operationId'>,\n ): Promise<TResult>\n resync(): void\n close(): void\n readonly done: Promise<void>\n}\n\ntype Waiter = {\n httpSucceeded: boolean\n resolve: () => void\n reject: (error: unknown) => void\n}\n\nexport function createLiveView<T>(options: LiveViewOptions<T>): LiveView<T> {\n const controller = new AbortController()\n const listeners = new Set<() => void>()\n const waiters = new Map<string, Waiter>()\n const getKey =\n options.getKey ?? ((record: T) => String((record as { id: unknown }).id))\n let current: AbortController | undefined\n let forceReconnect = false\n let hasSnapshot = false\n let snapshot: LiveViewSnapshot<T> = {\n items: [],\n status: 'connecting',\n error: undefined,\n }\n\n const publish = (next: LiveViewSnapshot<T>) => {\n snapshot = next\n for (const listener of listeners) listener()\n }\n\n const apply = (frame: LiveViewFrame<T>) => {\n if (frame.type !== 'heartbeat') {\n let items: readonly T[]\n if (frame.type === 'snapshot') {\n hasSnapshot = true\n items = [...frame.items]\n } else if (frame.type === 'remove') {\n items = snapshot.items.filter(\n (record) => getKey(record) !== String(frame.id),\n )\n } else {\n const key = getKey(frame.record)\n const index = snapshot.items.findIndex(\n (record) => getKey(record) === key,\n )\n const next = snapshot.items.filter((record) => getKey(record) !== key)\n if (frame.afterId === undefined) {\n if (index === -1) next.push(frame.record)\n else next.splice(index, 0, frame.record)\n } else {\n const anchor =\n frame.afterId === null\n ? -1\n : next.findIndex(\n (record) => getKey(record) === String(frame.afterId),\n )\n const placement =\n frame.afterId !== null && anchor === -1 ? next.length : anchor + 1\n next.splice(placement, 0, frame.record)\n }\n items = next\n }\n publish({ items, status: 'ready', error: undefined })\n if (frame.type === 'snapshot') {\n // A fresh snapshot is authoritative. Once the write request succeeded,\n // it safely settles operations whose matching event was lost while the\n // live connection was down.\n for (const [operationId, waiter] of waiters) {\n if (!waiter.httpSucceeded) continue\n waiter.resolve()\n waiters.delete(operationId)\n }\n }\n }\n if ('operationId' in frame && frame.operationId) {\n waiters.get(frame.operationId)?.resolve()\n waiters.delete(frame.operationId)\n }\n }\n\n const done = (async () => {\n let attempt = 0\n while (!controller.signal.aborted) {\n current = new AbortController()\n const signal = AbortSignal.any([controller.signal, current.signal])\n let heartbeatIntervalMs: number | undefined\n let livenessTimer: ReturnType<typeof setTimeout> | undefined\n const armLivenessTimer = () => {\n if (livenessTimer) clearTimeout(livenessTimer)\n if (!heartbeatIntervalMs) return\n livenessTimer = setTimeout(\n () => current?.abort(),\n heartbeatIntervalMs * (options.livenessFactor ?? 2.5),\n )\n }\n try {\n const frames = await options.subscribe({ signal })\n for await (const frame of frames) {\n if (signal.aborted) break\n attempt = 0\n apply(frame)\n if (frame.type === 'heartbeat' && frame.intervalMs) {\n heartbeatIntervalMs = frame.intervalMs\n }\n armLivenessTimer()\n }\n if (!signal.aborted) throw new Error('Live view closed')\n } catch (error) {\n if (controller.signal.aborted) break\n publish({\n ...snapshot,\n status: hasSnapshot ? 'reconnecting' : 'error',\n error: hasSnapshot ? undefined : error,\n })\n } finally {\n if (livenessTimer) clearTimeout(livenessTimer)\n }\n if (!controller.signal.aborted) {\n const delayMs = forceReconnect\n ? 0\n : Math.min(30_000, (options.retryMs ?? 1_000) * 2 ** attempt++)\n forceReconnect = false\n if (delayMs > 0) {\n await new Promise<void>((resolve) => {\n const timer = setTimeout(done, delayMs)\n function done() {\n clearTimeout(timer)\n controller.signal.removeEventListener('abort', done)\n resolve()\n }\n controller.signal.addEventListener('abort', done, { once: true })\n })\n }\n }\n }\n if (controller.signal.aborted) {\n publish({ ...snapshot, status: 'closed' })\n }\n })()\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n async mutate(method, args, callOptions = {}) {\n const operationId = options.createOperationId?.() ?? crypto.randomUUID()\n let waiter!: Waiter\n const acknowledged = new Promise<void>((resolve, reject) => {\n waiter = { httpSucceeded: false, resolve, reject }\n })\n // The HTTP request may fail before this promise is awaited. Attaching a\n // handler now prevents that rejection from surfacing as unhandled.\n void acknowledged.catch(() => {})\n waiters.set(operationId, waiter)\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const result = await method(args, { ...callOptions, operationId })\n waiter.httpSucceeded = true\n const timedOut = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n const error = new Error(\n `Operation ${operationId} acknowledgement timed out`,\n )\n waiters.delete(operationId)\n reject(error)\n }, options.ackTimeoutMs ?? 30_000)\n })\n await Promise.race([acknowledged, timedOut])\n return result\n } catch (error) {\n waiters.delete(operationId)\n waiter.reject(error)\n throw error\n } finally {\n if (timer) clearTimeout(timer)\n }\n },\n resync() {\n forceReconnect = true\n current?.abort()\n },\n close() {\n controller.abort()\n const error = new DOMException('Live view closed', 'AbortError')\n for (const waiter of waiters.values()) waiter.reject(error)\n waiters.clear()\n },\n done,\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { LiveView, LiveViewSnapshot } from './live-view.js';
2
+ /** React owns scheduling; the shared LiveView remains an external store. */
3
+ export declare function useLiveView<T>(view: LiveView<T>): LiveViewSnapshot<T>;
4
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/client/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE7D,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAMrE"}
@@ -0,0 +1,6 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ /** React owns scheduling; the shared LiveView remains an external store. */
3
+ export function useLiveView(view) {
4
+ return useSyncExternalStore(view.subscribe, view.getSnapshot, view.getSnapshot);
5
+ }
6
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/client/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAI5C,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAI,IAAiB;IAC9C,OAAO,oBAAoB,CACzB,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,WAAW,CACjB,CAAA;AACH,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\n\nimport type { LiveView, LiveViewSnapshot } from './live-view'\n\n/** React owns scheduling; the shared LiveView remains an external store. */\nexport function useLiveView<T>(view: LiveView<T>): LiveViewSnapshot<T> {\n return useSyncExternalStore(\n view.subscribe,\n view.getSnapshot,\n view.getSnapshot,\n )\n}\n"]}
@@ -0,0 +1,54 @@
1
+ export type RealtimeAction = 'create' | 'update' | 'delete';
2
+ export type RealtimeChange = {
3
+ table: string;
4
+ action: RealtimeAction;
5
+ record: Record<string, unknown>;
6
+ operationId?: string;
7
+ };
8
+ export type RealtimeHeartbeat = {
9
+ type: 'heartbeat';
10
+ intervalMs?: number;
11
+ };
12
+ export type RealtimeEvent = RealtimeChange | RealtimeHeartbeat;
13
+ export type RealtimeProcedure = {
14
+ call(input: {
15
+ tables: string[];
16
+ }, options?: {
17
+ signal?: AbortSignal;
18
+ lastEventId?: string;
19
+ }): Promise<AsyncIterable<RealtimeEvent>>;
20
+ };
21
+ export type RealtimeSyncHandle = {
22
+ close(): void;
23
+ done: Promise<void>;
24
+ };
25
+ export type RealtimeClock = {
26
+ setTimeout(fn: () => void, ms: number): unknown;
27
+ clearTimeout(handle: unknown): void;
28
+ };
29
+ export declare const DEFAULT_KEEPALIVE_MS = 5000;
30
+ export declare const DEFAULT_LIVENESS_FACTOR = 2.5;
31
+ export type RealtimeStreamOptions = {
32
+ subscribe: (options: {
33
+ signal: AbortSignal;
34
+ lastEventId?: string;
35
+ }) => Promise<AsyncIterable<RealtimeEvent>>;
36
+ onChange: (change: RealtimeChange) => void;
37
+ onReconnect: () => void | Promise<void>;
38
+ onError?: (error: unknown) => void;
39
+ onRetry?: (retry: {
40
+ attempt: number;
41
+ delayMs: number;
42
+ }) => void;
43
+ signal: AbortSignal;
44
+ retryMs?: number;
45
+ maxRetryMs?: number;
46
+ livenessFactor?: number;
47
+ defaultKeepaliveMs?: number;
48
+ clock?: RealtimeClock;
49
+ random?: () => number;
50
+ };
51
+ export declare function isRealtimeHeartbeat(event: RealtimeEvent): event is RealtimeHeartbeat;
52
+ /** Shared reconnecting transport used by LiveView, Query, and DB adapters. */
53
+ export declare function openRealtimeStream(options: RealtimeStreamOptions): RealtimeSyncHandle;
54
+ //# sourceMappingURL=realtime.d.ts.map