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.
- package/CHANGELOG.md +17 -1
- package/README.md +17 -5
- package/dist/api/crud-router.d.ts +4 -4
- package/dist/api/realtime-router.d.ts +1 -1
- package/dist/client/client.d.ts +53 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +111 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/live-view.d.ts +49 -0
- package/dist/client/live-view.d.ts.map +1 -0
- package/dist/client/live-view.js +184 -0
- package/dist/client/live-view.js.map +1 -0
- package/dist/client/react.d.ts +4 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +6 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/realtime.d.ts +54 -0
- package/dist/client/realtime.d.ts.map +1 -0
- package/dist/client/realtime.js +105 -0
- package/dist/client/realtime.js.map +1 -0
- package/dist/client/rest.d.ts +2 -0
- package/dist/client/rest.d.ts.map +1 -0
- package/dist/client/rest.js +2 -0
- package/dist/client/rest.js.map +1 -0
- package/dist/client/rpc-client.d.ts +38 -0
- package/dist/client/rpc-client.d.ts.map +1 -0
- package/dist/client/rpc-client.js +59 -0
- package/dist/client/rpc-client.js.map +1 -0
- package/dist/client/solid.d.ts +7 -0
- package/dist/client/solid.d.ts.map +1 -0
- package/dist/client/solid.js +21 -0
- package/dist/client/solid.js.map +1 -0
- package/dist/client/svelte.d.ts +6 -0
- package/dist/client/svelte.d.ts.map +1 -0
- package/dist/client/svelte.js +9 -0
- package/dist/client/svelte.js.map +1 -0
- package/dist/client/vue.d.ts +5 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +11 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/query/api.d.ts +10 -0
- package/dist/query/api.d.ts.map +1 -0
- package/dist/query/api.js +7 -0
- package/dist/query/api.js.map +1 -0
- package/dist/query/client.d.ts +33 -0
- package/dist/query/client.d.ts.map +1 -0
- package/dist/query/client.js +98 -0
- package/dist/query/client.js.map +1 -0
- package/dist/query/fetch.d.ts +3 -0
- package/dist/query/fetch.d.ts.map +1 -0
- package/dist/query/fetch.js +4 -0
- package/dist/query/fetch.js.map +1 -0
- package/dist/query/index.d.ts +8 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +4 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/infer.d.ts +25 -0
- package/dist/query/infer.d.ts.map +1 -0
- package/dist/query/infer.js +2 -0
- package/dist/query/infer.js.map +1 -0
- package/dist/query/react.d.ts +3 -0
- package/dist/query/react.d.ts.map +1 -0
- package/dist/query/react.js +2 -0
- package/dist/query/react.js.map +1 -0
- package/dist/query/realtime-flush.d.ts +26 -0
- package/dist/query/realtime-flush.d.ts.map +1 -0
- package/dist/query/realtime-flush.js +45 -0
- package/dist/query/realtime-flush.js.map +1 -0
- package/dist/query/realtime-stream.d.ts +3 -0
- package/dist/query/realtime-stream.d.ts.map +1 -0
- package/dist/query/realtime-stream.js +3 -0
- package/dist/query/realtime-stream.js.map +1 -0
- package/dist/query/realtime.d.ts +88 -0
- package/dist/query/realtime.d.ts.map +1 -0
- package/dist/query/realtime.js +255 -0
- package/dist/query/realtime.js.map +1 -0
- package/dist/start/auth-client.d.ts +11 -0
- package/dist/start/auth-client.d.ts.map +1 -0
- package/dist/start/auth-client.js +15 -0
- package/dist/start/auth-client.js.map +1 -0
- package/dist/start/index.d.ts +66 -0
- package/dist/start/index.d.ts.map +1 -0
- package/dist/start/index.js +49 -0
- package/dist/start/index.js.map +1 -0
- package/dist/start/isomorphic-fetch.d.ts +13 -0
- package/dist/start/isomorphic-fetch.d.ts.map +1 -0
- package/dist/start/isomorphic-fetch.js +33 -0
- package/dist/start/isomorphic-fetch.js.map +1 -0
- package/dist/sync/collection.d.ts +106 -0
- package/dist/sync/collection.d.ts.map +1 -0
- package/dist/sync/collection.js +258 -0
- package/dist/sync/collection.js.map +1 -0
- package/dist/sync/index.d.ts +8 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +4 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/realtime-sync.d.ts +31 -0
- package/dist/sync/realtime-sync.d.ts.map +1 -0
- package/dist/sync/realtime-sync.js +46 -0
- package/dist/sync/realtime-sync.js.map +1 -0
- package/dist/sync/sync-client.d.ts +28 -0
- package/dist/sync/sync-client.d.ts.map +1 -0
- package/dist/sync/sync-client.js +52 -0
- package/dist/sync/sync-client.js.map +1 -0
- package/dist/sync/update-queue.d.ts +27 -0
- package/dist/sync/update-queue.d.ts.map +1 -0
- package/dist/sync/update-queue.js +66 -0
- package/dist/sync/update-queue.js.map +1 -0
- package/package.json +103 -3
- package/skills/creating-bunderstack-apps/references/runtime-integrations.md +1 -1
- 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
|
-
## [
|
|
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
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# bunderstack
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The complete backend & full-stack framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, email, live views, and client bindings from one single file declaration.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
bun add bunderstack better-auth drizzle-orm valibot @libsql/client
|
|
@@ -44,16 +44,28 @@ Use the in-memory Publisher for one process or configure
|
|
|
44
44
|
`realtime: { redis: process.env.REDIS_URL! }` for multi-process delivery and
|
|
45
45
|
replay. Deployment metadata is generated with `bunx bunderstack blueprint`.
|
|
46
46
|
|
|
47
|
+
## Package Subpaths (0.21+)
|
|
48
|
+
|
|
49
|
+
- `bunderstack` — Backend runtime (`createBunderstack`, `provision`, `defineApi`, `buildApiRegistry`)
|
|
50
|
+
- `bunderstack/client` — Framework-neutral RPC & LiveView client (`createClient`, `createLiveView`)
|
|
51
|
+
- `bunderstack/client/rest` — Type-safe REST client
|
|
52
|
+
- `bunderstack/client/react`, `bunderstack/client/solid`, `bunderstack/client/vue`, `bunderstack/client/svelte` — LiveView UI bindings
|
|
53
|
+
- `bunderstack/query` & `bunderstack/query/react` — TanStack Query client (`createClient`, `syncRealtime`)
|
|
54
|
+
- `bunderstack/sync` — TanStack DB collections (`createSyncClient`)
|
|
55
|
+
- `bunderstack/start` & `bunderstack/start/auth` — TanStack Start full-stack integration
|
|
56
|
+
- `bunderstack/database/*` — Database adapters (`libsql`, `postgres-js`, `bun-sql`, `pglite`)
|
|
57
|
+
- `bunderstack/storage/*` — Storage adapters (`s3`, `disk`)
|
|
58
|
+
- `bunderstack/email/*` — Email adapters (`smtp`, `resend`, `console`)
|
|
59
|
+
- `bunderstack/jobs/*` — Job queue adapters (`memory`, `redis`)
|
|
60
|
+
|
|
47
61
|
See the [workspace documentation](https://github.com/kirill-dev-pro/bunderstack#readme) for webhooks, clients,
|
|
48
62
|
storage, collections, lifecycle, and complete examples.
|
|
49
63
|
|
|
50
64
|
## Upgrading
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
typed `filters`, error codes are oRPC's own, and realtime names tables by their
|
|
54
|
-
schema key. Every change is listed with before/after code in the migration
|
|
55
|
-
guides, which ship with the package as `CHANGELOG.md` and live in full at:
|
|
66
|
+
Every change is listed with before/after code in the migration guides, which live in full at:
|
|
56
67
|
|
|
68
|
+
- [Migrating to 0.21](https://github.com/kirill-dev-pro/bunderstack/blob/main/docs/MIGRATION-0.21.md)
|
|
57
69
|
- [Migrating to 0.17](https://github.com/kirill-dev-pro/bunderstack/blob/main/docs/MIGRATION-0.17.md)
|
|
58
70
|
- [Migrating to 0.16](https://github.com/kirill-dev-pro/bunderstack/blob/main/docs/MIGRATION-0.16.md)
|
|
59
71
|
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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":"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","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 @@
|
|
|
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"]}
|