velocious 1.0.511 → 1.0.512
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/README.md +1 -0
- package/build/database/live-query.js +244 -0
- package/build/database/record/index.js +25 -0
- package/build/database/record-changes.js +135 -0
- package/build/database/use-live-query.js +118 -0
- package/build/src/database/live-query.d.ts +168 -0
- package/build/src/database/live-query.d.ts.map +1 -0
- package/build/src/database/live-query.js +211 -0
- package/build/src/database/record/index.d.ts +11 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +23 -1
- package/build/src/database/record-changes.d.ts +111 -0
- package/build/src/database/record-changes.d.ts.map +1 -0
- package/build/src/database/record-changes.js +115 -0
- package/build/src/database/use-live-query.d.ts +44 -0
- package/build/src/database/use-live-query.d.ts.map +1 -0
- package/build/src/database/use-live-query.js +97 -0
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +17 -10
- package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
- package/build/src/sync/sync-realtime-bridge.js +9 -5
- package/build/sync/sync-api-client.js +19 -11
- package/build/sync/sync-realtime-bridge.js +9 -4
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/database/live-query.js +244 -0
- package/src/database/record/index.js +25 -0
- package/src/database/record-changes.js +135 -0
- package/src/database/use-live-query.js +118 -0
- package/src/sync/sync-api-client.js +19 -11
- package/src/sync/sync-realtime-bridge.js +9 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {useCallback, useEffect, useMemo, useRef, useSyncExternalStore} from "react"
|
|
4
|
+
|
|
5
|
+
import LiveQuery from "./live-query.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RecordModelClass type.
|
|
9
|
+
* @typedef {typeof import("./record/index.js").default} RecordModelClass */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* LiveQuerySource type.
|
|
13
|
+
* @template T
|
|
14
|
+
* @typedef {import("./live-query.js").LiveQuerySource<T>} LiveQuerySource */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* LiveQueryState type.
|
|
18
|
+
* @template T
|
|
19
|
+
* @typedef {import("./live-query.js").LiveQueryState<T>} LiveQueryState */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* UseLiveQueryOptions type.
|
|
23
|
+
* @typedef {object} UseLiveQueryOptions
|
|
24
|
+
* @property {boolean} [active] - Whether the query is active. Default true; pass false to pause and return the empty state.
|
|
25
|
+
* @property {number} [debounce] - Trailing debounce in ms for re-runs. Defaults to microtask coalescing.
|
|
26
|
+
* @property {RecordModelClass[]} [models] - Model classes to observe. Defaults to the query's model class; pass this to also react to joined models.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stable empty state returned while there is no active query, so a paused hook
|
|
31
|
+
* keeps a referentially stable snapshot for `useSyncExternalStore`.
|
|
32
|
+
* @type {LiveQueryState<?>} */
|
|
33
|
+
const EMPTY_STATE = {error: null, loading: false, results: []}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Assigns and stores a stable identity key for query sources without a `toSql`.
|
|
37
|
+
* @type {WeakMap<object, string>} */
|
|
38
|
+
const queryIdentityKeys = new WeakMap()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Monotonic counter backing the queryIdentityKeys registry.
|
|
42
|
+
* @type {number} */
|
|
43
|
+
let nextQueryIdentity = 0
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds a dependency key identifying a query's semantics so the underlying
|
|
47
|
+
* controller is rebuilt when they change. Model-class queries expose `toSql`, so
|
|
48
|
+
* distinct conditions yield distinct keys; other sources fall back to a stable
|
|
49
|
+
* per-object identity (such sources must be memoized by the caller).
|
|
50
|
+
* @param {LiveQuerySource<?> & {toSql?: () => ?}} query - Query source.
|
|
51
|
+
* @param {RecordModelClass[] | undefined} models - Explicit model classes to observe.
|
|
52
|
+
* @returns {string} Dependency key.
|
|
53
|
+
*/
|
|
54
|
+
function liveQueryDependencyKey(query, models) {
|
|
55
|
+
const modelNames = (models ?? [query.getModelClass()]).map((modelClass) => modelClass.getModelName()).join(",")
|
|
56
|
+
|
|
57
|
+
if (typeof query.toSql === "function") return `${modelNames}::${String(query.toSql())}`
|
|
58
|
+
|
|
59
|
+
let identityKey = queryIdentityKeys.get(query)
|
|
60
|
+
|
|
61
|
+
if (identityKey === undefined) {
|
|
62
|
+
identityKey = `#${++nextQueryIdentity}`
|
|
63
|
+
queryIdentityKeys.set(query, identityKey)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return `${modelNames}::${identityKey}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* React hook declaring what a screen shows and keeping it current from committed
|
|
71
|
+
* local model changes. Runs `query.toArray()` once, subscribes to the query's
|
|
72
|
+
* model class(es) on the record-change bus, and re-runs (coalesced, stale-safe)
|
|
73
|
+
* whenever a watched model commits — so local writes, pull applies, and realtime
|
|
74
|
+
* applies all refresh the results without any manual refresh plumbing.
|
|
75
|
+
* @template T
|
|
76
|
+
* @param {(LiveQuerySource<T> & {toSql?: () => ?}) | null | undefined} query - Query source, e.g. `Model.where({...})`.
|
|
77
|
+
* @param {UseLiveQueryOptions} [options] - Hook options.
|
|
78
|
+
* @returns {LiveQueryState<T>} Current results, loading, and last error.
|
|
79
|
+
*/
|
|
80
|
+
export default function useLiveQuery(query, options = {}) {
|
|
81
|
+
const {active = true, debounce, models} = options
|
|
82
|
+
const enabled = active && Boolean(query)
|
|
83
|
+
const dependencyKey = enabled && query ? liveQueryDependencyKey(query, models) : "disabled"
|
|
84
|
+
|
|
85
|
+
const queryRef = useRef(query)
|
|
86
|
+
const modelsRef = useRef(models)
|
|
87
|
+
|
|
88
|
+
queryRef.current = query
|
|
89
|
+
modelsRef.current = models
|
|
90
|
+
|
|
91
|
+
const liveQuery = useMemo(() => {
|
|
92
|
+
if (!enabled || !queryRef.current) return null
|
|
93
|
+
|
|
94
|
+
return new LiveQuery({debounce, models: modelsRef.current, query: queryRef.current})
|
|
95
|
+
}, [dependencyKey, debounce, enabled])
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!liveQuery) return undefined
|
|
99
|
+
|
|
100
|
+
liveQuery.start()
|
|
101
|
+
|
|
102
|
+
return () => liveQuery.close()
|
|
103
|
+
}, [liveQuery])
|
|
104
|
+
|
|
105
|
+
const subscribe = useCallback((/** @type {() => void} */ listener) => {
|
|
106
|
+
if (!liveQuery) return () => {}
|
|
107
|
+
|
|
108
|
+
return liveQuery.subscribe(listener)
|
|
109
|
+
}, [liveQuery])
|
|
110
|
+
|
|
111
|
+
const getSnapshot = useCallback(() => {
|
|
112
|
+
if (!liveQuery) return /** @type {LiveQueryState<T>} */ (EMPTY_STATE)
|
|
113
|
+
|
|
114
|
+
return liveQuery.getState()
|
|
115
|
+
}, [liveQuery])
|
|
116
|
+
|
|
117
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
118
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import {optionalBoolean, optionalInteger} from "typanic"
|
|
4
4
|
|
|
5
|
+
import recordChanges from "../database/record-changes.js"
|
|
6
|
+
|
|
5
7
|
const syncTaskPromises = new Map()
|
|
6
8
|
|
|
7
9
|
/** @typedef {import("./sync-api-client-types.js").SyncChangeApplyResult} SyncChangeApplyResult */
|
|
@@ -128,18 +130,24 @@ export default class SyncApiClient {
|
|
|
128
130
|
|
|
129
131
|
pages += 1
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
// Coalesce record-change events across this page's applies so N applied rows trigger one
|
|
134
|
+
// live-query re-run. Only the apply loop is batched: the network page fetch above and the
|
|
135
|
+
// cursor save below stay outside, so live queries flush right after the applies instead of
|
|
136
|
+
// waiting for the rest of the pull.
|
|
137
|
+
await recordChanges.batch(async () => {
|
|
138
|
+
for (const sync of syncs) {
|
|
139
|
+
const applyResult = await args.applySync(sync)
|
|
140
|
+
const resourceType = applyResult.resourceType ?? sync.resourceType()
|
|
141
|
+
|
|
142
|
+
changed ||= applyResult.changed === true
|
|
143
|
+
syncedCount += 1
|
|
144
|
+
|
|
145
|
+
if (resourceType) {
|
|
146
|
+
resourceCounts[resourceType] = (resourceCounts[resourceType] || 0) + 1
|
|
147
|
+
resourceChanged[resourceType] ||= applyResult.changed === true
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
|
-
}
|
|
150
|
+
})
|
|
143
151
|
|
|
144
152
|
afterCursor = changesResponse.nextCursor
|
|
145
153
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import recordChanges from "../database/record-changes.js"
|
|
4
|
+
|
|
3
5
|
import SyncApiClient from "./sync-api-client.js"
|
|
4
6
|
import {VELOCIOUS_SYNC_CHANNEL} from "./sync-channel-name.js"
|
|
5
7
|
|
|
@@ -350,11 +352,14 @@ export default class SyncRealtimeBridge {
|
|
|
350
352
|
const syncPayloads = Array.isArray(body.syncs) ? body.syncs : [body]
|
|
351
353
|
const applySync = this.syncClient.remoteApplySync({source: "remote change"})
|
|
352
354
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
// Coalesce record-change events across the pushed batch so it triggers one live-query re-run.
|
|
356
|
+
await recordChanges.batch(async () => {
|
|
357
|
+
for (const syncPayload of syncPayloads) {
|
|
358
|
+
const sync = SyncApiClient.syncEnvelopeFromPayload({resourceType, ...syncPayload})
|
|
355
359
|
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
await applySync(sync)
|
|
361
|
+
}
|
|
362
|
+
})
|
|
358
363
|
}
|
|
359
364
|
|
|
360
365
|
/**
|