bunderstack 0.19.2 → 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 +39 -1
- package/dist/api/crud-router.d.ts +23 -28
- package/dist/api/crud-router.d.ts.map +1 -1
- package/dist/api/crud-router.js +39 -18
- package/dist/api/crud-router.js.map +1 -1
- package/dist/api/live-window.d.ts.map +1 -1
- package/dist/api/live-window.js +13 -3
- package/dist/api/live-window.js.map +1 -1
- package/dist/api/realtime-router.d.ts +3 -1
- package/dist/api/realtime-router.d.ts.map +1 -1
- package/dist/api/realtime-router.js +1 -0
- package/dist/api/realtime-router.js.map +1 -1
- package/dist/api/types.d.ts +2 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +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/codegen.d.ts +36 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +235 -0
- package/dist/codegen.js.map +1 -0
- package/dist/crud-operations.d.ts.map +1 -1
- package/dist/crud-operations.js +7 -3
- package/dist/crud-operations.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live/protocol.d.ts +3 -0
- package/dist/live/protocol.d.ts.map +1 -1
- package/dist/live/protocol.js.map +1 -1
- 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/realtime/facade.d.ts +3 -1
- package/dist/realtime/facade.d.ts.map +1 -1
- package/dist/realtime/facade.js +2 -1
- package/dist/realtime/facade.js.map +1 -1
- package/dist/realtime/filter.d.ts.map +1 -1
- package/dist/realtime/filter.js +1 -0
- package/dist/realtime/filter.js.map +1 -1
- package/dist/realtime/publisher.d.ts +2 -0
- package/dist/realtime/publisher.d.ts.map +1 -1
- package/dist/realtime/publisher.js.map +1 -1
- 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 +107 -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 @@
|
|
|
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",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/access.d.ts",
|
|
45
45
|
"default": "./dist/access.js"
|
|
46
46
|
},
|
|
47
|
+
"./codegen": {
|
|
48
|
+
"types": "./dist/codegen.d.ts",
|
|
49
|
+
"default": "./dist/codegen.js"
|
|
50
|
+
},
|
|
47
51
|
"./database/bun-sql": {
|
|
48
52
|
"types": "./dist/database/bun-sql.d.ts",
|
|
49
53
|
"default": "./dist/database/bun-sql.js"
|
|
@@ -103,6 +107,50 @@
|
|
|
103
107
|
"./api": {
|
|
104
108
|
"types": "./dist/api/types.d.ts",
|
|
105
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"
|
|
106
154
|
}
|
|
107
155
|
},
|
|
108
156
|
"scripts": {
|
|
@@ -115,7 +163,10 @@
|
|
|
115
163
|
"typecheck": "tsc --noEmit"
|
|
116
164
|
},
|
|
117
165
|
"dependencies": {
|
|
166
|
+
"@orpc/client": "2.0.0-beta.26",
|
|
167
|
+
"@orpc/server": "2.0.0-beta.26",
|
|
118
168
|
"@standard-schema/spec": "1.1.0",
|
|
169
|
+
"@standardserver/core": "0.7.1",
|
|
119
170
|
"valibot": "1.4.2",
|
|
120
171
|
"yaml": "^2.9.0"
|
|
121
172
|
},
|
|
@@ -123,33 +174,56 @@
|
|
|
123
174
|
"@electric-sql/pglite": ">=0.3.0",
|
|
124
175
|
"@libsql/client": ">=0.14.0",
|
|
125
176
|
"@orpc/bun": "2.0.0-beta.26",
|
|
177
|
+
"@orpc/client": "2.0.0-beta.26",
|
|
126
178
|
"@orpc/json-schema": "2.0.0-beta.26",
|
|
127
179
|
"@orpc/openapi": "2.0.0-beta.26",
|
|
128
180
|
"@orpc/publisher": "2.0.0-beta.26",
|
|
129
181
|
"@orpc/server": "2.0.0-beta.26",
|
|
182
|
+
"@orpc/tanstack-query": "2.0.0-beta.26",
|
|
130
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",
|
|
131
190
|
"@types/nodemailer": "^6",
|
|
191
|
+
"@types/react": "^19.0.0",
|
|
132
192
|
"better-auth": "^1.0.0",
|
|
133
193
|
"drizzle-kit": "^0.30.0",
|
|
134
194
|
"drizzle-orm": "^0.45.0",
|
|
135
|
-
"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"
|
|
136
200
|
},
|
|
137
201
|
"peerDependencies": {
|
|
138
202
|
"@electric-sql/pglite": ">=0.3.0",
|
|
139
203
|
"@libsql/client": ">=0.14.0",
|
|
140
204
|
"@orpc/bun": "2.0.0-beta.26",
|
|
205
|
+
"@orpc/client": "2.0.0-beta.26",
|
|
141
206
|
"@orpc/json-schema": "2.0.0-beta.26",
|
|
142
207
|
"@orpc/openapi": "2.0.0-beta.26",
|
|
143
208
|
"@orpc/publisher": "2.0.0-beta.26",
|
|
144
209
|
"@orpc/server": "2.0.0-beta.26",
|
|
210
|
+
"@orpc/tanstack-query": "2.0.0-beta.26",
|
|
145
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",
|
|
146
217
|
"better-auth": "^1.0.0",
|
|
147
218
|
"drizzle-kit": "^0.30.0",
|
|
148
219
|
"drizzle-orm": "^0.45.0",
|
|
149
220
|
"drizzle-valibot": "0.4.2",
|
|
150
221
|
"nodemailer": ">=6 <10",
|
|
151
222
|
"postgres": ">=3.4.0",
|
|
152
|
-
"
|
|
223
|
+
"react": ">=18",
|
|
224
|
+
"solid-js": ">=2.0.0-rc.0",
|
|
225
|
+
"typescript": ">=5",
|
|
226
|
+
"vue": ">=3.5"
|
|
153
227
|
},
|
|
154
228
|
"peerDependenciesMeta": {
|
|
155
229
|
"@electric-sql/pglite": {
|
|
@@ -158,6 +232,27 @@
|
|
|
158
232
|
"@libsql/client": {
|
|
159
233
|
"optional": true
|
|
160
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
|
+
},
|
|
161
256
|
"drizzle-kit": {
|
|
162
257
|
"optional": true
|
|
163
258
|
},
|
|
@@ -167,8 +262,17 @@
|
|
|
167
262
|
"postgres": {
|
|
168
263
|
"optional": true
|
|
169
264
|
},
|
|
265
|
+
"react": {
|
|
266
|
+
"optional": true
|
|
267
|
+
},
|
|
268
|
+
"solid-js": {
|
|
269
|
+
"optional": true
|
|
270
|
+
},
|
|
170
271
|
"typescript": {
|
|
171
272
|
"optional": true
|
|
273
|
+
},
|
|
274
|
+
"vue": {
|
|
275
|
+
"optional": true
|
|
172
276
|
}
|
|
173
277
|
}
|
|
174
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
|
|