@koolbase/core 12.5.1 → 12.7.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/dist/cjs/collection-controller.d.ts +7 -0
- package/dist/cjs/collection-controller.js +16 -0
- package/dist/cjs/database.js +14 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/query-refresh.d.ts +10 -0
- package/dist/cjs/query-refresh.js +41 -0
- package/dist/cjs/record-controller.d.ts +36 -0
- package/dist/cjs/record-controller.js +110 -0
- package/dist/esm/collection-controller.d.ts +7 -0
- package/dist/esm/collection-controller.js +16 -0
- package/dist/esm/database.js +14 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/query-refresh.d.ts +10 -0
- package/dist/esm/query-refresh.js +36 -0
- package/dist/esm/record-controller.d.ts +36 -0
- package/dist/esm/record-controller.js +106 -0
- package/package.json +2 -2
|
@@ -38,6 +38,7 @@ export declare class KoolbaseCollectionController {
|
|
|
38
38
|
private pageOne;
|
|
39
39
|
private laterPages;
|
|
40
40
|
private nextOffset;
|
|
41
|
+
private unregister;
|
|
41
42
|
constructor(db: QueryReader, collection: string, query?: CollectionQuery);
|
|
42
43
|
getState(): CollectionState;
|
|
43
44
|
/** Called on every change. Returns the unsubscribe. */
|
|
@@ -46,6 +47,12 @@ export declare class KoolbaseCollectionController {
|
|
|
46
47
|
load(): Promise<void>;
|
|
47
48
|
/** Page one again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
48
49
|
refresh(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* After a write to this collection: page one again from the server,
|
|
52
|
+
* SILENTLY -- no `refreshing`, which is the user's pull-to-refresh. What is
|
|
53
|
+
* shown stays while it runs, and stays if it fails.
|
|
54
|
+
*/
|
|
55
|
+
private refreshAfterWrite;
|
|
49
56
|
/**
|
|
50
57
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
51
58
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.KoolbaseCollectionController = exports.initialCollectionState = void 0;
|
|
27
27
|
exports.collectionQueryKey = collectionQueryKey;
|
|
28
|
+
const query_refresh_js_1 = require("./query-refresh.js");
|
|
28
29
|
exports.initialCollectionState = Object.freeze({
|
|
29
30
|
status: 'loading',
|
|
30
31
|
records: Object.freeze([]),
|
|
@@ -73,6 +74,8 @@ class KoolbaseCollectionController {
|
|
|
73
74
|
// Where the next page starts on the server. Not records.length: once rows
|
|
74
75
|
// are de-duplicated, a shifted page could otherwise be requested forever.
|
|
75
76
|
this.nextOffset = 0;
|
|
77
|
+
// A write to this collection re-runs this query (query-refresh.ts).
|
|
78
|
+
this.unregister = null;
|
|
76
79
|
this.db = db;
|
|
77
80
|
this.collection = collection;
|
|
78
81
|
this.spec = query;
|
|
@@ -93,6 +96,7 @@ class KoolbaseCollectionController {
|
|
|
93
96
|
async load() {
|
|
94
97
|
if (this.disposed)
|
|
95
98
|
return;
|
|
99
|
+
this.unregister ?? (this.unregister = (0, query_refresh_js_1.registerQueryRefresher)(this.collection, () => this.refreshAfterWrite()));
|
|
96
100
|
await this.firstPage('default');
|
|
97
101
|
}
|
|
98
102
|
/** Page one again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
@@ -104,6 +108,16 @@ class KoolbaseCollectionController {
|
|
|
104
108
|
if (!this.isStale(gen))
|
|
105
109
|
this.set({ refreshing: false });
|
|
106
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* After a write to this collection: page one again from the server,
|
|
113
|
+
* SILENTLY -- no `refreshing`, which is the user's pull-to-refresh. What is
|
|
114
|
+
* shown stays while it runs, and stays if it fails.
|
|
115
|
+
*/
|
|
116
|
+
async refreshAfterWrite() {
|
|
117
|
+
if (this.disposed)
|
|
118
|
+
return;
|
|
119
|
+
await this.firstPage('network-only');
|
|
120
|
+
}
|
|
107
121
|
/**
|
|
108
122
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
109
123
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -137,6 +151,8 @@ class KoolbaseCollectionController {
|
|
|
137
151
|
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
138
152
|
dispose() {
|
|
139
153
|
this.disposed = true;
|
|
154
|
+
this.unregister?.();
|
|
155
|
+
this.unregister = null;
|
|
140
156
|
this.listeners.clear();
|
|
141
157
|
}
|
|
142
158
|
async firstPage(cache) {
|
package/dist/cjs/database.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.KoolbaseDatabase = void 0;
|
|
4
4
|
const network_js_1 = require("./network.js");
|
|
5
|
+
const query_refresh_js_1 = require("./query-refresh.js");
|
|
5
6
|
const errors_js_1 = require("./errors.js");
|
|
6
7
|
const cache_store_js_1 = require("./cache-store.js");
|
|
7
8
|
const offline_state_js_1 = require("./offline-state.js");
|
|
@@ -229,6 +230,7 @@ class KoolbaseDatabase {
|
|
|
229
230
|
*/
|
|
230
231
|
async invalidate(collection) {
|
|
231
232
|
await (0, cache_store_js_2.invalidateCache)(this.getUserId() ?? 'anonymous', collection);
|
|
233
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
232
234
|
}
|
|
233
235
|
// ─── Insert (online-first with offline fallback) ───────────────────────────
|
|
234
236
|
/**
|
|
@@ -252,6 +254,7 @@ class KoolbaseDatabase {
|
|
|
252
254
|
const raw = await this.request('POST', '/v1/sdk/db/insert', { collection, data });
|
|
253
255
|
const record = (0, record_js_1.recordFromWire)(raw);
|
|
254
256
|
await (0, cache_store_js_2.invalidateCache)(userId, collection);
|
|
257
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
255
258
|
// The response carries a fresh revision, so caching it keeps the
|
|
256
259
|
// baseline current for whatever edits this record next.
|
|
257
260
|
await (0, cache_store_js_1.cacheRecord)(userId, collection, record.id, record.data, record.revision);
|
|
@@ -326,6 +329,7 @@ class KoolbaseDatabase {
|
|
|
326
329
|
// Keep the cache fresh, same intent as insert's post-success invalidate.
|
|
327
330
|
const userId = this.getUserId() ?? 'anonymous';
|
|
328
331
|
await (0, cache_store_js_2.invalidateCache)(userId, collection);
|
|
332
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
329
333
|
await (0, cache_store_js_1.cacheRecord)(userId, collection, record.id, record.data, record.revision);
|
|
330
334
|
return { record, created };
|
|
331
335
|
}
|
|
@@ -345,6 +349,7 @@ class KoolbaseDatabase {
|
|
|
345
349
|
const body = await this.request('POST', '/v1/sdk/db/delete-where', { collection, filters });
|
|
346
350
|
const userId = this.getUserId() ?? 'anonymous';
|
|
347
351
|
await (0, cache_store_js_2.invalidateCache)(userId, collection);
|
|
352
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
348
353
|
return body.deleted ?? 0;
|
|
349
354
|
}
|
|
350
355
|
// ─── Batch (atomic, online-only) ────────────────────────────────────────────
|
|
@@ -408,6 +413,7 @@ class KoolbaseDatabase {
|
|
|
408
413
|
}
|
|
409
414
|
for (const col of touched) {
|
|
410
415
|
await (0, cache_store_js_2.invalidateCache)(userId, col);
|
|
416
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(col);
|
|
411
417
|
}
|
|
412
418
|
return results;
|
|
413
419
|
}
|
|
@@ -599,6 +605,7 @@ class KoolbaseDatabase {
|
|
|
599
605
|
}
|
|
600
606
|
await this.dropConflict(c.id);
|
|
601
607
|
await (0, cache_store_js_2.invalidateCache)(this.getUserId() ?? 'anonymous', c.collection);
|
|
608
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(c.collection);
|
|
602
609
|
}
|
|
603
610
|
// ─── Update (online-first with offline fallback) ───────────────────────────
|
|
604
611
|
/**
|
|
@@ -662,9 +669,13 @@ class KoolbaseDatabase {
|
|
|
662
669
|
try {
|
|
663
670
|
const raw = await this.request('PATCH', `/v1/sdk/db/records/${recordId}`, { data });
|
|
664
671
|
const updated = (0, record_js_1.recordFromWire)(raw);
|
|
672
|
+
// Its collection's live queries re-run (every one, if the record's
|
|
673
|
+
// collection is not known).
|
|
674
|
+
const refreshed = updated.collection;
|
|
665
675
|
if (updated.collection) {
|
|
666
676
|
await (0, cache_store_js_1.cacheRecord)(userId, updated.collection, updated.id, updated.data, updated.revision);
|
|
667
677
|
}
|
|
678
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(refreshed);
|
|
668
679
|
return updated;
|
|
669
680
|
}
|
|
670
681
|
catch (e) {
|
|
@@ -711,6 +722,9 @@ class KoolbaseDatabase {
|
|
|
711
722
|
try {
|
|
712
723
|
await this.request('DELETE', `/v1/sdk/db/records/${recordId}`);
|
|
713
724
|
await (0, cache_store_js_1.removeCachedRecord)(userId, recordId);
|
|
725
|
+
// Its collection is known from the baseline when the record was seen;
|
|
726
|
+
// otherwise every live query re-runs, rather than a guess.
|
|
727
|
+
await (0, query_refresh_js_1.refreshCollectionQueries)(base?.collection);
|
|
714
728
|
}
|
|
715
729
|
catch (e) {
|
|
716
730
|
// A server that answered has refused: a permission denial or a missing
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { KoolbaseAuth } from './auth.js';
|
|
|
12
12
|
export { KoolbaseDatabase } from './database.js';
|
|
13
13
|
export { KoolbaseCollectionController, collectionQueryKey, initialCollectionState, } from './collection-controller.js';
|
|
14
14
|
export type { CollectionQuery, CollectionState, CollectionStatus } from './collection-controller.js';
|
|
15
|
+
export { KoolbaseRecordController, initialRecordState } from './record-controller.js';
|
|
16
|
+
export type { RecordState, RecordStatus } from './record-controller.js';
|
|
15
17
|
export { KoolbaseFlags } from './flags.js';
|
|
16
18
|
export { KoolbaseFunctions } from './functions.js';
|
|
17
19
|
export { KoolbaseRealtime } from './realtime.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
19
19
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.memoryPlatform = exports.getPlatform = exports.setPlatform = exports.RestoreResult = exports.koolbaseSdkVersion = exports.getOrCreateDeviceId = exports.KoolbaseMessaging = exports.isPending = exports.isFiscalized = exports.decodeFiscalResponse = exports.KoolbaseFiscalError = exports.KoolbaseFiscal = exports.disabledAnalytics = exports.KoolbaseAnalytics = exports.KoolbaseStorage = exports.KoolbaseRealtime = exports.KoolbaseFunctions = exports.KoolbaseFlags = exports.initialCollectionState = exports.collectionQueryKey = exports.KoolbaseCollectionController = exports.KoolbaseDatabase = exports.KoolbaseAuth = void 0;
|
|
22
|
+
exports.memoryPlatform = exports.getPlatform = exports.setPlatform = exports.RestoreResult = exports.koolbaseSdkVersion = exports.getOrCreateDeviceId = exports.KoolbaseMessaging = exports.isPending = exports.isFiscalized = exports.decodeFiscalResponse = exports.KoolbaseFiscalError = exports.KoolbaseFiscal = exports.disabledAnalytics = exports.KoolbaseAnalytics = exports.KoolbaseStorage = exports.KoolbaseRealtime = exports.KoolbaseFunctions = exports.KoolbaseFlags = exports.initialRecordState = exports.KoolbaseRecordController = exports.initialCollectionState = exports.collectionQueryKey = exports.KoolbaseCollectionController = exports.KoolbaseDatabase = exports.KoolbaseAuth = void 0;
|
|
23
23
|
__exportStar(require("./types.js"), exports);
|
|
24
24
|
__exportStar(require("./errors.js"), exports);
|
|
25
25
|
__exportStar(require("./network.js"), exports);
|
|
@@ -38,6 +38,9 @@ var collection_controller_js_1 = require("./collection-controller.js");
|
|
|
38
38
|
Object.defineProperty(exports, "KoolbaseCollectionController", { enumerable: true, get: function () { return collection_controller_js_1.KoolbaseCollectionController; } });
|
|
39
39
|
Object.defineProperty(exports, "collectionQueryKey", { enumerable: true, get: function () { return collection_controller_js_1.collectionQueryKey; } });
|
|
40
40
|
Object.defineProperty(exports, "initialCollectionState", { enumerable: true, get: function () { return collection_controller_js_1.initialCollectionState; } });
|
|
41
|
+
var record_controller_js_1 = require("./record-controller.js");
|
|
42
|
+
Object.defineProperty(exports, "KoolbaseRecordController", { enumerable: true, get: function () { return record_controller_js_1.KoolbaseRecordController; } });
|
|
43
|
+
Object.defineProperty(exports, "initialRecordState", { enumerable: true, get: function () { return record_controller_js_1.initialRecordState; } });
|
|
41
44
|
var flags_js_1 = require("./flags.js");
|
|
42
45
|
Object.defineProperty(exports, "KoolbaseFlags", { enumerable: true, get: function () { return flags_js_1.KoolbaseFlags; } });
|
|
43
46
|
var functions_js_1 = require("./functions.js");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Registers one live query's refresher; returns the unregister. */
|
|
2
|
+
export declare function registerQueryRefresher(collection: string, refresh: () => Promise<void>): () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Re-runs every live query on a collection, after a write -- or every live
|
|
5
|
+
* query when the collection is not known (a delete of a record never read on
|
|
6
|
+
* this device). One after another; a failure in one never stops the others.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refreshCollectionQueries(collection?: string): Promise<void>;
|
|
9
|
+
/** Tests only. */
|
|
10
|
+
export declare function clearQueryRefreshers(): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Live queries refreshed after a write -- the Flutter SDK's _refreshers and
|
|
3
|
+
// refreshCollectionStreams, mirrored. A query refreshes ITSELF: a key carries
|
|
4
|
+
// the collection and filters but not the ordering, limit or paging, so a
|
|
5
|
+
// refresh rebuilt from a key alone would be a different query.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.registerQueryRefresher = registerQueryRefresher;
|
|
8
|
+
exports.refreshCollectionQueries = refreshCollectionQueries;
|
|
9
|
+
exports.clearQueryRefreshers = clearQueryRefreshers;
|
|
10
|
+
const refreshers = new Map();
|
|
11
|
+
let next = 0;
|
|
12
|
+
/** Registers one live query's refresher; returns the unregister. */
|
|
13
|
+
function registerQueryRefresher(collection, refresh) {
|
|
14
|
+
const key = `${collection}:${++next}`;
|
|
15
|
+
refreshers.set(key, refresh);
|
|
16
|
+
return () => {
|
|
17
|
+
refreshers.delete(key);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Re-runs every live query on a collection, after a write -- or every live
|
|
22
|
+
* query when the collection is not known (a delete of a record never read on
|
|
23
|
+
* this device). One after another; a failure in one never stops the others.
|
|
24
|
+
*/
|
|
25
|
+
async function refreshCollectionQueries(collection) {
|
|
26
|
+
const prefix = collection === undefined ? null : `${collection}:`;
|
|
27
|
+
const due = [...refreshers.entries()].filter(([k]) => prefix === null || k.startsWith(prefix)).map(([, f]) => f);
|
|
28
|
+
for (const refresh of due) {
|
|
29
|
+
try {
|
|
30
|
+
await refresh();
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.warn('[Koolbase] A live query failed to refresh after a write:', e);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Tests only. */
|
|
39
|
+
function clearQueryRefreshers() {
|
|
40
|
+
refreshers.clear();
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KoolbaseDatabase } from './database.js';
|
|
2
|
+
import type { KoolbaseRecord } from './types.js';
|
|
3
|
+
export type RecordStatus = 'loading' | 'loaded' | 'notFound' | 'error';
|
|
4
|
+
export interface RecordState {
|
|
5
|
+
readonly status: RecordStatus;
|
|
6
|
+
readonly record: KoolbaseRecord | null;
|
|
7
|
+
/** Why the first load failed, while status is 'error'. */
|
|
8
|
+
readonly error: unknown;
|
|
9
|
+
/** True while refresh() runs. */
|
|
10
|
+
readonly refreshing: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const initialRecordState: RecordState;
|
|
13
|
+
type RecordReader = Pick<KoolbaseDatabase, 'get'>;
|
|
14
|
+
export declare class KoolbaseRecordController {
|
|
15
|
+
readonly collection: string;
|
|
16
|
+
readonly id: string;
|
|
17
|
+
private readonly db;
|
|
18
|
+
private state;
|
|
19
|
+
private readonly listeners;
|
|
20
|
+
private generation;
|
|
21
|
+
private disposed;
|
|
22
|
+
constructor(db: RecordReader, collection: string, id: string);
|
|
23
|
+
getState(): RecordState;
|
|
24
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
25
|
+
subscribe(listener: () => void): () => void;
|
|
26
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
27
|
+
load(): Promise<void>;
|
|
28
|
+
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
29
|
+
refresh(): Promise<void>;
|
|
30
|
+
/** Stops all updates. A result still in flight is dropped when it lands. */
|
|
31
|
+
dispose(): void;
|
|
32
|
+
private fetch;
|
|
33
|
+
private isStale;
|
|
34
|
+
private set;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// One record, read by id, framework-free: the detail-screen counterpart of
|
|
3
|
+
// KoolbaseCollectionController. @koolbase/react-native's useRecord wraps it.
|
|
4
|
+
//
|
|
5
|
+
// - loading, loaded, notFound or error. notFound is its own state because
|
|
6
|
+
// the API answers a denied read as "not found" too (it does not reveal
|
|
7
|
+
// whether a record exists): a detail screen says "not available", not
|
|
8
|
+
// "something went wrong".
|
|
9
|
+
// - an empty id is notFound with no request: a screen opened without one.
|
|
10
|
+
// - a record from a different collection is notFound: the API names each
|
|
11
|
+
// record's collection, and a mistyped or crafted id must not put another
|
|
12
|
+
// collection's record on this screen.
|
|
13
|
+
// - refresh() keeps the record shown while it runs, and if it fails.
|
|
14
|
+
// - every fetch has a generation: a result superseded by a later refresh,
|
|
15
|
+
// or landing after dispose(), is dropped.
|
|
16
|
+
// Not yet: an offline fallback from the record cache, which keeps data and
|
|
17
|
+
// revision but not createdAt or createdBy.
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.KoolbaseRecordController = exports.initialRecordState = void 0;
|
|
20
|
+
const database_errors_js_1 = require("./database-errors.js");
|
|
21
|
+
exports.initialRecordState = Object.freeze({
|
|
22
|
+
status: 'loading',
|
|
23
|
+
record: null,
|
|
24
|
+
error: null,
|
|
25
|
+
refreshing: false,
|
|
26
|
+
});
|
|
27
|
+
class KoolbaseRecordController {
|
|
28
|
+
constructor(db, collection, id) {
|
|
29
|
+
this.state = exports.initialRecordState;
|
|
30
|
+
this.listeners = new Set();
|
|
31
|
+
this.generation = 0;
|
|
32
|
+
this.disposed = false;
|
|
33
|
+
this.db = db;
|
|
34
|
+
this.collection = collection;
|
|
35
|
+
this.id = id;
|
|
36
|
+
}
|
|
37
|
+
getState() {
|
|
38
|
+
return this.state;
|
|
39
|
+
}
|
|
40
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
41
|
+
subscribe(listener) {
|
|
42
|
+
if (this.disposed)
|
|
43
|
+
return () => { };
|
|
44
|
+
this.listeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
this.listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
50
|
+
async load() {
|
|
51
|
+
if (this.disposed)
|
|
52
|
+
return;
|
|
53
|
+
await this.fetch();
|
|
54
|
+
}
|
|
55
|
+
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
56
|
+
async refresh() {
|
|
57
|
+
if (this.disposed)
|
|
58
|
+
return;
|
|
59
|
+
this.set({ refreshing: true });
|
|
60
|
+
const gen = await this.fetch();
|
|
61
|
+
if (!this.isStale(gen))
|
|
62
|
+
this.set({ refreshing: false });
|
|
63
|
+
}
|
|
64
|
+
/** Stops all updates. A result still in flight is dropped when it lands. */
|
|
65
|
+
dispose() {
|
|
66
|
+
this.disposed = true;
|
|
67
|
+
this.listeners.clear();
|
|
68
|
+
}
|
|
69
|
+
async fetch() {
|
|
70
|
+
const gen = ++this.generation;
|
|
71
|
+
if (!this.id) {
|
|
72
|
+
this.set({ status: 'notFound', record: null, error: null });
|
|
73
|
+
return gen;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const record = await this.db.get(this.id);
|
|
77
|
+
if (this.isStale(gen))
|
|
78
|
+
return gen;
|
|
79
|
+
if (record.collection !== undefined && record.collection !== this.collection) {
|
|
80
|
+
this.set({ status: 'notFound', record: null, error: null });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.set({ status: 'loaded', record, error: null });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (this.isStale(gen))
|
|
88
|
+
return gen;
|
|
89
|
+
if (error instanceof database_errors_js_1.KoolbaseNotFoundError) {
|
|
90
|
+
this.set({ status: 'notFound', record: null, error: null });
|
|
91
|
+
}
|
|
92
|
+
else if (this.state.record === null) {
|
|
93
|
+
this.set({ status: 'error', error });
|
|
94
|
+
}
|
|
95
|
+
// Otherwise keep the record shown: stale beats blank.
|
|
96
|
+
}
|
|
97
|
+
return gen;
|
|
98
|
+
}
|
|
99
|
+
isStale(gen) {
|
|
100
|
+
return this.disposed || gen !== this.generation;
|
|
101
|
+
}
|
|
102
|
+
set(patch) {
|
|
103
|
+
if (this.disposed)
|
|
104
|
+
return;
|
|
105
|
+
this.state = Object.freeze({ ...this.state, ...patch });
|
|
106
|
+
for (const listener of [...this.listeners])
|
|
107
|
+
listener();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.KoolbaseRecordController = KoolbaseRecordController;
|
|
@@ -38,6 +38,7 @@ export declare class KoolbaseCollectionController {
|
|
|
38
38
|
private pageOne;
|
|
39
39
|
private laterPages;
|
|
40
40
|
private nextOffset;
|
|
41
|
+
private unregister;
|
|
41
42
|
constructor(db: QueryReader, collection: string, query?: CollectionQuery);
|
|
42
43
|
getState(): CollectionState;
|
|
43
44
|
/** Called on every change. Returns the unsubscribe. */
|
|
@@ -46,6 +47,12 @@ export declare class KoolbaseCollectionController {
|
|
|
46
47
|
load(): Promise<void>;
|
|
47
48
|
/** Page one again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
48
49
|
refresh(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* After a write to this collection: page one again from the server,
|
|
52
|
+
* SILENTLY -- no `refreshing`, which is the user's pull-to-refresh. What is
|
|
53
|
+
* shown stays while it runs, and stays if it fails.
|
|
54
|
+
*/
|
|
55
|
+
private refreshAfterWrite;
|
|
49
56
|
/**
|
|
50
57
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
51
58
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// Known limit: if rows are added or removed on the server between pages,
|
|
22
22
|
// the pages shift and a row can be skipped until refresh(), which reloads
|
|
23
23
|
// from page one. Duplicates are removed; a skipped row cannot be detected.
|
|
24
|
+
import { registerQueryRefresher } from './query-refresh.js';
|
|
24
25
|
export const initialCollectionState = Object.freeze({
|
|
25
26
|
status: 'loading',
|
|
26
27
|
records: Object.freeze([]),
|
|
@@ -69,6 +70,8 @@ export class KoolbaseCollectionController {
|
|
|
69
70
|
// Where the next page starts on the server. Not records.length: once rows
|
|
70
71
|
// are de-duplicated, a shifted page could otherwise be requested forever.
|
|
71
72
|
this.nextOffset = 0;
|
|
73
|
+
// A write to this collection re-runs this query (query-refresh.ts).
|
|
74
|
+
this.unregister = null;
|
|
72
75
|
this.db = db;
|
|
73
76
|
this.collection = collection;
|
|
74
77
|
this.spec = query;
|
|
@@ -89,6 +92,7 @@ export class KoolbaseCollectionController {
|
|
|
89
92
|
async load() {
|
|
90
93
|
if (this.disposed)
|
|
91
94
|
return;
|
|
95
|
+
this.unregister ?? (this.unregister = registerQueryRefresher(this.collection, () => this.refreshAfterWrite()));
|
|
92
96
|
await this.firstPage('default');
|
|
93
97
|
}
|
|
94
98
|
/** Page one again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
@@ -100,6 +104,16 @@ export class KoolbaseCollectionController {
|
|
|
100
104
|
if (!this.isStale(gen))
|
|
101
105
|
this.set({ refreshing: false });
|
|
102
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* After a write to this collection: page one again from the server,
|
|
109
|
+
* SILENTLY -- no `refreshing`, which is the user's pull-to-refresh. What is
|
|
110
|
+
* shown stays while it runs, and stays if it fails.
|
|
111
|
+
*/
|
|
112
|
+
async refreshAfterWrite() {
|
|
113
|
+
if (this.disposed)
|
|
114
|
+
return;
|
|
115
|
+
await this.firstPage('network-only');
|
|
116
|
+
}
|
|
103
117
|
/**
|
|
104
118
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
105
119
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -133,6 +147,8 @@ export class KoolbaseCollectionController {
|
|
|
133
147
|
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
134
148
|
dispose() {
|
|
135
149
|
this.disposed = true;
|
|
150
|
+
this.unregister?.();
|
|
151
|
+
this.unregister = null;
|
|
136
152
|
this.listeners.clear();
|
|
137
153
|
}
|
|
138
154
|
async firstPage(cache) {
|
package/dist/esm/database.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { koolbaseFetch } from './network.js';
|
|
2
|
+
import { refreshCollectionQueries } from './query-refresh.js';
|
|
2
3
|
import { KoolbaseError, KoolbaseOfflineBaselineUnavailableError, KoolbaseUnauthenticatedError, } from './errors.js';
|
|
3
4
|
import { cacheRecord, getCachedRecord, removeCachedRecord } from './cache-store.js';
|
|
4
5
|
import { readOfflineState, mutateOfflineState, queueWrite, } from './offline-state.js';
|
|
@@ -226,6 +227,7 @@ export class KoolbaseDatabase {
|
|
|
226
227
|
*/
|
|
227
228
|
async invalidate(collection) {
|
|
228
229
|
await invalidateCache(this.getUserId() ?? 'anonymous', collection);
|
|
230
|
+
await refreshCollectionQueries(collection);
|
|
229
231
|
}
|
|
230
232
|
// ─── Insert (online-first with offline fallback) ───────────────────────────
|
|
231
233
|
/**
|
|
@@ -249,6 +251,7 @@ export class KoolbaseDatabase {
|
|
|
249
251
|
const raw = await this.request('POST', '/v1/sdk/db/insert', { collection, data });
|
|
250
252
|
const record = recordFromWire(raw);
|
|
251
253
|
await invalidateCache(userId, collection);
|
|
254
|
+
await refreshCollectionQueries(collection);
|
|
252
255
|
// The response carries a fresh revision, so caching it keeps the
|
|
253
256
|
// baseline current for whatever edits this record next.
|
|
254
257
|
await cacheRecord(userId, collection, record.id, record.data, record.revision);
|
|
@@ -323,6 +326,7 @@ export class KoolbaseDatabase {
|
|
|
323
326
|
// Keep the cache fresh, same intent as insert's post-success invalidate.
|
|
324
327
|
const userId = this.getUserId() ?? 'anonymous';
|
|
325
328
|
await invalidateCache(userId, collection);
|
|
329
|
+
await refreshCollectionQueries(collection);
|
|
326
330
|
await cacheRecord(userId, collection, record.id, record.data, record.revision);
|
|
327
331
|
return { record, created };
|
|
328
332
|
}
|
|
@@ -342,6 +346,7 @@ export class KoolbaseDatabase {
|
|
|
342
346
|
const body = await this.request('POST', '/v1/sdk/db/delete-where', { collection, filters });
|
|
343
347
|
const userId = this.getUserId() ?? 'anonymous';
|
|
344
348
|
await invalidateCache(userId, collection);
|
|
349
|
+
await refreshCollectionQueries(collection);
|
|
345
350
|
return body.deleted ?? 0;
|
|
346
351
|
}
|
|
347
352
|
// ─── Batch (atomic, online-only) ────────────────────────────────────────────
|
|
@@ -405,6 +410,7 @@ export class KoolbaseDatabase {
|
|
|
405
410
|
}
|
|
406
411
|
for (const col of touched) {
|
|
407
412
|
await invalidateCache(userId, col);
|
|
413
|
+
await refreshCollectionQueries(col);
|
|
408
414
|
}
|
|
409
415
|
return results;
|
|
410
416
|
}
|
|
@@ -596,6 +602,7 @@ export class KoolbaseDatabase {
|
|
|
596
602
|
}
|
|
597
603
|
await this.dropConflict(c.id);
|
|
598
604
|
await invalidateCache(this.getUserId() ?? 'anonymous', c.collection);
|
|
605
|
+
await refreshCollectionQueries(c.collection);
|
|
599
606
|
}
|
|
600
607
|
// ─── Update (online-first with offline fallback) ───────────────────────────
|
|
601
608
|
/**
|
|
@@ -659,9 +666,13 @@ export class KoolbaseDatabase {
|
|
|
659
666
|
try {
|
|
660
667
|
const raw = await this.request('PATCH', `/v1/sdk/db/records/${recordId}`, { data });
|
|
661
668
|
const updated = recordFromWire(raw);
|
|
669
|
+
// Its collection's live queries re-run (every one, if the record's
|
|
670
|
+
// collection is not known).
|
|
671
|
+
const refreshed = updated.collection;
|
|
662
672
|
if (updated.collection) {
|
|
663
673
|
await cacheRecord(userId, updated.collection, updated.id, updated.data, updated.revision);
|
|
664
674
|
}
|
|
675
|
+
await refreshCollectionQueries(refreshed);
|
|
665
676
|
return updated;
|
|
666
677
|
}
|
|
667
678
|
catch (e) {
|
|
@@ -708,6 +719,9 @@ export class KoolbaseDatabase {
|
|
|
708
719
|
try {
|
|
709
720
|
await this.request('DELETE', `/v1/sdk/db/records/${recordId}`);
|
|
710
721
|
await removeCachedRecord(userId, recordId);
|
|
722
|
+
// Its collection is known from the baseline when the record was seen;
|
|
723
|
+
// otherwise every live query re-runs, rather than a guess.
|
|
724
|
+
await refreshCollectionQueries(base?.collection);
|
|
711
725
|
}
|
|
712
726
|
catch (e) {
|
|
713
727
|
// A server that answered has refused: a permission denial or a missing
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { KoolbaseAuth } from './auth.js';
|
|
|
12
12
|
export { KoolbaseDatabase } from './database.js';
|
|
13
13
|
export { KoolbaseCollectionController, collectionQueryKey, initialCollectionState, } from './collection-controller.js';
|
|
14
14
|
export type { CollectionQuery, CollectionState, CollectionStatus } from './collection-controller.js';
|
|
15
|
+
export { KoolbaseRecordController, initialRecordState } from './record-controller.js';
|
|
16
|
+
export type { RecordState, RecordStatus } from './record-controller.js';
|
|
15
17
|
export { KoolbaseFlags } from './flags.js';
|
|
16
18
|
export { KoolbaseFunctions } from './functions.js';
|
|
17
19
|
export { KoolbaseRealtime } from './realtime.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './storage-errors.js';
|
|
|
16
16
|
export { KoolbaseAuth } from './auth.js';
|
|
17
17
|
export { KoolbaseDatabase } from './database.js';
|
|
18
18
|
export { KoolbaseCollectionController, collectionQueryKey, initialCollectionState, } from './collection-controller.js';
|
|
19
|
+
export { KoolbaseRecordController, initialRecordState } from './record-controller.js';
|
|
19
20
|
export { KoolbaseFlags } from './flags.js';
|
|
20
21
|
export { KoolbaseFunctions } from './functions.js';
|
|
21
22
|
export { KoolbaseRealtime } from './realtime.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Registers one live query's refresher; returns the unregister. */
|
|
2
|
+
export declare function registerQueryRefresher(collection: string, refresh: () => Promise<void>): () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Re-runs every live query on a collection, after a write -- or every live
|
|
5
|
+
* query when the collection is not known (a delete of a record never read on
|
|
6
|
+
* this device). One after another; a failure in one never stops the others.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refreshCollectionQueries(collection?: string): Promise<void>;
|
|
9
|
+
/** Tests only. */
|
|
10
|
+
export declare function clearQueryRefreshers(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Live queries refreshed after a write -- the Flutter SDK's _refreshers and
|
|
2
|
+
// refreshCollectionStreams, mirrored. A query refreshes ITSELF: a key carries
|
|
3
|
+
// the collection and filters but not the ordering, limit or paging, so a
|
|
4
|
+
// refresh rebuilt from a key alone would be a different query.
|
|
5
|
+
const refreshers = new Map();
|
|
6
|
+
let next = 0;
|
|
7
|
+
/** Registers one live query's refresher; returns the unregister. */
|
|
8
|
+
export function registerQueryRefresher(collection, refresh) {
|
|
9
|
+
const key = `${collection}:${++next}`;
|
|
10
|
+
refreshers.set(key, refresh);
|
|
11
|
+
return () => {
|
|
12
|
+
refreshers.delete(key);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Re-runs every live query on a collection, after a write -- or every live
|
|
17
|
+
* query when the collection is not known (a delete of a record never read on
|
|
18
|
+
* this device). One after another; a failure in one never stops the others.
|
|
19
|
+
*/
|
|
20
|
+
export async function refreshCollectionQueries(collection) {
|
|
21
|
+
const prefix = collection === undefined ? null : `${collection}:`;
|
|
22
|
+
const due = [...refreshers.entries()].filter(([k]) => prefix === null || k.startsWith(prefix)).map(([, f]) => f);
|
|
23
|
+
for (const refresh of due) {
|
|
24
|
+
try {
|
|
25
|
+
await refresh();
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.warn('[Koolbase] A live query failed to refresh after a write:', e);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Tests only. */
|
|
34
|
+
export function clearQueryRefreshers() {
|
|
35
|
+
refreshers.clear();
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KoolbaseDatabase } from './database.js';
|
|
2
|
+
import type { KoolbaseRecord } from './types.js';
|
|
3
|
+
export type RecordStatus = 'loading' | 'loaded' | 'notFound' | 'error';
|
|
4
|
+
export interface RecordState {
|
|
5
|
+
readonly status: RecordStatus;
|
|
6
|
+
readonly record: KoolbaseRecord | null;
|
|
7
|
+
/** Why the first load failed, while status is 'error'. */
|
|
8
|
+
readonly error: unknown;
|
|
9
|
+
/** True while refresh() runs. */
|
|
10
|
+
readonly refreshing: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const initialRecordState: RecordState;
|
|
13
|
+
type RecordReader = Pick<KoolbaseDatabase, 'get'>;
|
|
14
|
+
export declare class KoolbaseRecordController {
|
|
15
|
+
readonly collection: string;
|
|
16
|
+
readonly id: string;
|
|
17
|
+
private readonly db;
|
|
18
|
+
private state;
|
|
19
|
+
private readonly listeners;
|
|
20
|
+
private generation;
|
|
21
|
+
private disposed;
|
|
22
|
+
constructor(db: RecordReader, collection: string, id: string);
|
|
23
|
+
getState(): RecordState;
|
|
24
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
25
|
+
subscribe(listener: () => void): () => void;
|
|
26
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
27
|
+
load(): Promise<void>;
|
|
28
|
+
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
29
|
+
refresh(): Promise<void>;
|
|
30
|
+
/** Stops all updates. A result still in flight is dropped when it lands. */
|
|
31
|
+
dispose(): void;
|
|
32
|
+
private fetch;
|
|
33
|
+
private isStale;
|
|
34
|
+
private set;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// One record, read by id, framework-free: the detail-screen counterpart of
|
|
2
|
+
// KoolbaseCollectionController. @koolbase/react-native's useRecord wraps it.
|
|
3
|
+
//
|
|
4
|
+
// - loading, loaded, notFound or error. notFound is its own state because
|
|
5
|
+
// the API answers a denied read as "not found" too (it does not reveal
|
|
6
|
+
// whether a record exists): a detail screen says "not available", not
|
|
7
|
+
// "something went wrong".
|
|
8
|
+
// - an empty id is notFound with no request: a screen opened without one.
|
|
9
|
+
// - a record from a different collection is notFound: the API names each
|
|
10
|
+
// record's collection, and a mistyped or crafted id must not put another
|
|
11
|
+
// collection's record on this screen.
|
|
12
|
+
// - refresh() keeps the record shown while it runs, and if it fails.
|
|
13
|
+
// - every fetch has a generation: a result superseded by a later refresh,
|
|
14
|
+
// or landing after dispose(), is dropped.
|
|
15
|
+
// Not yet: an offline fallback from the record cache, which keeps data and
|
|
16
|
+
// revision but not createdAt or createdBy.
|
|
17
|
+
import { KoolbaseNotFoundError } from './database-errors.js';
|
|
18
|
+
export const initialRecordState = Object.freeze({
|
|
19
|
+
status: 'loading',
|
|
20
|
+
record: null,
|
|
21
|
+
error: null,
|
|
22
|
+
refreshing: false,
|
|
23
|
+
});
|
|
24
|
+
export class KoolbaseRecordController {
|
|
25
|
+
constructor(db, collection, id) {
|
|
26
|
+
this.state = initialRecordState;
|
|
27
|
+
this.listeners = new Set();
|
|
28
|
+
this.generation = 0;
|
|
29
|
+
this.disposed = false;
|
|
30
|
+
this.db = db;
|
|
31
|
+
this.collection = collection;
|
|
32
|
+
this.id = id;
|
|
33
|
+
}
|
|
34
|
+
getState() {
|
|
35
|
+
return this.state;
|
|
36
|
+
}
|
|
37
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
38
|
+
subscribe(listener) {
|
|
39
|
+
if (this.disposed)
|
|
40
|
+
return () => { };
|
|
41
|
+
this.listeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
this.listeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
47
|
+
async load() {
|
|
48
|
+
if (this.disposed)
|
|
49
|
+
return;
|
|
50
|
+
await this.fetch();
|
|
51
|
+
}
|
|
52
|
+
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
53
|
+
async refresh() {
|
|
54
|
+
if (this.disposed)
|
|
55
|
+
return;
|
|
56
|
+
this.set({ refreshing: true });
|
|
57
|
+
const gen = await this.fetch();
|
|
58
|
+
if (!this.isStale(gen))
|
|
59
|
+
this.set({ refreshing: false });
|
|
60
|
+
}
|
|
61
|
+
/** Stops all updates. A result still in flight is dropped when it lands. */
|
|
62
|
+
dispose() {
|
|
63
|
+
this.disposed = true;
|
|
64
|
+
this.listeners.clear();
|
|
65
|
+
}
|
|
66
|
+
async fetch() {
|
|
67
|
+
const gen = ++this.generation;
|
|
68
|
+
if (!this.id) {
|
|
69
|
+
this.set({ status: 'notFound', record: null, error: null });
|
|
70
|
+
return gen;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const record = await this.db.get(this.id);
|
|
74
|
+
if (this.isStale(gen))
|
|
75
|
+
return gen;
|
|
76
|
+
if (record.collection !== undefined && record.collection !== this.collection) {
|
|
77
|
+
this.set({ status: 'notFound', record: null, error: null });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.set({ status: 'loaded', record, error: null });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (this.isStale(gen))
|
|
85
|
+
return gen;
|
|
86
|
+
if (error instanceof KoolbaseNotFoundError) {
|
|
87
|
+
this.set({ status: 'notFound', record: null, error: null });
|
|
88
|
+
}
|
|
89
|
+
else if (this.state.record === null) {
|
|
90
|
+
this.set({ status: 'error', error });
|
|
91
|
+
}
|
|
92
|
+
// Otherwise keep the record shown: stale beats blank.
|
|
93
|
+
}
|
|
94
|
+
return gen;
|
|
95
|
+
}
|
|
96
|
+
isStale(gen) {
|
|
97
|
+
return this.disposed || gen !== this.generation;
|
|
98
|
+
}
|
|
99
|
+
set(patch) {
|
|
100
|
+
if (this.disposed)
|
|
101
|
+
return;
|
|
102
|
+
this.state = Object.freeze({ ...this.state, ...patch });
|
|
103
|
+
for (const listener of [...this.listeners])
|
|
104
|
+
listener();
|
|
105
|
+
}
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koolbase/core",
|
|
3
|
-
"version": "12.
|
|
4
|
-
"description": "Koolbase SDK core
|
|
3
|
+
"version": "12.7.0",
|
|
4
|
+
"description": "Koolbase SDK core — shared behaviour behind @koolbase/react-native and @koolbase/js. Install one of those, not this.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"types": "./dist/esm/index.d.ts",
|
|
7
7
|
"files": [
|