velocious 1.0.511 → 1.0.513
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 +3 -2
- 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/environment-handlers/node.js +24 -1
- 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/environment-handlers/node.d.ts +12 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +21 -2
- 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/environment-handlers/node.js +24 -1
- 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
|
+
}
|
|
@@ -1050,13 +1050,36 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
1050
1050
|
const structureSql = await db.structureSql()
|
|
1051
1051
|
|
|
1052
1052
|
if (structureSql) {
|
|
1053
|
-
|
|
1053
|
+
const migrationInserts = await this._schemaMigrationsInsertSql({db})
|
|
1054
|
+
|
|
1055
|
+
sqlByIdentifier[identifier] = structureSql + migrationInserts
|
|
1054
1056
|
}
|
|
1055
1057
|
}
|
|
1056
1058
|
|
|
1057
1059
|
return sqlByIdentifier
|
|
1058
1060
|
}
|
|
1059
1061
|
|
|
1062
|
+
/**
|
|
1063
|
+
* Generates INSERT statements for every row in `schema_migrations` so the
|
|
1064
|
+
* structure snapshot carries the migration ledger along with the DDL. Without
|
|
1065
|
+
* these rows a fresh DB loaded from the snapshot will re-run every migration,
|
|
1066
|
+
* which fails when the snapshot already contains the post-migration schema.
|
|
1067
|
+
* @param {object} args - Options object.
|
|
1068
|
+
* @param {import("../database/drivers/base.js").default} args.db - Database connection.
|
|
1069
|
+
* @returns {Promise<string>} - INSERT statements (empty string when none).
|
|
1070
|
+
*/
|
|
1071
|
+
async _schemaMigrationsInsertSql({db}) {
|
|
1072
|
+
const {default: MigrationsLedger} = await import("../database/migrations-ledger.js")
|
|
1073
|
+
|
|
1074
|
+
if (!await MigrationsLedger.tableExists(db)) return ""
|
|
1075
|
+
|
|
1076
|
+
const versions = await MigrationsLedger.appliedVersions(db)
|
|
1077
|
+
|
|
1078
|
+
if (versions.length == 0) return ""
|
|
1079
|
+
|
|
1080
|
+
return versions.map((version) => `INSERT INTO schema_migrations (version) VALUES (${db.quote(version)});`).join("\n") + "\n"
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1060
1083
|
/**
|
|
1061
1084
|
* Registers frontend-model websocket channel publishers so lifecycle
|
|
1062
1085
|
* event hooks broadcast over the shared "frontend-models" channel.
|
|
@@ -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
|
/**
|