@koolbase/core 12.13.0 → 12.14.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/cache-store.d.ts +14 -0
- package/dist/cjs/cache-store.js +64 -0
- package/dist/cjs/connectivity.d.ts +25 -0
- package/dist/cjs/connectivity.js +59 -0
- package/dist/cjs/database.d.ts +41 -0
- package/dist/cjs/database.js +140 -20
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/offline-state.d.ts +20 -0
- package/dist/cjs/offline-state.js +70 -0
- package/dist/cjs/platform.d.ts +16 -0
- package/dist/cjs/react/use-record.d.ts +1 -1
- package/dist/cjs/record-controller.d.ts +8 -1
- package/dist/cjs/record-controller.js +29 -7
- package/dist/cjs/shared.d.ts +1 -0
- package/dist/cjs/sync-engine.js +18 -5
- package/dist/esm/cache-store.d.ts +14 -0
- package/dist/esm/cache-store.js +62 -0
- package/dist/esm/connectivity.d.ts +25 -0
- package/dist/esm/connectivity.js +55 -0
- package/dist/esm/database.d.ts +41 -0
- package/dist/esm/database.js +129 -9
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/offline-state.d.ts +20 -0
- package/dist/esm/offline-state.js +68 -0
- package/dist/esm/platform.d.ts +16 -0
- package/dist/esm/react/use-record.d.ts +1 -1
- package/dist/esm/record-controller.d.ts +8 -1
- package/dist/esm/record-controller.js +29 -7
- package/dist/esm/shared.d.ts +1 -0
- package/dist/esm/sync-engine.js +19 -6
- package/package.json +1 -1
|
@@ -58,3 +58,17 @@ export declare function addToWriteQueue(userId: string, write: Omit<LegacyPendin
|
|
|
58
58
|
export declare function removeFromWriteQueue(userId: string, writeId: string): Promise<void>;
|
|
59
59
|
export declare function incrementWriteRetry(userId: string, writeId: string): Promise<void>;
|
|
60
60
|
export declare function optimisticallyInsert(userId: string, collection: string, record: KoolbaseRecord): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* The reverse of optimisticallyInsert: takes one record out of every cached
|
|
63
|
+
* result of a collection.
|
|
64
|
+
*
|
|
65
|
+
* Precise on purpose. Dropping the collection's cached results instead would
|
|
66
|
+
* leave a device that is still offline with no saved lists to show at all,
|
|
67
|
+
* to get rid of one record.
|
|
68
|
+
*/
|
|
69
|
+
export declare function removeFromCachedQueries(userId: string, collection: string, recordId: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Applies an offline edit to the record wherever a cached result holds it, so
|
|
72
|
+
* a saved list shows what the user just changed rather than what it was.
|
|
73
|
+
*/
|
|
74
|
+
export declare function updateInCachedQueries(userId: string, collection: string, recordId: string, data: Record<string, unknown>): Promise<void>;
|
package/dist/cjs/cache-store.js
CHANGED
|
@@ -13,6 +13,8 @@ exports.addToWriteQueue = addToWriteQueue;
|
|
|
13
13
|
exports.removeFromWriteQueue = removeFromWriteQueue;
|
|
14
14
|
exports.incrementWriteRetry = incrementWriteRetry;
|
|
15
15
|
exports.optimisticallyInsert = optimisticallyInsert;
|
|
16
|
+
exports.removeFromCachedQueries = removeFromCachedQueries;
|
|
17
|
+
exports.updateInCachedQueries = updateInCachedQueries;
|
|
16
18
|
const platform_js_1 = require("./platform.js");
|
|
17
19
|
const CACHE_VERSION = 'v1';
|
|
18
20
|
function cacheKey(userId, collection, queryHash) {
|
|
@@ -211,3 +213,65 @@ async function optimisticallyInsert(userId, collection, record) {
|
|
|
211
213
|
// ignore
|
|
212
214
|
}
|
|
213
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* The reverse of optimisticallyInsert: takes one record out of every cached
|
|
218
|
+
* result of a collection.
|
|
219
|
+
*
|
|
220
|
+
* Precise on purpose. Dropping the collection's cached results instead would
|
|
221
|
+
* leave a device that is still offline with no saved lists to show at all,
|
|
222
|
+
* to get rid of one record.
|
|
223
|
+
*/
|
|
224
|
+
async function removeFromCachedQueries(userId, collection, recordId) {
|
|
225
|
+
try {
|
|
226
|
+
const keys = await (0, platform_js_1.getPlatform)().storage.getAllKeys();
|
|
227
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:${collection}:`;
|
|
228
|
+
for (const key of keys.filter(k => k.startsWith(prefix))) {
|
|
229
|
+
const raw = await (0, platform_js_1.getPlatform)().storage.getItem(key);
|
|
230
|
+
if (!raw)
|
|
231
|
+
continue;
|
|
232
|
+
const cached = JSON.parse(raw);
|
|
233
|
+
if (!Array.isArray(cached.records))
|
|
234
|
+
continue;
|
|
235
|
+
const kept = cached.records.filter(r => r.id !== recordId);
|
|
236
|
+
const removed = cached.records.length - kept.length;
|
|
237
|
+
if (removed === 0)
|
|
238
|
+
continue;
|
|
239
|
+
cached.records = kept;
|
|
240
|
+
cached.total = Math.max(0, cached.total - removed);
|
|
241
|
+
await (0, platform_js_1.getPlatform)().storage.setItem(key, JSON.stringify(cached));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// ignore
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Applies an offline edit to the record wherever a cached result holds it, so
|
|
250
|
+
* a saved list shows what the user just changed rather than what it was.
|
|
251
|
+
*/
|
|
252
|
+
async function updateInCachedQueries(userId, collection, recordId, data) {
|
|
253
|
+
try {
|
|
254
|
+
const keys = await (0, platform_js_1.getPlatform)().storage.getAllKeys();
|
|
255
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:${collection}:`;
|
|
256
|
+
for (const key of keys.filter(k => k.startsWith(prefix))) {
|
|
257
|
+
const raw = await (0, platform_js_1.getPlatform)().storage.getItem(key);
|
|
258
|
+
if (!raw)
|
|
259
|
+
continue;
|
|
260
|
+
const cached = JSON.parse(raw);
|
|
261
|
+
if (!Array.isArray(cached.records))
|
|
262
|
+
continue;
|
|
263
|
+
let changed = false;
|
|
264
|
+
cached.records = cached.records.map(r => {
|
|
265
|
+
if (r.id !== recordId)
|
|
266
|
+
return r;
|
|
267
|
+
changed = true;
|
|
268
|
+
return { ...r, data: { ...(r.data ?? {}), ...data } };
|
|
269
|
+
});
|
|
270
|
+
if (changed)
|
|
271
|
+
await (0, platform_js_1.getPlatform)().storage.setItem(key, JSON.stringify(cached));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// ignore
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the device reports a network connection. A hint, not a promise that
|
|
3
|
+
* Koolbase is reachable: requests can still fail while online.
|
|
4
|
+
*
|
|
5
|
+
* 'unknown' until the platform first answers -- a real state, not a guess. An
|
|
6
|
+
* app should neither show "offline" nor run online-only logic on it.
|
|
7
|
+
*/
|
|
8
|
+
export type ConnectivityState = 'unknown' | 'online' | 'offline';
|
|
9
|
+
/**
|
|
10
|
+
* The device's connectivity, as state an app can render: getState() for now,
|
|
11
|
+
* subscribe() for changes (the shape React's useSyncExternalStore takes).
|
|
12
|
+
*/
|
|
13
|
+
export declare class KoolbaseConnectivity {
|
|
14
|
+
private state;
|
|
15
|
+
private readonly listeners;
|
|
16
|
+
private stop;
|
|
17
|
+
private heard;
|
|
18
|
+
/** Starts following the platform. Called once by Koolbase.initialize. */
|
|
19
|
+
start(): void;
|
|
20
|
+
getState(): ConnectivityState;
|
|
21
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
22
|
+
subscribe(listener: () => void): () => void;
|
|
23
|
+
dispose(): void;
|
|
24
|
+
private set;
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KoolbaseConnectivity = void 0;
|
|
4
|
+
const platform_js_1 = require("./platform.js");
|
|
5
|
+
/**
|
|
6
|
+
* The device's connectivity, as state an app can render: getState() for now,
|
|
7
|
+
* subscribe() for changes (the shape React's useSyncExternalStore takes).
|
|
8
|
+
*/
|
|
9
|
+
class KoolbaseConnectivity {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.state = 'unknown';
|
|
12
|
+
this.listeners = new Set();
|
|
13
|
+
this.stop = null;
|
|
14
|
+
// A change event is fresher than the first read: once one arrives, a late
|
|
15
|
+
// answer to that read is ignored.
|
|
16
|
+
this.heard = false;
|
|
17
|
+
}
|
|
18
|
+
/** Starts following the platform. Called once by Koolbase.initialize. */
|
|
19
|
+
start() {
|
|
20
|
+
if (this.stop)
|
|
21
|
+
return;
|
|
22
|
+
const network = (0, platform_js_1.getPlatform)().network;
|
|
23
|
+
this.stop = network.onChange((online) => {
|
|
24
|
+
this.heard = true;
|
|
25
|
+
this.set(online ? 'online' : 'offline');
|
|
26
|
+
});
|
|
27
|
+
if (network.current) {
|
|
28
|
+
network.current().then((online) => {
|
|
29
|
+
if (!this.heard && online !== null && online !== undefined)
|
|
30
|
+
this.set(online ? 'online' : 'offline');
|
|
31
|
+
}, () => { });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
getState() {
|
|
35
|
+
return this.state;
|
|
36
|
+
}
|
|
37
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
38
|
+
subscribe(listener) {
|
|
39
|
+
this.listeners.add(listener);
|
|
40
|
+
return () => { this.listeners.delete(listener); };
|
|
41
|
+
}
|
|
42
|
+
dispose() {
|
|
43
|
+
this.stop?.();
|
|
44
|
+
this.stop = null;
|
|
45
|
+
this.listeners.clear();
|
|
46
|
+
}
|
|
47
|
+
set(next) {
|
|
48
|
+
if (next === this.state)
|
|
49
|
+
return;
|
|
50
|
+
this.state = next;
|
|
51
|
+
for (const listener of [...this.listeners]) {
|
|
52
|
+
try {
|
|
53
|
+
listener();
|
|
54
|
+
}
|
|
55
|
+
catch { /* one broken listener must not stop the rest */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.KoolbaseConnectivity = KoolbaseConnectivity;
|
package/dist/cjs/database.d.ts
CHANGED
|
@@ -119,6 +119,16 @@ export declare class KoolbaseDatabase {
|
|
|
119
119
|
*/
|
|
120
120
|
batch(operations: BatchOp[]): Promise<BatchResult[]>;
|
|
121
121
|
get(recordId: string): Promise<KoolbaseRecord>;
|
|
122
|
+
/**
|
|
123
|
+
* The device's saved copy of a record: what this device last read, with the
|
|
124
|
+
* changes queued on it applied. Never contacts the server. Null when there is
|
|
125
|
+
* none: never read here, or deleted on this device.
|
|
126
|
+
*
|
|
127
|
+
* For showing something at once while get() asks the server -- saved-first,
|
|
128
|
+
* never saved-only. Kept data and revision, not createdAt or createdBy, which
|
|
129
|
+
* the saved copy does not hold (empty strings here).
|
|
130
|
+
*/
|
|
131
|
+
getSaved(recordId: string): Promise<KoolbaseRecord | null>;
|
|
122
132
|
/**
|
|
123
133
|
* Writes that could not be applied, waiting for a decision.
|
|
124
134
|
*
|
|
@@ -134,6 +144,28 @@ export declare class KoolbaseDatabase {
|
|
|
134
144
|
* moment matters. Snapshot, not a live handle; per-user.
|
|
135
145
|
*/
|
|
136
146
|
pendingWrites(): Promise<PendingWrite[]>;
|
|
147
|
+
/**
|
|
148
|
+
* The pending writes now, and again whenever they change -- here, in another
|
|
149
|
+
* tab, or because the signed-in user changed. For a sync badge that reflects
|
|
150
|
+
* reality. null while signed out: per-user state has no answer then, and a
|
|
151
|
+
* fake empty list is how a badge says "all synced" while a user's changes
|
|
152
|
+
* sit queued. Returns the unsubscribe.
|
|
153
|
+
*/
|
|
154
|
+
watchPendingWrites(listener: (writes: PendingWrite[] | null) => void): () => void;
|
|
155
|
+
/** The unresolved conflicts now, and again whenever they change. null while signed out. */
|
|
156
|
+
watchConflicts(listener: (conflicts: KoolbaseConflict[] | null) => void): () => void;
|
|
157
|
+
/**
|
|
158
|
+
* The signed-in user changed: every watcher reads again, for the new user
|
|
159
|
+
* (or null, signed out). Koolbase.initialize wires it to auth changes.
|
|
160
|
+
*/
|
|
161
|
+
sessionChanged(): void;
|
|
162
|
+
private readonly stateWatchers;
|
|
163
|
+
private offStateChange;
|
|
164
|
+
/**
|
|
165
|
+
* One watcher: reads at once, then on every change to the signed-in user's
|
|
166
|
+
* offline state. Reads can finish out of order; only the latest delivers.
|
|
167
|
+
*/
|
|
168
|
+
private watchState;
|
|
137
169
|
/**
|
|
138
170
|
* Count and total over a whole collection, with the read rule applied
|
|
139
171
|
* inside the query.
|
|
@@ -173,6 +205,15 @@ export declare class KoolbaseDatabase {
|
|
|
173
205
|
*/
|
|
174
206
|
private requireUserId;
|
|
175
207
|
private requireConflict;
|
|
208
|
+
/**
|
|
209
|
+
* Removes a decided conflict and points the writes held behind it at the
|
|
210
|
+
* state the record is now in -- in one transaction, so the conflict can
|
|
211
|
+
* never be gone while its held writes still describe the old world.
|
|
212
|
+
*
|
|
213
|
+
* Without a known state (a refused add, a delete) there is nothing to
|
|
214
|
+
* rebase onto: the conflict is removed and the held writes stay as they are.
|
|
215
|
+
*/
|
|
216
|
+
private settleConflict;
|
|
176
217
|
private dropConflict;
|
|
177
218
|
/**
|
|
178
219
|
* Issues the resolving write, conditional on the revision the refusal
|
package/dist/cjs/database.js
CHANGED
|
@@ -7,9 +7,10 @@ const database_errors_js_1 = require("./database-errors.js");
|
|
|
7
7
|
const errors_js_1 = require("./errors.js");
|
|
8
8
|
const cache_store_js_1 = require("./cache-store.js");
|
|
9
9
|
const offline_state_js_1 = require("./offline-state.js");
|
|
10
|
+
const cache_store_js_2 = require("./cache-store.js");
|
|
10
11
|
const conflict_js_1 = require("./conflict.js");
|
|
11
12
|
const pending_write_js_1 = require("./pending-write.js");
|
|
12
|
-
const
|
|
13
|
+
const cache_store_js_3 = require("./cache-store.js");
|
|
13
14
|
const sync_engine_js_1 = require("./sync-engine.js");
|
|
14
15
|
const record_js_1 = require("./record.js");
|
|
15
16
|
const database_errors_js_2 = require("./database-errors.js");
|
|
@@ -45,6 +46,8 @@ function batchOpToWire(op) {
|
|
|
45
46
|
}
|
|
46
47
|
class KoolbaseDatabase {
|
|
47
48
|
constructor(config, getUserId, getToken, onSessionExpired) {
|
|
49
|
+
this.stateWatchers = new Set();
|
|
50
|
+
this.offStateChange = null;
|
|
48
51
|
/**
|
|
49
52
|
* Resolves by id, reloading the stored conflict first.
|
|
50
53
|
*
|
|
@@ -65,12 +68,15 @@ class KoolbaseDatabase {
|
|
|
65
68
|
resolveWithServer: async (id) => {
|
|
66
69
|
const c = await this.requireConflict(id);
|
|
67
70
|
// The server's version stands. Recorded as a decision by removing the
|
|
68
|
-
// conflict, rather than the change quietly disappearing
|
|
69
|
-
|
|
71
|
+
// conflict, rather than the change quietly disappearing -- and what
|
|
72
|
+
// stands is what the writes held behind it now build on.
|
|
73
|
+
await this.settleConflict(c, c.server, c.serverRevision);
|
|
70
74
|
},
|
|
71
75
|
abandon: async (id) => {
|
|
72
76
|
const c = await this.requireConflict(id);
|
|
73
|
-
|
|
77
|
+
// Neither version claimed to win, so the record stands as the server
|
|
78
|
+
// has it: what anything still queued must build on.
|
|
79
|
+
await this.settleConflict(c, c.server, c.serverRevision);
|
|
74
80
|
},
|
|
75
81
|
};
|
|
76
82
|
this.config = config;
|
|
@@ -204,12 +210,12 @@ class KoolbaseDatabase {
|
|
|
204
210
|
// query's identity, so both policies share one cache entry. onRefresh is
|
|
205
211
|
// who to tell, not what to read, so it stays out the same way.
|
|
206
212
|
const { cache: cachePolicy, onRefresh, ...queryOptions } = options;
|
|
207
|
-
const queryHash = (0,
|
|
208
|
-
const cached = cachePolicy === 'network-only' ? null : await (0,
|
|
213
|
+
const queryHash = (0, cache_store_js_3.hashQuery)(collection, queryOptions);
|
|
214
|
+
const cached = cachePolicy === 'network-only' ? null : await (0, cache_store_js_3.getCached)(userId, collection, queryHash);
|
|
209
215
|
if (cached) {
|
|
210
216
|
this.runQuery(collection, queryOptions)
|
|
211
217
|
.then(async (result) => {
|
|
212
|
-
await (0,
|
|
218
|
+
await (0, cache_store_js_3.setCached)(userId, collection, queryHash, result);
|
|
213
219
|
// Told outside the catch below, which is for the network: an
|
|
214
220
|
// exception from the caller's callback is the caller's to see.
|
|
215
221
|
if (onRefresh)
|
|
@@ -221,7 +227,7 @@ class KoolbaseDatabase {
|
|
|
221
227
|
return { ...cached, isFromCache: true };
|
|
222
228
|
}
|
|
223
229
|
const result = await this.runQuery(collection, queryOptions);
|
|
224
|
-
await (0,
|
|
230
|
+
await (0, cache_store_js_3.setCached)(userId, collection, queryHash, result);
|
|
225
231
|
return { ...result, isFromCache: false };
|
|
226
232
|
}
|
|
227
233
|
/**
|
|
@@ -230,7 +236,7 @@ class KoolbaseDatabase {
|
|
|
230
236
|
* a server, or another user. The SDK's own writes already do this.
|
|
231
237
|
*/
|
|
232
238
|
async invalidate(collection) {
|
|
233
|
-
await (0,
|
|
239
|
+
await (0, cache_store_js_3.invalidateCache)(this.getUserId() ?? 'anonymous', collection);
|
|
234
240
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
235
241
|
}
|
|
236
242
|
// ─── Insert (online-first with offline fallback) ───────────────────────────
|
|
@@ -254,7 +260,7 @@ class KoolbaseDatabase {
|
|
|
254
260
|
// next query sees real data instead of a stale optimistic copy.
|
|
255
261
|
const raw = await this.request('POST', '/v1/sdk/db/insert', { collection, data });
|
|
256
262
|
const record = (0, record_js_1.recordFromWire)(raw);
|
|
257
|
-
await (0,
|
|
263
|
+
await (0, cache_store_js_3.invalidateCache)(userId, collection);
|
|
258
264
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
259
265
|
// The response carries a fresh revision, so caching it keeps the
|
|
260
266
|
// baseline current for whatever edits this record next.
|
|
@@ -290,7 +296,7 @@ class KoolbaseDatabase {
|
|
|
290
296
|
createdAt: new Date().toISOString(),
|
|
291
297
|
updatedAt: new Date().toISOString(),
|
|
292
298
|
};
|
|
293
|
-
await (0,
|
|
299
|
+
await (0, cache_store_js_3.optimisticallyInsert)(userId, collection, optimisticRecord);
|
|
294
300
|
// No baseline: an insert has no prior state, and the record does not
|
|
295
301
|
// exist on the server yet, so there is nothing to be conditional against.
|
|
296
302
|
// An offline edit to it composes against this queued write instead.
|
|
@@ -329,7 +335,7 @@ class KoolbaseDatabase {
|
|
|
329
335
|
const record = (0, record_js_1.recordFromWire)(body);
|
|
330
336
|
// Keep the cache fresh, same intent as insert's post-success invalidate.
|
|
331
337
|
const userId = this.getUserId() ?? 'anonymous';
|
|
332
|
-
await (0,
|
|
338
|
+
await (0, cache_store_js_3.invalidateCache)(userId, collection);
|
|
333
339
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
334
340
|
await (0, cache_store_js_1.cacheRecord)(userId, collection, record.id, record.data, record.revision);
|
|
335
341
|
return { record, created };
|
|
@@ -349,7 +355,7 @@ class KoolbaseDatabase {
|
|
|
349
355
|
async deleteWhere(collection, filters) {
|
|
350
356
|
const body = await this.request('POST', '/v1/sdk/db/delete-where', { collection, filters });
|
|
351
357
|
const userId = this.getUserId() ?? 'anonymous';
|
|
352
|
-
await (0,
|
|
358
|
+
await (0, cache_store_js_3.invalidateCache)(userId, collection);
|
|
353
359
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
354
360
|
return body.deleted ?? 0;
|
|
355
361
|
}
|
|
@@ -413,7 +419,7 @@ class KoolbaseDatabase {
|
|
|
413
419
|
}
|
|
414
420
|
}
|
|
415
421
|
for (const col of touched) {
|
|
416
|
-
await (0,
|
|
422
|
+
await (0, cache_store_js_3.invalidateCache)(userId, col);
|
|
417
423
|
void (0, query_refresh_js_1.refreshCollectionQueries)(col);
|
|
418
424
|
}
|
|
419
425
|
return results;
|
|
@@ -430,6 +436,29 @@ class KoolbaseDatabase {
|
|
|
430
436
|
}
|
|
431
437
|
return record;
|
|
432
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* The device's saved copy of a record: what this device last read, with the
|
|
441
|
+
* changes queued on it applied. Never contacts the server. Null when there is
|
|
442
|
+
* none: never read here, or deleted on this device.
|
|
443
|
+
*
|
|
444
|
+
* For showing something at once while get() asks the server -- saved-first,
|
|
445
|
+
* never saved-only. Kept data and revision, not createdAt or createdBy, which
|
|
446
|
+
* the saved copy does not hold (empty strings here).
|
|
447
|
+
*/
|
|
448
|
+
async getSaved(recordId) {
|
|
449
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
450
|
+
const base = await this.resolveBaseline(userId, recordId);
|
|
451
|
+
if (!base)
|
|
452
|
+
return null;
|
|
453
|
+
return {
|
|
454
|
+
id: recordId,
|
|
455
|
+
collection: base.collection,
|
|
456
|
+
data: { ...base.baseline },
|
|
457
|
+
createdAt: '',
|
|
458
|
+
updatedAt: '',
|
|
459
|
+
revision: base.revision,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
433
462
|
// ─── Conflicts ──────────────────────────────────────────────────────────────
|
|
434
463
|
/**
|
|
435
464
|
* Writes that could not be applied, waiting for a decision.
|
|
@@ -450,6 +479,69 @@ class KoolbaseDatabase {
|
|
|
450
479
|
const { pending } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
451
480
|
return pending.map(pending_write_js_1.toPendingWrite);
|
|
452
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* The pending writes now, and again whenever they change -- here, in another
|
|
484
|
+
* tab, or because the signed-in user changed. For a sync badge that reflects
|
|
485
|
+
* reality. null while signed out: per-user state has no answer then, and a
|
|
486
|
+
* fake empty list is how a badge says "all synced" while a user's changes
|
|
487
|
+
* sit queued. Returns the unsubscribe.
|
|
488
|
+
*/
|
|
489
|
+
watchPendingWrites(listener) {
|
|
490
|
+
return this.watchState(async (userId) => {
|
|
491
|
+
if (!userId)
|
|
492
|
+
return () => listener(null);
|
|
493
|
+
const { pending } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
494
|
+
const writes = pending.map(pending_write_js_1.toPendingWrite);
|
|
495
|
+
return () => listener(writes);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/** The unresolved conflicts now, and again whenever they change. null while signed out. */
|
|
499
|
+
watchConflicts(listener) {
|
|
500
|
+
return this.watchState(async (userId) => {
|
|
501
|
+
if (!userId)
|
|
502
|
+
return () => listener(null);
|
|
503
|
+
const list = await this.conflicts();
|
|
504
|
+
return () => listener(list);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* The signed-in user changed: every watcher reads again, for the new user
|
|
509
|
+
* (or null, signed out). Koolbase.initialize wires it to auth changes.
|
|
510
|
+
*/
|
|
511
|
+
sessionChanged() {
|
|
512
|
+
for (const emit of [...this.stateWatchers])
|
|
513
|
+
emit();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* One watcher: reads at once, then on every change to the signed-in user's
|
|
517
|
+
* offline state. Reads can finish out of order; only the latest delivers.
|
|
518
|
+
*/
|
|
519
|
+
watchState(read) {
|
|
520
|
+
let latest = 0;
|
|
521
|
+
let active = true;
|
|
522
|
+
const emit = () => {
|
|
523
|
+
const mine = ++latest;
|
|
524
|
+
read(this.getUserId()).then((deliver) => { if (active && mine === latest)
|
|
525
|
+
deliver(); }, () => { });
|
|
526
|
+
};
|
|
527
|
+
this.stateWatchers.add(emit);
|
|
528
|
+
if (!this.offStateChange) {
|
|
529
|
+
this.offStateChange = (0, offline_state_js_1.onOfflineStateChange)((userId) => {
|
|
530
|
+
if (userId === this.getUserId())
|
|
531
|
+
for (const w of [...this.stateWatchers])
|
|
532
|
+
w();
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
emit();
|
|
536
|
+
return () => {
|
|
537
|
+
active = false;
|
|
538
|
+
this.stateWatchers.delete(emit);
|
|
539
|
+
if (this.stateWatchers.size === 0 && this.offStateChange) {
|
|
540
|
+
this.offStateChange();
|
|
541
|
+
this.offStateChange = null;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
453
545
|
/**
|
|
454
546
|
* Count and total over a whole collection, with the read rule applied
|
|
455
547
|
* inside the query.
|
|
@@ -540,6 +632,25 @@ class KoolbaseDatabase {
|
|
|
540
632
|
}
|
|
541
633
|
return found;
|
|
542
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Removes a decided conflict and points the writes held behind it at the
|
|
637
|
+
* state the record is now in -- in one transaction, so the conflict can
|
|
638
|
+
* never be gone while its held writes still describe the old world.
|
|
639
|
+
*
|
|
640
|
+
* Without a known state (a refused add, a delete) there is nothing to
|
|
641
|
+
* rebase onto: the conflict is removed and the held writes stay as they are.
|
|
642
|
+
*/
|
|
643
|
+
async settleConflict(c, resolved, revision) {
|
|
644
|
+
if (!resolved || !c.recordId) {
|
|
645
|
+
await this.dropConflict(c.id);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const userId = this.requireUserId('conflict resolution');
|
|
649
|
+
await (0, offline_state_js_1.mutateOfflineState)(userId, (s) => {
|
|
650
|
+
s.conflicts = s.conflicts.filter((x) => x.id !== c.id);
|
|
651
|
+
(0, offline_state_js_1.rebaseHeldWrites)(s, c.recordId, resolved, revision);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
543
654
|
async dropConflict(id) {
|
|
544
655
|
const userId = this.requireUserId('conflict resolution');
|
|
545
656
|
await (0, offline_state_js_1.mutateOfflineState)(userId, (s) => {
|
|
@@ -554,6 +665,8 @@ class KoolbaseDatabase {
|
|
|
554
665
|
*/
|
|
555
666
|
async applyResolution(c, payload) {
|
|
556
667
|
const rev = c.serverRevision;
|
|
668
|
+
// What the server now holds, from its answer: the state held writes build on.
|
|
669
|
+
let landed = null;
|
|
557
670
|
try {
|
|
558
671
|
if (c.operation === 'insert') {
|
|
559
672
|
// Resolving a rejected insert IS the insert, retried — with amended
|
|
@@ -564,7 +677,7 @@ class KoolbaseDatabase {
|
|
|
564
677
|
// original on retry rather than duplicating — the queue's own
|
|
565
678
|
// lost-response discipline, extended to the one insert path that
|
|
566
679
|
// lacked it.
|
|
567
|
-
await this.request('POST', '/v1/sdk/db/insert', {
|
|
680
|
+
landed = await this.request('POST', '/v1/sdk/db/insert', {
|
|
568
681
|
collection: c.collection,
|
|
569
682
|
data: payload,
|
|
570
683
|
idempotency_key: c.id,
|
|
@@ -575,7 +688,7 @@ class KoolbaseDatabase {
|
|
|
575
688
|
await this.request('DELETE', `/v1/sdk/db/records/${c.recordId}${q}`);
|
|
576
689
|
}
|
|
577
690
|
else {
|
|
578
|
-
await this.request('PATCH', `/v1/sdk/db/records/${c.recordId}`, { data: payload, ...(rev !== undefined ? { expected_revision: rev } : {}) });
|
|
691
|
+
landed = await this.request('PATCH', `/v1/sdk/db/records/${c.recordId}`, { data: payload, ...(rev !== undefined ? { expected_revision: rev } : {}) });
|
|
579
692
|
}
|
|
580
693
|
}
|
|
581
694
|
catch (e) {
|
|
@@ -604,8 +717,9 @@ class KoolbaseDatabase {
|
|
|
604
717
|
}
|
|
605
718
|
throw e;
|
|
606
719
|
}
|
|
607
|
-
|
|
608
|
-
await
|
|
720
|
+
const landedRevision = landed && typeof landed.$revision === 'number' ? landed.$revision : undefined;
|
|
721
|
+
await this.settleConflict(c, landed ?? undefined, landedRevision);
|
|
722
|
+
await (0, cache_store_js_3.invalidateCache)(this.getUserId() ?? 'anonymous', c.collection);
|
|
609
723
|
void (0, query_refresh_js_1.refreshCollectionQueries)(c.collection);
|
|
610
724
|
}
|
|
611
725
|
// ─── Update (online-first with offline fallback) ───────────────────────────
|
|
@@ -642,8 +756,11 @@ class KoolbaseDatabase {
|
|
|
642
756
|
for (const w of queued) {
|
|
643
757
|
if (w.operation === 'insert')
|
|
644
758
|
projected = { ...(w.data ?? {}) };
|
|
759
|
+
// An edit applies to the record as it was: the queued add's state, or
|
|
760
|
+
// -- first in a chain with no add -- the baseline the edit was made
|
|
761
|
+
// against. Starting from nothing dropped every field it did not touch.
|
|
645
762
|
else if (w.operation === 'update')
|
|
646
|
-
projected = { ...(projected ?? {}), ...(w.data ?? {}) };
|
|
763
|
+
projected = { ...(projected ?? w.baseline ?? {}), ...(w.data ?? {}) };
|
|
647
764
|
else if (w.operation === 'delete')
|
|
648
765
|
projected = null;
|
|
649
766
|
}
|
|
@@ -712,6 +829,8 @@ class KoolbaseDatabase {
|
|
|
712
829
|
});
|
|
713
830
|
const merged = { ...base.baseline, ...data };
|
|
714
831
|
await (0, cache_store_js_1.cacheRecord)(userId, base.collection, recordId, merged, base.revision);
|
|
832
|
+
// And in the saved lists, so they show what the user just changed.
|
|
833
|
+
await (0, cache_store_js_2.updateInCachedQueries)(userId, base.collection, recordId, data);
|
|
715
834
|
// Optimistic: durable locally and queued to send, not yet accepted.
|
|
716
835
|
return {
|
|
717
836
|
id: recordId,
|
|
@@ -772,8 +891,9 @@ class KoolbaseDatabase {
|
|
|
772
891
|
});
|
|
773
892
|
// The queued write holds its own copy of the baseline, so removing the
|
|
774
893
|
// cached record costs nothing and keeps local reads consistent with what
|
|
775
|
-
// the user just did.
|
|
894
|
+
// the user just did -- saved lists included.
|
|
776
895
|
await (0, cache_store_js_1.removeCachedRecord)(userId, recordId);
|
|
896
|
+
await (0, cache_store_js_2.removeFromCachedQueries)(userId, base.collection, recordId);
|
|
777
897
|
}
|
|
778
898
|
}
|
|
779
899
|
// ─── Vectors ────────────────────────────────────────────────────────────────
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type { RecordState, RecordStatus } from './record-controller.js';
|
|
|
17
17
|
export { KoolbaseFlags } from './flags.js';
|
|
18
18
|
export { KoolbaseFunctions } from './functions.js';
|
|
19
19
|
export { KoolbaseRealtime } from './realtime.js';
|
|
20
|
+
export { KoolbaseConnectivity } from './connectivity.js';
|
|
21
|
+
export type { ConnectivityState } from './connectivity.js';
|
|
20
22
|
export { KoolbaseStorage } from './storage.js';
|
|
21
23
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
22
24
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
@@ -27,4 +29,4 @@ export { getOrCreateDeviceId } from './device-id.js';
|
|
|
27
29
|
export { koolbaseSdkVersion } from './device-metadata.js';
|
|
28
30
|
export { RestoreResult } from './types.js';
|
|
29
31
|
export type { AuthStateListener, FetchLike, KoolbaseAuthStorage, ResendVerificationResult } from './types.js';
|
|
30
|
-
export { setPlatform, getPlatform, memoryPlatform, type PlatformAdapter, type PlatformStorage, type PlatformNetwork, type PlatformLifecycle, type PlatformLocks, type PlatformInfo, } from './platform.js';
|
|
32
|
+
export { setPlatform, getPlatform, memoryPlatform, type PlatformAdapter, type PlatformBroadcast, type PlatformStorage, type PlatformNetwork, type PlatformLifecycle, type PlatformLocks, type PlatformInfo, } from './platform.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.initialRecordState = exports.KoolbaseRecordController = 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.KoolbaseConnectivity = 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);
|
|
@@ -47,6 +47,8 @@ var functions_js_1 = require("./functions.js");
|
|
|
47
47
|
Object.defineProperty(exports, "KoolbaseFunctions", { enumerable: true, get: function () { return functions_js_1.KoolbaseFunctions; } });
|
|
48
48
|
var realtime_js_1 = require("./realtime.js");
|
|
49
49
|
Object.defineProperty(exports, "KoolbaseRealtime", { enumerable: true, get: function () { return realtime_js_1.KoolbaseRealtime; } });
|
|
50
|
+
var connectivity_js_1 = require("./connectivity.js");
|
|
51
|
+
Object.defineProperty(exports, "KoolbaseConnectivity", { enumerable: true, get: function () { return connectivity_js_1.KoolbaseConnectivity; } });
|
|
50
52
|
var storage_js_1 = require("./storage.js");
|
|
51
53
|
Object.defineProperty(exports, "KoolbaseStorage", { enumerable: true, get: function () { return storage_js_1.KoolbaseStorage; } });
|
|
52
54
|
var analytics_js_1 = require("./analytics.js");
|
|
@@ -103,3 +103,23 @@ export declare function queueWrite(userId: string, write: Omit<QueuedWrite, 'ret
|
|
|
103
103
|
* and nothing is written on a guess.
|
|
104
104
|
*/
|
|
105
105
|
export declare function migrateLegacyQueue(userId: string): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Points the writes still queued for a record at the state it is now in.
|
|
108
|
+
*
|
|
109
|
+
* When a conflict is decided, the writes held behind it carry baselines
|
|
110
|
+
* describing the state the conflicted write would have produced -- a state
|
|
111
|
+
* that no longer exists, and may never have. Released unchanged they would
|
|
112
|
+
* replay against a revision that has moved on, and each would conflict in
|
|
113
|
+
* turn: one disagreement multiplying into as many conflicts as the user had
|
|
114
|
+
* queued. The same rule as the Flutter SDK's rebaseAfterResolution.
|
|
115
|
+
*
|
|
116
|
+
* Refreshing the baseline is enough because every operation is an absolute
|
|
117
|
+
* assignment: a patch says what a field becomes, not what to do to it. If a
|
|
118
|
+
* relative operation is ever added (an increment, an append), the chain must
|
|
119
|
+
* be replayed through its projected states instead.
|
|
120
|
+
*/
|
|
121
|
+
export declare function rebaseHeldWrites(state: OfflineState, recordId: string, resolved: Record<string, unknown>, revision: number | undefined): void;
|
|
122
|
+
type StateListener = (userId: string) => void;
|
|
123
|
+
/** Called with the user id whenever that user's offline state changes. */
|
|
124
|
+
export declare function onOfflineStateChange(listener: StateListener): () => void;
|
|
125
|
+
export {};
|