bunderstack-sync 0.17.0-beta.2 → 0.17.0-beta.4

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.
@@ -1,97 +0,0 @@
1
- import type { QueryClient } from '@tanstack/react-query'
2
- import {
3
- createClient,
4
- type AnyBunderstackApp,
5
- type BunderstackClient,
6
- type InferInsert,
7
- type InferSchema,
8
- type InferSelect,
9
- type InferTables,
10
- } from 'bunderstack-query'
11
-
12
- import { createTableCollection, type TableCollection } from './collection'
13
- import { createSyncRealtimeClient, type SyncRealtimeTarget } from './realtime-sync'
14
-
15
- export type RowFor<TSchema extends Record<string, unknown>, K extends keyof TSchema> =
16
- [InferSelect<TSchema[K]>] extends [never]
17
- ? { id: string | number }
18
- : InferSelect<TSchema[K]> extends { id: string | number }
19
- ? InferSelect<TSchema[K]>
20
- : { id: string | number }
21
-
22
- export type CreateFor<TSchema extends Record<string, unknown>, K extends keyof TSchema> =
23
- [InferInsert<TSchema[K]>] extends [never]
24
- ? Partial<RowFor<TSchema, K>>
25
- : InferInsert<TSchema[K]>
26
-
27
- export type SyncClientOptions = {
28
- baseUrl?: string
29
- fetch?: (
30
- input: RequestInfo | URL,
31
- init?: RequestInit,
32
- ) => Promise<Response>
33
- queryClient: QueryClient
34
- realtime?: boolean
35
- }
36
-
37
- export type BunderstackSyncClient<TApp extends AnyBunderstackApp> = {
38
- [K in InferTables<TApp>]: TableCollection<
39
- RowFor<InferSchema<TApp>, K>,
40
- CreateFor<InferSchema<TApp>, K>,
41
- Partial<RowFor<InferSchema<TApp>, K>>
42
- >
43
- } & {
44
- files: BunderstackClient<TApp>['files']
45
- realtime: { close(): void; subscribe(tables: string[]): Promise<void> } | undefined
46
- }
47
-
48
- export function createSyncClient<TApp extends AnyBunderstackApp>(
49
- options: SyncClientOptions,
50
- ): BunderstackSyncClient<TApp> {
51
- const api = createClient<TApp>(options)
52
- const materialized = new Map<string, SyncRealtimeTarget>()
53
- const realtimeHandles = new Map<string, { close(): void }>()
54
- const tables = new Map<string, unknown>()
55
- const realtimeEnabled = options.realtime ?? typeof window !== 'undefined'
56
-
57
- const result = new Proxy({} as BunderstackSyncClient<TApp>, {
58
- get(_target, property) {
59
- if (typeof property !== 'string') return undefined
60
- if (property === 'files') return api.files
61
- if (property === 'realtime') {
62
- return realtimeEnabled
63
- ? {
64
- close: () => realtimeHandles.forEach((handle) => handle.close()),
65
- subscribe: async (names: string[]) => {
66
- for (const name of names) void (result as any)[name]
67
- },
68
- }
69
- : undefined
70
- }
71
- if (['then', 'toJSON', 'constructor', '$$typeof'].includes(property)) return undefined
72
- const cached = tables.get(property)
73
- if (cached) return cached
74
- const collection = createTableCollection({
75
- tableName: property,
76
- procedures: (api as any)[property],
77
- queryClient: options.queryClient,
78
- })
79
- tables.set(property, collection)
80
- materialized.set(property, collection)
81
- if (realtimeEnabled) {
82
- realtimeHandles.set(
83
- property,
84
- createSyncRealtimeClient({
85
- api: api as any,
86
- queryClient: options.queryClient,
87
- tables: [property],
88
- resolve: (table) => materialized.get(table),
89
- resolveAll: () => materialized.values(),
90
- }),
91
- )
92
- }
93
- return collection
94
- },
95
- })
96
- return result
97
- }
@@ -1,93 +0,0 @@
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
-
12
- type Waiter = {
13
- resolve: () => void
14
- reject: (error: unknown) => void
15
- }
16
-
17
- type Slot = {
18
- changes: Record<string, unknown>
19
- waiters: Waiter[]
20
- /** Promise of the running drain loop; never rejects. */
21
- done: Promise<void> | null
22
- }
23
-
24
- export type UpdateQueueConfig<TKey, TRow> = {
25
- /** Sends one merged batch of changes and resolves with the canonical row. */
26
- send: (key: TKey, changes: Record<string, unknown>) => Promise<TRow>
27
- /** Called with the server's row before the batch's waiters resolve. Awaited,
28
- * so a handler that needs to recover (refetch) finishes first. */
29
- onResult: (row: TRow) => void | Promise<void>
30
- }
31
-
32
- export type UpdateQueue<TKey> = {
33
- /** Queues changes for `key`. Resolves once a request carrying them lands. */
34
- enqueue: (key: TKey, changes: Record<string, unknown>) => Promise<void>
35
- /** Drops changes still queued for `key` (resolving their waiters) and waits
36
- * for any in-flight request to land. For operations that supersede pending
37
- * updates and must not race them — a delete of the same row. */
38
- settle: (key: TKey) => Promise<void>
39
- }
40
-
41
- export function createUpdateQueue<TKey, TRow>(
42
- config: UpdateQueueConfig<TKey, TRow>,
43
- ): UpdateQueue<TKey> {
44
- const slots = new Map<TKey, Slot>()
45
-
46
- async function drain(key: TKey, slot: Slot) {
47
- while (Object.keys(slot.changes).length > 0) {
48
- // Taking changes and waiters as one snapshot is what guarantees a waiter
49
- // resolves only after a request that carried its own changes.
50
- const changes = slot.changes
51
- const waiters = slot.waiters
52
- slot.changes = {}
53
- slot.waiters = []
54
- try {
55
- await config.onResult(await config.send(key, changes))
56
- for (const waiter of waiters) waiter.resolve()
57
- } catch (error) {
58
- // Replaying queued changes on top of a failed base would diverge from
59
- // the server silently, so the whole key is rolled back instead.
60
- const queued = slot.waiters
61
- slot.changes = {}
62
- slot.waiters = []
63
- for (const waiter of [...waiters, ...queued]) waiter.reject(error)
64
- }
65
- }
66
- slots.delete(key)
67
- }
68
-
69
- return {
70
- enqueue(key, changes) {
71
- let slot = slots.get(key)
72
- if (!slot) {
73
- slot = { changes: {}, waiters: [], done: null }
74
- slots.set(key, slot)
75
- }
76
- Object.assign(slot.changes, changes)
77
- const promise = new Promise<void>((resolve, reject) => {
78
- slot!.waiters.push({ resolve, reject })
79
- })
80
- if (!slot.done) slot.done = drain(key, slot)
81
- return promise
82
- },
83
- settle(key) {
84
- const slot = slots.get(key)
85
- if (!slot) return Promise.resolve()
86
- const waiters = slot.waiters
87
- slot.changes = {}
88
- slot.waiters = []
89
- for (const waiter of waiters) waiter.resolve()
90
- return slot.done ?? Promise.resolve()
91
- },
92
- }
93
- }