bunderstack-sync 0.17.0-beta.2 → 0.17.0-beta.3
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/dist/collection.d.ts +106 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +258 -0
- package/dist/collection.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/realtime-sync.d.ts +25 -0
- package/dist/realtime-sync.d.ts.map +1 -0
- package/dist/realtime-sync.js +31 -0
- package/dist/realtime-sync.js.map +1 -0
- package/dist/sync-client.d.ts +32 -0
- package/dist/sync-client.d.ts.map +1 -0
- package/dist/sync-client.js +53 -0
- package/dist/sync-client.js.map +1 -0
- package/dist/update-queue.d.ts +27 -0
- package/dist/update-queue.d.ts.map +1 -0
- package/dist/update-queue.js +66 -0
- package/dist/update-queue.js.map +1 -0
- package/package.json +12 -9
- package/src/collection.ts +0 -424
- package/src/index.ts +0 -7
- package/src/realtime-sync.ts +0 -61
- package/src/sync-client.ts +0 -97
- package/src/update-queue.ts +0 -93
package/src/sync-client.ts
DELETED
|
@@ -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
|
-
}
|
package/src/update-queue.ts
DELETED
|
@@ -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
|
-
}
|