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.
- package/CHANGELOG.md +17 -1
- 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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type RealtimeProcedure, type RealtimeSyncHandle } from '../client/realtime.js';
|
|
2
|
+
type SyncableCollection = {
|
|
3
|
+
utils: {
|
|
4
|
+
writeUpsert(item: unknown): void;
|
|
5
|
+
writeDelete(key: unknown): void;
|
|
6
|
+
refetch(): Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type SyncRealtimeTarget = {
|
|
10
|
+
applyRealtimeEvent(action: 'create' | 'update' | 'delete', record: Record<string, unknown>): void;
|
|
11
|
+
refetchAll(): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
export type SyncRealtimeConfig = {
|
|
14
|
+
api: {
|
|
15
|
+
realtime: {
|
|
16
|
+
changes: RealtimeProcedure;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
tables: string[];
|
|
20
|
+
collections?: Record<string, SyncableCollection>;
|
|
21
|
+
resolve?: (table: string) => SyncRealtimeTarget | undefined;
|
|
22
|
+
resolveAll?: () => Iterable<SyncRealtimeTarget>;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
retryMs?: number;
|
|
25
|
+
maxRetryMs?: number;
|
|
26
|
+
onError?: (error: unknown) => void;
|
|
27
|
+
};
|
|
28
|
+
/** TanStack DB consumes the shared raw stream without a QueryClient detour. */
|
|
29
|
+
export declare function createSyncRealtimeClient(config: SyncRealtimeConfig): RealtimeSyncHandle;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=realtime-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-sync.d.ts","sourceRoot":"","sources":["../../src/sync/realtime-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAA;AAE3B,KAAK,kBAAkB,GAAG;IACxB,KAAK,EAAE;QACL,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;QAChC,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;QAC/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KACzB,CAAA;CACF,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,kBAAkB,CAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,IAAI,CAAA;IACP,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE;QAAE,QAAQ,EAAE;YAAE,OAAO,EAAE,iBAAiB,CAAA;SAAE,CAAA;KAAE,CAAA;IACjD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,kBAAkB,GAAG,SAAS,CAAA;IAC3D,UAAU,CAAC,EAAE,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;IAC/C,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC,CAAA;AAED,+EAA+E;AAC/E,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,GACzB,kBAAkB,CAoDpB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { openRealtimeStream, } from '../client/realtime.js';
|
|
2
|
+
/** TanStack DB consumes the shared raw stream without a QueryClient detour. */
|
|
3
|
+
export function createSyncRealtimeClient(config) {
|
|
4
|
+
const collections = config.collections ?? {};
|
|
5
|
+
const caller = new AbortController();
|
|
6
|
+
const signal = config.signal
|
|
7
|
+
? AbortSignal.any([caller.signal, config.signal])
|
|
8
|
+
: caller.signal;
|
|
9
|
+
const stream = openRealtimeStream({
|
|
10
|
+
signal,
|
|
11
|
+
retryMs: config.retryMs,
|
|
12
|
+
maxRetryMs: config.maxRetryMs,
|
|
13
|
+
onError: config.onError,
|
|
14
|
+
subscribe: ({ signal, lastEventId }) => config.api.realtime.changes.call({ tables: config.tables }, { signal, lastEventId }),
|
|
15
|
+
onChange: (event) => {
|
|
16
|
+
if (config.resolve) {
|
|
17
|
+
config
|
|
18
|
+
.resolve(event.table)
|
|
19
|
+
?.applyRealtimeEvent(event.action, event.record);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const collection = collections[event.table];
|
|
23
|
+
if (!collection)
|
|
24
|
+
return;
|
|
25
|
+
if (event.action === 'delete')
|
|
26
|
+
collection.utils.writeDelete(event.record['id']);
|
|
27
|
+
else
|
|
28
|
+
collection.utils.writeUpsert(event.record);
|
|
29
|
+
},
|
|
30
|
+
onReconnect: async () => {
|
|
31
|
+
if (config.resolveAll) {
|
|
32
|
+
await Promise.all([...config.resolveAll()].map((target) => target.refetchAll()));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
await Promise.all(Object.values(collections).map((collection) => collection.utils.refetch()));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
close() {
|
|
40
|
+
caller.abort();
|
|
41
|
+
stream.close();
|
|
42
|
+
},
|
|
43
|
+
done: stream.done,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=realtime-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-sync.js","sourceRoot":"","sources":["../../src/sync/realtime-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,GAGnB,MAAM,oBAAoB,CAAA;AA8B3B,+EAA+E;AAC/E,MAAM,UAAU,wBAAwB,CACtC,MAA0B;IAE1B,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM;QAC1B,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC,CAAC,MAAM,CAAC,MAAM,CAAA;IAEjB,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,MAAM;QACN,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,CACrC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAC9B,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EACzB,EAAE,MAAM,EAAE,WAAW,EAAE,CACxB;QACH,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;YAClB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,MAAM;qBACH,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;oBACrB,EAAE,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;gBAClD,OAAM;YACR,CAAC;YACD,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAC3C,IAAI,CAAC,UAAU;gBAAE,OAAM;YACvB,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ;gBAC3B,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;;gBAC7C,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACjD,CAAC;QACD,WAAW,EAAE,KAAK,IAAI,EAAE;YACtB,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;gBACtB,MAAM,OAAO,CAAC,GAAG,CACf,CAAC,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAC9D,CAAA;gBACD,OAAM;YACR,CAAC;YACD,MAAM,OAAO,CAAC,GAAG,CACf,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAC5C,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,CAC3B,CACF,CAAA;QACH,CAAC;KACF,CAAC,CAAA;IAEF,OAAO;QACL,KAAK;YACH,MAAM,CAAC,KAAK,EAAE,CAAA;YACd,MAAM,CAAC,KAAK,EAAE,CAAA;QAChB,CAAC;QACD,IAAI,EAAE,MAAM,CAAC,IAAI;KAClB,CAAA;AACH,CAAC","sourcesContent":["import {\n openRealtimeStream,\n type RealtimeProcedure,\n type RealtimeSyncHandle,\n} from '../client/realtime'\n\ntype SyncableCollection = {\n utils: {\n writeUpsert(item: unknown): void\n writeDelete(key: unknown): void\n refetch(): Promise<void>\n }\n}\n\nexport type SyncRealtimeTarget = {\n applyRealtimeEvent(\n action: 'create' | 'update' | 'delete',\n record: Record<string, unknown>,\n ): void\n refetchAll(): Promise<void>\n}\n\nexport type SyncRealtimeConfig = {\n api: { realtime: { changes: RealtimeProcedure } }\n tables: string[]\n collections?: Record<string, SyncableCollection>\n resolve?: (table: string) => SyncRealtimeTarget | undefined\n resolveAll?: () => Iterable<SyncRealtimeTarget>\n signal?: AbortSignal\n retryMs?: number\n maxRetryMs?: number\n onError?: (error: unknown) => void\n}\n\n/** TanStack DB consumes the shared raw stream without a QueryClient detour. */\nexport function createSyncRealtimeClient(\n config: SyncRealtimeConfig,\n): RealtimeSyncHandle {\n const collections = config.collections ?? {}\n const caller = new AbortController()\n const signal = config.signal\n ? AbortSignal.any([caller.signal, config.signal])\n : caller.signal\n\n const stream = openRealtimeStream({\n signal,\n retryMs: config.retryMs,\n maxRetryMs: config.maxRetryMs,\n onError: config.onError,\n subscribe: ({ signal, lastEventId }) =>\n config.api.realtime.changes.call(\n { tables: config.tables },\n { signal, lastEventId },\n ),\n onChange: (event) => {\n if (config.resolve) {\n config\n .resolve(event.table)\n ?.applyRealtimeEvent(event.action, event.record)\n return\n }\n const collection = collections[event.table]\n if (!collection) return\n if (event.action === 'delete')\n collection.utils.writeDelete(event.record['id'])\n else collection.utils.writeUpsert(event.record)\n },\n onReconnect: async () => {\n if (config.resolveAll) {\n await Promise.all(\n [...config.resolveAll()].map((target) => target.refetchAll()),\n )\n return\n }\n await Promise.all(\n Object.values(collections).map((collection) =>\n collection.utils.refetch(),\n ),\n )\n },\n })\n\n return {\n close() {\n caller.abort()\n stream.close()\n },\n done: stream.done,\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { type AnyBunderstackApp, type BunderstackClient, type InferInsert, type InferSchema, type InferSelect, type InferTables } from '../query/index.js';
|
|
3
|
+
import { type TableCollection } from './collection.js';
|
|
4
|
+
export type RowFor<TSchema extends Record<string, unknown>, K extends keyof TSchema> = [InferSelect<TSchema[K]>] extends [never] ? {
|
|
5
|
+
id: string | number;
|
|
6
|
+
} : InferSelect<TSchema[K]> extends {
|
|
7
|
+
id: string | number;
|
|
8
|
+
} ? InferSelect<TSchema[K]> : {
|
|
9
|
+
id: string | number;
|
|
10
|
+
};
|
|
11
|
+
export type CreateFor<TSchema extends Record<string, unknown>, K extends keyof TSchema> = [InferInsert<TSchema[K]>] extends [never] ? Partial<RowFor<TSchema, K>> : InferInsert<TSchema[K]>;
|
|
12
|
+
export type SyncClientOptions = {
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
15
|
+
queryClient: QueryClient;
|
|
16
|
+
realtime?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type BunderstackSyncClient<TApp extends AnyBunderstackApp> = {
|
|
19
|
+
[K in InferTables<TApp>]: TableCollection<RowFor<InferSchema<TApp>, K>, CreateFor<InferSchema<TApp>, K>, Partial<RowFor<InferSchema<TApp>, K>>>;
|
|
20
|
+
} & {
|
|
21
|
+
files: BunderstackClient<TApp>['files'];
|
|
22
|
+
realtime: {
|
|
23
|
+
close(): void;
|
|
24
|
+
subscribe(tables: string[]): Promise<void>;
|
|
25
|
+
} | undefined;
|
|
26
|
+
};
|
|
27
|
+
export declare function createSyncClient<TApp extends AnyBunderstackApp>(options: SyncClientOptions): BunderstackSyncClient<TApp>;
|
|
28
|
+
//# sourceMappingURL=sync-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../../src/sync/sync-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAM1E,MAAM,MAAM,MAAM,CAChB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,CAAC,SAAS,MAAM,OAAO,IACrB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACzC;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACvB,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACrD,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACvB;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AAE7B,MAAM,MAAM,SAAS,CACnB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,CAAC,SAAS,MAAM,OAAO,IACrB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACzC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAC3B,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;AAE3B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC3E,WAAW,EAAE,WAAW,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,IAAI,SAAS,iBAAiB,IAAI;KACjE,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,eAAe,CACvC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAC5B,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAC/B,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CACtC;CACF,GAAG;IACF,KAAK,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;IACvC,QAAQ,EACJ;QAAE,KAAK,IAAI,IAAI,CAAC;QAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,GAC7D,SAAS,CAAA;CACd,CAAA;AAED,wBAAgB,gBAAgB,CAAC,IAAI,SAAS,iBAAiB,EAC7D,OAAO,EAAE,iBAAiB,GACzB,qBAAqB,CAAC,IAAI,CAAC,CA+C7B"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createClient, } from '../query/index.js';
|
|
2
|
+
import { createTableCollection } from './collection.js';
|
|
3
|
+
import { createSyncRealtimeClient, } from './realtime-sync.js';
|
|
4
|
+
export function createSyncClient(options) {
|
|
5
|
+
const api = createClient(options);
|
|
6
|
+
const materialized = new Map();
|
|
7
|
+
const realtimeHandles = new Map();
|
|
8
|
+
const tables = new Map();
|
|
9
|
+
const realtimeEnabled = options.realtime ?? typeof window !== 'undefined';
|
|
10
|
+
const result = new Proxy({}, {
|
|
11
|
+
get(_target, property) {
|
|
12
|
+
if (typeof property !== 'string')
|
|
13
|
+
return undefined;
|
|
14
|
+
if (property === 'files')
|
|
15
|
+
return api.files;
|
|
16
|
+
if (property === 'realtime') {
|
|
17
|
+
return realtimeEnabled
|
|
18
|
+
? {
|
|
19
|
+
close: () => realtimeHandles.forEach((handle) => handle.close()),
|
|
20
|
+
subscribe: async (names) => {
|
|
21
|
+
for (const name of names)
|
|
22
|
+
void result[name];
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
if (['then', 'toJSON', 'constructor', '$$typeof'].includes(property))
|
|
28
|
+
return undefined;
|
|
29
|
+
const cached = tables.get(property);
|
|
30
|
+
if (cached)
|
|
31
|
+
return cached;
|
|
32
|
+
const collection = createTableCollection({
|
|
33
|
+
tableName: property,
|
|
34
|
+
procedures: api[property],
|
|
35
|
+
queryClient: options.queryClient,
|
|
36
|
+
});
|
|
37
|
+
tables.set(property, collection);
|
|
38
|
+
materialized.set(property, collection);
|
|
39
|
+
if (realtimeEnabled) {
|
|
40
|
+
realtimeHandles.set(property, createSyncRealtimeClient({
|
|
41
|
+
api: api,
|
|
42
|
+
tables: [property],
|
|
43
|
+
resolve: (table) => materialized.get(table),
|
|
44
|
+
resolveAll: () => materialized.values(),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
return collection;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=sync-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-client.js","sourceRoot":"","sources":["../../src/sync/sync-client.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,GAOb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,qBAAqB,EAAwB,MAAM,cAAc,CAAA;AAC1E,OAAO,EACL,wBAAwB,GAEzB,MAAM,iBAAiB,CAAA;AAsCxB,MAAM,UAAU,gBAAgB,CAC9B,OAA0B;IAE1B,MAAM,GAAG,GAAG,YAAY,CAAO,OAAO,CAAC,CAAA;IACvC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA8B,CAAA;IAC1D,MAAM,eAAe,GAAG,IAAI,GAAG,EAA6B,CAAA;IAC5D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAmB,CAAA;IACzC,MAAM,eAAe,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,MAAM,KAAK,WAAW,CAAA;IAEzE,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,EAAiC,EAAE;QAC1D,GAAG,CAAC,OAAO,EAAE,QAAQ;YACnB,IAAI,OAAO,QAAQ,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAA;YAClD,IAAI,QAAQ,KAAK,OAAO;gBAAE,OAAO,GAAG,CAAC,KAAK,CAAA;YAC1C,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAC5B,OAAO,eAAe;oBACpB,CAAC,CAAC;wBACE,KAAK,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;wBAChE,SAAS,EAAE,KAAK,EAAE,KAAe,EAAE,EAAE;4BACnC,KAAK,MAAM,IAAI,IAAI,KAAK;gCAAE,KAAM,MAAc,CAAC,IAAI,CAAC,CAAA;wBACtD,CAAC;qBACF;oBACH,CAAC,CAAC,SAAS,CAAA;YACf,CAAC;YACD,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAClE,OAAO,SAAS,CAAA;YAClB,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACnC,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAA;YACzB,MAAM,UAAU,GAAG,qBAAqB,CAAC;gBACvC,SAAS,EAAE,QAAQ;gBACnB,UAAU,EAAG,GAAW,CAAC,QAAQ,CAAC;gBAClC,WAAW,EAAE,OAAO,CAAC,WAAW;aACjC,CAAC,CAAA;YACF,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;YAChC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;YACtC,IAAI,eAAe,EAAE,CAAC;gBACpB,eAAe,CAAC,GAAG,CACjB,QAAQ,EACR,wBAAwB,CAAC;oBACvB,GAAG,EAAE,GAAU;oBACf,MAAM,EAAE,CAAC,QAAQ,CAAC;oBAClB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;oBAC3C,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE;iBACxC,CAAC,CACH,CAAA;YACH,CAAC;YACD,OAAO,UAAU,CAAA;QACnB,CAAC;KACF,CAAC,CAAA;IACF,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { QueryClient } from '@tanstack/react-query'\n\nimport {\n createClient,\n type AnyBunderstackApp,\n type BunderstackClient,\n type InferInsert,\n type InferSchema,\n type InferSelect,\n type InferTables,\n} from '../query/index'\nimport { createTableCollection, type TableCollection } from './collection'\nimport {\n createSyncRealtimeClient,\n type SyncRealtimeTarget,\n} from './realtime-sync'\n\nexport type RowFor<\n TSchema extends Record<string, unknown>,\n K extends keyof TSchema,\n> = [InferSelect<TSchema[K]>] extends [never]\n ? { id: string | number }\n : InferSelect<TSchema[K]> extends { id: string | number }\n ? InferSelect<TSchema[K]>\n : { id: string | number }\n\nexport type CreateFor<\n TSchema extends Record<string, unknown>,\n K extends keyof TSchema,\n> = [InferInsert<TSchema[K]>] extends [never]\n ? Partial<RowFor<TSchema, K>>\n : InferInsert<TSchema[K]>\n\nexport type SyncClientOptions = {\n baseUrl?: string\n fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n queryClient: QueryClient\n realtime?: boolean\n}\n\nexport type BunderstackSyncClient<TApp extends AnyBunderstackApp> = {\n [K in InferTables<TApp>]: TableCollection<\n RowFor<InferSchema<TApp>, K>,\n CreateFor<InferSchema<TApp>, K>,\n Partial<RowFor<InferSchema<TApp>, K>>\n >\n} & {\n files: BunderstackClient<TApp>['files']\n realtime:\n | { close(): void; subscribe(tables: string[]): Promise<void> }\n | undefined\n}\n\nexport function createSyncClient<TApp extends AnyBunderstackApp>(\n options: SyncClientOptions,\n): BunderstackSyncClient<TApp> {\n const api = createClient<TApp>(options)\n const materialized = new Map<string, SyncRealtimeTarget>()\n const realtimeHandles = new Map<string, { close(): void }>()\n const tables = new Map<string, unknown>()\n const realtimeEnabled = options.realtime ?? typeof window !== 'undefined'\n\n const result = new Proxy({} as BunderstackSyncClient<TApp>, {\n get(_target, property) {\n if (typeof property !== 'string') return undefined\n if (property === 'files') return api.files\n if (property === 'realtime') {\n return realtimeEnabled\n ? {\n close: () => realtimeHandles.forEach((handle) => handle.close()),\n subscribe: async (names: string[]) => {\n for (const name of names) void (result as any)[name]\n },\n }\n : undefined\n }\n if (['then', 'toJSON', 'constructor', '$$typeof'].includes(property))\n return undefined\n const cached = tables.get(property)\n if (cached) return cached\n const collection = createTableCollection({\n tableName: property,\n procedures: (api as any)[property],\n queryClient: options.queryClient,\n })\n tables.set(property, collection)\n materialized.set(property, collection)\n if (realtimeEnabled) {\n realtimeHandles.set(\n property,\n createSyncRealtimeClient({\n api: api as any,\n tables: [property],\n resolve: (table) => materialized.get(table),\n resolveAll: () => materialized.values(),\n }),\n )\n }\n return collection\n },\n })\n return result\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Per-key coalescing queue for high-frequency row updates.
|
|
2
|
+
*
|
|
3
|
+
* Cursor-shaped workloads call `collection.update()` many times a second. Sent
|
|
4
|
+
* one-for-one, each call is a request, a database write and a broadcast. This
|
|
5
|
+
* queue holds a single merged slot per row key: while a request is in flight,
|
|
6
|
+
* further updates for that key merge into the slot and go out as one follow-up
|
|
7
|
+
* once it lands. Request frequency settles at `min(update rate, 1/RTT)` without
|
|
8
|
+
* a tuned throttle constant, and the optimistic UI is untouched — only the
|
|
9
|
+
* network is coalesced.
|
|
10
|
+
*/
|
|
11
|
+
export type UpdateQueueConfig<TKey, TRow> = {
|
|
12
|
+
/** Sends one merged batch of changes and resolves with the canonical row. */
|
|
13
|
+
send: (key: TKey, changes: Record<string, unknown>) => Promise<TRow>;
|
|
14
|
+
/** Called with the server's row before the batch's waiters resolve. Awaited,
|
|
15
|
+
* so a handler that needs to recover (refetch) finishes first. */
|
|
16
|
+
onResult: (row: TRow) => void | Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
export type UpdateQueue<TKey> = {
|
|
19
|
+
/** Queues changes for `key`. Resolves once a request carrying them lands. */
|
|
20
|
+
enqueue: (key: TKey, changes: Record<string, unknown>) => Promise<void>;
|
|
21
|
+
/** Drops changes still queued for `key` (resolving their waiters) and waits
|
|
22
|
+
* for any in-flight request to land. For operations that supersede pending
|
|
23
|
+
* updates and must not race them — a delete of the same row. */
|
|
24
|
+
settle: (key: TKey) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
export declare function createUpdateQueue<TKey, TRow>(config: UpdateQueueConfig<TKey, TRow>): UpdateQueue<TKey>;
|
|
27
|
+
//# sourceMappingURL=update-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-queue.d.ts","sourceRoot":"","sources":["../../src/sync/update-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAcH,MAAM,MAAM,iBAAiB,CAAC,IAAI,EAAE,IAAI,IAAI;IAC1C,6EAA6E;IAC7E,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpE;sEACkE;IAClE,QAAQ,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9C,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,IAAI,IAAI;IAC9B,6EAA6E;IAC7E,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvE;;oEAEgE;IAChE,MAAM,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACrC,CAAA;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAC1C,MAAM,EAAE,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,GACpC,WAAW,CAAC,IAAI,CAAC,CAkDnB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Per-key coalescing queue for high-frequency row updates.
|
|
2
|
+
*
|
|
3
|
+
* Cursor-shaped workloads call `collection.update()` many times a second. Sent
|
|
4
|
+
* one-for-one, each call is a request, a database write and a broadcast. This
|
|
5
|
+
* queue holds a single merged slot per row key: while a request is in flight,
|
|
6
|
+
* further updates for that key merge into the slot and go out as one follow-up
|
|
7
|
+
* once it lands. Request frequency settles at `min(update rate, 1/RTT)` without
|
|
8
|
+
* a tuned throttle constant, and the optimistic UI is untouched — only the
|
|
9
|
+
* network is coalesced.
|
|
10
|
+
*/
|
|
11
|
+
export function createUpdateQueue(config) {
|
|
12
|
+
const slots = new Map();
|
|
13
|
+
async function drain(key, slot) {
|
|
14
|
+
while (Object.keys(slot.changes).length > 0) {
|
|
15
|
+
// Taking changes and waiters as one snapshot is what guarantees a waiter
|
|
16
|
+
// resolves only after a request that carried its own changes.
|
|
17
|
+
const changes = slot.changes;
|
|
18
|
+
const waiters = slot.waiters;
|
|
19
|
+
slot.changes = {};
|
|
20
|
+
slot.waiters = [];
|
|
21
|
+
try {
|
|
22
|
+
await config.onResult(await config.send(key, changes));
|
|
23
|
+
for (const waiter of waiters)
|
|
24
|
+
waiter.resolve();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Replaying queued changes on top of a failed base would diverge from
|
|
28
|
+
// the server silently, so the whole key is rolled back instead.
|
|
29
|
+
const queued = slot.waiters;
|
|
30
|
+
slot.changes = {};
|
|
31
|
+
slot.waiters = [];
|
|
32
|
+
for (const waiter of [...waiters, ...queued])
|
|
33
|
+
waiter.reject(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
slots.delete(key);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
enqueue(key, changes) {
|
|
40
|
+
let slot = slots.get(key);
|
|
41
|
+
if (!slot) {
|
|
42
|
+
slot = { changes: {}, waiters: [], done: null };
|
|
43
|
+
slots.set(key, slot);
|
|
44
|
+
}
|
|
45
|
+
Object.assign(slot.changes, changes);
|
|
46
|
+
const promise = new Promise((resolve, reject) => {
|
|
47
|
+
slot.waiters.push({ resolve, reject });
|
|
48
|
+
});
|
|
49
|
+
if (!slot.done)
|
|
50
|
+
slot.done = drain(key, slot);
|
|
51
|
+
return promise;
|
|
52
|
+
},
|
|
53
|
+
settle(key) {
|
|
54
|
+
const slot = slots.get(key);
|
|
55
|
+
if (!slot)
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
const waiters = slot.waiters;
|
|
58
|
+
slot.changes = {};
|
|
59
|
+
slot.waiters = [];
|
|
60
|
+
for (const waiter of waiters)
|
|
61
|
+
waiter.resolve();
|
|
62
|
+
return slot.done ?? Promise.resolve();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=update-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-queue.js","sourceRoot":"","sources":["../../src/sync/update-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA+BH,MAAM,UAAU,iBAAiB,CAC/B,MAAqC;IAErC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAc,CAAA;IAEnC,KAAK,UAAU,KAAK,CAAC,GAAS,EAAE,IAAU;QACxC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5C,yEAAyE;YACzE,8DAA8D;YAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;YAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;YAC5B,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;YACjB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;YACjB,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAA;gBACtD,KAAK,MAAM,MAAM,IAAI,OAAO;oBAAE,MAAM,CAAC,OAAO,EAAE,CAAA;YAChD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,sEAAsE;gBACtE,gEAAgE;gBAChE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAA;gBAC3B,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;gBACjB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;gBACjB,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,CAAC;oBAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACpE,CAAC;QACH,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACnB,CAAC;IAED,OAAO;QACL,OAAO,CAAC,GAAG,EAAE,OAAO;YAClB,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACzB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;gBAC/C,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YACtB,CAAC;YACD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YACpC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACpD,IAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;YACzC,CAAC,CAAC,CAAA;YACF,IAAI,CAAC,IAAI,CAAC,IAAI;gBAAE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YAC5C,OAAO,OAAO,CAAA;QAChB,CAAC;QACD,MAAM,CAAC,GAAG;YACR,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAC3B,IAAI,CAAC,IAAI;gBAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;YACnC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;YAC5B,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;YACjB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;YACjB,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,MAAM,CAAC,OAAO,EAAE,CAAA;YAC9C,OAAO,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,CAAA;QACvC,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/** Per-key coalescing queue for high-frequency row updates.\n *\n * Cursor-shaped workloads call `collection.update()` many times a second. Sent\n * one-for-one, each call is a request, a database write and a broadcast. This\n * queue holds a single merged slot per row key: while a request is in flight,\n * further updates for that key merge into the slot and go out as one follow-up\n * once it lands. Request frequency settles at `min(update rate, 1/RTT)` without\n * a tuned throttle constant, and the optimistic UI is untouched — only the\n * network is coalesced.\n */\n\ntype Waiter = {\n resolve: () => void\n reject: (error: unknown) => void\n}\n\ntype Slot = {\n changes: Record<string, unknown>\n waiters: Waiter[]\n /** Promise of the running drain loop; never rejects. */\n done: Promise<void> | null\n}\n\nexport type UpdateQueueConfig<TKey, TRow> = {\n /** Sends one merged batch of changes and resolves with the canonical row. */\n send: (key: TKey, changes: Record<string, unknown>) => Promise<TRow>\n /** Called with the server's row before the batch's waiters resolve. Awaited,\n * so a handler that needs to recover (refetch) finishes first. */\n onResult: (row: TRow) => void | Promise<void>\n}\n\nexport type UpdateQueue<TKey> = {\n /** Queues changes for `key`. Resolves once a request carrying them lands. */\n enqueue: (key: TKey, changes: Record<string, unknown>) => Promise<void>\n /** Drops changes still queued for `key` (resolving their waiters) and waits\n * for any in-flight request to land. For operations that supersede pending\n * updates and must not race them — a delete of the same row. */\n settle: (key: TKey) => Promise<void>\n}\n\nexport function createUpdateQueue<TKey, TRow>(\n config: UpdateQueueConfig<TKey, TRow>,\n): UpdateQueue<TKey> {\n const slots = new Map<TKey, Slot>()\n\n async function drain(key: TKey, slot: Slot) {\n while (Object.keys(slot.changes).length > 0) {\n // Taking changes and waiters as one snapshot is what guarantees a waiter\n // resolves only after a request that carried its own changes.\n const changes = slot.changes\n const waiters = slot.waiters\n slot.changes = {}\n slot.waiters = []\n try {\n await config.onResult(await config.send(key, changes))\n for (const waiter of waiters) waiter.resolve()\n } catch (error) {\n // Replaying queued changes on top of a failed base would diverge from\n // the server silently, so the whole key is rolled back instead.\n const queued = slot.waiters\n slot.changes = {}\n slot.waiters = []\n for (const waiter of [...waiters, ...queued]) waiter.reject(error)\n }\n }\n slots.delete(key)\n }\n\n return {\n enqueue(key, changes) {\n let slot = slots.get(key)\n if (!slot) {\n slot = { changes: {}, waiters: [], done: null }\n slots.set(key, slot)\n }\n Object.assign(slot.changes, changes)\n const promise = new Promise<void>((resolve, reject) => {\n slot!.waiters.push({ resolve, reject })\n })\n if (!slot.done) slot.done = drain(key, slot)\n return promise\n },\n settle(key) {\n const slot = slots.get(key)\n if (!slot) return Promise.resolve()\n const waiters = slot.waiters\n slot.changes = {}\n slot.waiters = []\n for (const waiter of waiters) waiter.resolve()\n return slot.done ?? Promise.resolve()\n },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunderstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, email, and validated env from one config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backend",
|
|
@@ -107,6 +107,50 @@
|
|
|
107
107
|
"./api": {
|
|
108
108
|
"types": "./dist/api/types.d.ts",
|
|
109
109
|
"default": "./dist/api/types.js"
|
|
110
|
+
},
|
|
111
|
+
"./client": {
|
|
112
|
+
"types": "./dist/client/index.d.ts",
|
|
113
|
+
"default": "./dist/client/index.js"
|
|
114
|
+
},
|
|
115
|
+
"./client/rest": {
|
|
116
|
+
"types": "./dist/client/rest.d.ts",
|
|
117
|
+
"default": "./dist/client/rest.js"
|
|
118
|
+
},
|
|
119
|
+
"./client/react": {
|
|
120
|
+
"types": "./dist/client/react.d.ts",
|
|
121
|
+
"default": "./dist/client/react.js"
|
|
122
|
+
},
|
|
123
|
+
"./client/solid": {
|
|
124
|
+
"types": "./dist/client/solid.d.ts",
|
|
125
|
+
"default": "./dist/client/solid.js"
|
|
126
|
+
},
|
|
127
|
+
"./client/svelte": {
|
|
128
|
+
"types": "./dist/client/svelte.d.ts",
|
|
129
|
+
"default": "./dist/client/svelte.js"
|
|
130
|
+
},
|
|
131
|
+
"./client/vue": {
|
|
132
|
+
"types": "./dist/client/vue.d.ts",
|
|
133
|
+
"default": "./dist/client/vue.js"
|
|
134
|
+
},
|
|
135
|
+
"./query": {
|
|
136
|
+
"types": "./dist/query/index.d.ts",
|
|
137
|
+
"default": "./dist/query/index.js"
|
|
138
|
+
},
|
|
139
|
+
"./query/react": {
|
|
140
|
+
"types": "./dist/query/react.d.ts",
|
|
141
|
+
"default": "./dist/query/react.js"
|
|
142
|
+
},
|
|
143
|
+
"./sync": {
|
|
144
|
+
"types": "./dist/sync/index.d.ts",
|
|
145
|
+
"default": "./dist/sync/index.js"
|
|
146
|
+
},
|
|
147
|
+
"./start": {
|
|
148
|
+
"types": "./dist/start/index.d.ts",
|
|
149
|
+
"default": "./dist/start/index.js"
|
|
150
|
+
},
|
|
151
|
+
"./start/auth": {
|
|
152
|
+
"types": "./dist/start/auth-client.d.ts",
|
|
153
|
+
"default": "./dist/start/auth-client.js"
|
|
110
154
|
}
|
|
111
155
|
},
|
|
112
156
|
"scripts": {
|
|
@@ -119,7 +163,10 @@
|
|
|
119
163
|
"typecheck": "tsc --noEmit"
|
|
120
164
|
},
|
|
121
165
|
"dependencies": {
|
|
166
|
+
"@orpc/client": "2.0.0-beta.26",
|
|
167
|
+
"@orpc/server": "2.0.0-beta.26",
|
|
122
168
|
"@standard-schema/spec": "1.1.0",
|
|
169
|
+
"@standardserver/core": "0.7.1",
|
|
123
170
|
"valibot": "1.4.2",
|
|
124
171
|
"yaml": "^2.9.0"
|
|
125
172
|
},
|
|
@@ -127,33 +174,56 @@
|
|
|
127
174
|
"@electric-sql/pglite": ">=0.3.0",
|
|
128
175
|
"@libsql/client": ">=0.14.0",
|
|
129
176
|
"@orpc/bun": "2.0.0-beta.26",
|
|
177
|
+
"@orpc/client": "2.0.0-beta.26",
|
|
130
178
|
"@orpc/json-schema": "2.0.0-beta.26",
|
|
131
179
|
"@orpc/openapi": "2.0.0-beta.26",
|
|
132
180
|
"@orpc/publisher": "2.0.0-beta.26",
|
|
133
181
|
"@orpc/server": "2.0.0-beta.26",
|
|
182
|
+
"@orpc/tanstack-query": "2.0.0-beta.26",
|
|
134
183
|
"@orpc/valibot": "2.0.0-beta.26",
|
|
184
|
+
"@standardserver/core": "0.7.1",
|
|
185
|
+
"@tanstack/db": "0.6.16",
|
|
186
|
+
"@tanstack/query-core": "^5.101.1",
|
|
187
|
+
"@tanstack/query-db-collection": "1.1.0",
|
|
188
|
+
"@tanstack/react-query": "^5.101.1",
|
|
189
|
+
"@tanstack/react-start": "^1.168.26",
|
|
135
190
|
"@types/nodemailer": "^6",
|
|
191
|
+
"@types/react": "^19.0.0",
|
|
136
192
|
"better-auth": "^1.0.0",
|
|
137
193
|
"drizzle-kit": "^0.30.0",
|
|
138
194
|
"drizzle-orm": "^0.45.0",
|
|
139
|
-
"drizzle-valibot": "0.4.2"
|
|
195
|
+
"drizzle-valibot": "0.4.2",
|
|
196
|
+
"react": "^19.0.0",
|
|
197
|
+
"solid-js": "2.0.0-rc.0",
|
|
198
|
+
"typescript": "^5.8.0",
|
|
199
|
+
"vue": "^3.5.0"
|
|
140
200
|
},
|
|
141
201
|
"peerDependencies": {
|
|
142
202
|
"@electric-sql/pglite": ">=0.3.0",
|
|
143
203
|
"@libsql/client": ">=0.14.0",
|
|
144
204
|
"@orpc/bun": "2.0.0-beta.26",
|
|
205
|
+
"@orpc/client": "2.0.0-beta.26",
|
|
145
206
|
"@orpc/json-schema": "2.0.0-beta.26",
|
|
146
207
|
"@orpc/openapi": "2.0.0-beta.26",
|
|
147
208
|
"@orpc/publisher": "2.0.0-beta.26",
|
|
148
209
|
"@orpc/server": "2.0.0-beta.26",
|
|
210
|
+
"@orpc/tanstack-query": "2.0.0-beta.26",
|
|
149
211
|
"@orpc/valibot": "2.0.0-beta.26",
|
|
212
|
+
"@tanstack/db": "0.6.16",
|
|
213
|
+
"@tanstack/query-core": "^5.101.0",
|
|
214
|
+
"@tanstack/query-db-collection": "1.1.0",
|
|
215
|
+
"@tanstack/react-query": "^5.101.0",
|
|
216
|
+
"@tanstack/react-start": ">=1.0.0",
|
|
150
217
|
"better-auth": "^1.0.0",
|
|
151
218
|
"drizzle-kit": "^0.30.0",
|
|
152
219
|
"drizzle-orm": "^0.45.0",
|
|
153
220
|
"drizzle-valibot": "0.4.2",
|
|
154
221
|
"nodemailer": ">=6 <10",
|
|
155
222
|
"postgres": ">=3.4.0",
|
|
156
|
-
"
|
|
223
|
+
"react": ">=18",
|
|
224
|
+
"solid-js": ">=2.0.0-rc.0",
|
|
225
|
+
"typescript": ">=5",
|
|
226
|
+
"vue": ">=3.5"
|
|
157
227
|
},
|
|
158
228
|
"peerDependenciesMeta": {
|
|
159
229
|
"@electric-sql/pglite": {
|
|
@@ -162,6 +232,27 @@
|
|
|
162
232
|
"@libsql/client": {
|
|
163
233
|
"optional": true
|
|
164
234
|
},
|
|
235
|
+
"@orpc/tanstack-query": {
|
|
236
|
+
"optional": true
|
|
237
|
+
},
|
|
238
|
+
"@tanstack/db": {
|
|
239
|
+
"optional": true
|
|
240
|
+
},
|
|
241
|
+
"@tanstack/query-core": {
|
|
242
|
+
"optional": true
|
|
243
|
+
},
|
|
244
|
+
"@tanstack/query-db-collection": {
|
|
245
|
+
"optional": true
|
|
246
|
+
},
|
|
247
|
+
"@tanstack/react-query": {
|
|
248
|
+
"optional": true
|
|
249
|
+
},
|
|
250
|
+
"@tanstack/react-start": {
|
|
251
|
+
"optional": true
|
|
252
|
+
},
|
|
253
|
+
"better-auth": {
|
|
254
|
+
"optional": true
|
|
255
|
+
},
|
|
165
256
|
"drizzle-kit": {
|
|
166
257
|
"optional": true
|
|
167
258
|
},
|
|
@@ -171,8 +262,17 @@
|
|
|
171
262
|
"postgres": {
|
|
172
263
|
"optional": true
|
|
173
264
|
},
|
|
265
|
+
"react": {
|
|
266
|
+
"optional": true
|
|
267
|
+
},
|
|
268
|
+
"solid-js": {
|
|
269
|
+
"optional": true
|
|
270
|
+
},
|
|
174
271
|
"typescript": {
|
|
175
272
|
"optional": true
|
|
273
|
+
},
|
|
274
|
+
"vue": {
|
|
275
|
+
"optional": true
|
|
176
276
|
}
|
|
177
277
|
}
|
|
178
278
|
}
|
|
@@ -61,7 +61,7 @@ client filters it before cache callbacks and does not advance the Publisher
|
|
|
61
61
|
resume ID. Do not add an application polling loop or publish heartbeat events
|
|
62
62
|
through oRPC Publisher.
|
|
63
63
|
|
|
64
|
-
`bunderstack
|
|
64
|
+
`bunderstack/sync` reconciles successful mutations from the canonical row
|
|
65
65
|
returned by generated CRUD, without a follow-up list refetch. Realtime echoes
|
|
66
66
|
are idempotent, and reconnect performs the full refetch used to repair drift.
|
|
67
67
|
Keep custom replacement procedures compatible by returning the complete row,
|
|
@@ -94,7 +94,7 @@ circular evaluation loop at boot.
|
|
|
94
94
|
One catch-all route, built by the adapter rather than by hand:
|
|
95
95
|
|
|
96
96
|
```ts
|
|
97
|
-
import { createApiHandlers } from 'bunderstack
|
|
97
|
+
import { createApiHandlers } from 'bunderstack/start'
|
|
98
98
|
import { createFileRoute } from '@tanstack/react-router'
|
|
99
99
|
import { app } from '../../bunderstack'
|
|
100
100
|
|
|
@@ -188,7 +188,7 @@ query client consumes it automatically without updating cache state or the
|
|
|
188
188
|
Publisher resume ID. Delete custom polling, keepalive, SSE registration, and
|
|
189
189
|
client reconnect loops instead of wrapping them around the oRPC stream.
|
|
190
190
|
|
|
191
|
-
For TanStack DB applications, use `bunderstack
|
|
191
|
+
For TanStack DB applications, use `bunderstack/sync`. Successful mutations are
|
|
192
192
|
reconciled from their complete server response without a follow-up `list`
|
|
193
193
|
refetch, and same-row updates are coalesced while a request is in flight.
|
|
194
194
|
|