@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
|
@@ -8,6 +8,8 @@ exports.readOfflineState = readOfflineState;
|
|
|
8
8
|
exports.mutateOfflineState = mutateOfflineState;
|
|
9
9
|
exports.queueWrite = queueWrite;
|
|
10
10
|
exports.migrateLegacyQueue = migrateLegacyQueue;
|
|
11
|
+
exports.rebaseHeldWrites = rebaseHeldWrites;
|
|
12
|
+
exports.onOfflineStateChange = onOfflineStateChange;
|
|
11
13
|
const platform_js_1 = require("./platform.js");
|
|
12
14
|
const shared_js_1 = require("./shared.js");
|
|
13
15
|
/**
|
|
@@ -141,6 +143,7 @@ async function mutateOfflineState(userId, mutate) {
|
|
|
141
143
|
}
|
|
142
144
|
await (0, platform_js_1.getPlatform)().storage.setItem(stateKey(userId), serialised);
|
|
143
145
|
});
|
|
146
|
+
announce(userId);
|
|
144
147
|
}
|
|
145
148
|
/** Adds a write to the queue, under the user's lock. */
|
|
146
149
|
async function queueWrite(userId, write) {
|
|
@@ -213,3 +216,70 @@ async function migrateLegacyQueue(userId) {
|
|
|
213
216
|
});
|
|
214
217
|
await (0, platform_js_1.getPlatform)().storage.removeItem(legacyQueueKey(userId));
|
|
215
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Points the writes still queued for a record at the state it is now in.
|
|
221
|
+
*
|
|
222
|
+
* When a conflict is decided, the writes held behind it carry baselines
|
|
223
|
+
* describing the state the conflicted write would have produced -- a state
|
|
224
|
+
* that no longer exists, and may never have. Released unchanged they would
|
|
225
|
+
* replay against a revision that has moved on, and each would conflict in
|
|
226
|
+
* turn: one disagreement multiplying into as many conflicts as the user had
|
|
227
|
+
* queued. The same rule as the Flutter SDK's rebaseAfterResolution.
|
|
228
|
+
*
|
|
229
|
+
* Refreshing the baseline is enough because every operation is an absolute
|
|
230
|
+
* assignment: a patch says what a field becomes, not what to do to it. If a
|
|
231
|
+
* relative operation is ever added (an increment, an append), the chain must
|
|
232
|
+
* be replayed through its projected states instead.
|
|
233
|
+
*/
|
|
234
|
+
function rebaseHeldWrites(state, recordId, resolved, revision) {
|
|
235
|
+
let current = {};
|
|
236
|
+
for (const [k, v] of Object.entries(resolved)) {
|
|
237
|
+
if (!k.startsWith('$'))
|
|
238
|
+
current[k] = v;
|
|
239
|
+
}
|
|
240
|
+
for (const w of state.pending) {
|
|
241
|
+
if (w.recordId !== recordId)
|
|
242
|
+
continue;
|
|
243
|
+
w.baseline = { ...current };
|
|
244
|
+
w.baseRevision = revision;
|
|
245
|
+
// Each write is composed against the one before it.
|
|
246
|
+
if (w.operation === 'update')
|
|
247
|
+
current = { ...current, ...(w.data ?? {}) };
|
|
248
|
+
else if (w.operation === 'delete')
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ─── Change notification ─────────────────────────────────────────────────────
|
|
253
|
+
const ANNOUNCE_PREFIX = 'koolbase:offline-state:';
|
|
254
|
+
function stateListeners() {
|
|
255
|
+
return (0, shared_js_1.shared)('offlineStateListeners', () => new Set());
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Tells whoever is watching that a user's offline state changed -- here, and
|
|
259
|
+
* in every other copy of the SDK sharing this storage (other tabs). A badge in
|
|
260
|
+
* one tab must move when another tab's sync drains the queue.
|
|
261
|
+
*/
|
|
262
|
+
function announce(userId) {
|
|
263
|
+
for (const listener of [...stateListeners()]) {
|
|
264
|
+
try {
|
|
265
|
+
listener(userId);
|
|
266
|
+
}
|
|
267
|
+
catch { /* one broken listener must not stop the rest */ }
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
(0, platform_js_1.getPlatform)().broadcast?.post(ANNOUNCE_PREFIX + userId);
|
|
271
|
+
}
|
|
272
|
+
catch { /* best effort */ }
|
|
273
|
+
}
|
|
274
|
+
/** Called with the user id whenever that user's offline state changes. */
|
|
275
|
+
function onOfflineStateChange(listener) {
|
|
276
|
+
stateListeners().add(listener);
|
|
277
|
+
const off = (0, platform_js_1.getPlatform)().broadcast?.on((message) => {
|
|
278
|
+
if (message.startsWith(ANNOUNCE_PREFIX))
|
|
279
|
+
listener(message.slice(ANNOUNCE_PREFIX.length));
|
|
280
|
+
});
|
|
281
|
+
return () => {
|
|
282
|
+
stateListeners().delete(listener);
|
|
283
|
+
off?.();
|
|
284
|
+
};
|
|
285
|
+
}
|
package/dist/cjs/platform.d.ts
CHANGED
|
@@ -12,6 +12,21 @@ export interface PlatformNetwork {
|
|
|
12
12
|
* Koolbase is unreachable is handled the same as any failed request.
|
|
13
13
|
*/
|
|
14
14
|
onChange(callback: (online: boolean) => void): () => void;
|
|
15
|
+
/**
|
|
16
|
+
* What the host believes right now: true online, false offline, null when
|
|
17
|
+
* it cannot tell. Optional: a host without it reports nothing until its
|
|
18
|
+
* first change, and connectivity stays 'unknown' until then.
|
|
19
|
+
*/
|
|
20
|
+
current?(): Promise<boolean | null>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A message to every other copy of the SDK sharing this storage -- other tabs
|
|
24
|
+
* of the same origin in a browser. Optional: one process (React Native) has no
|
|
25
|
+
* other copies to tell.
|
|
26
|
+
*/
|
|
27
|
+
export interface PlatformBroadcast {
|
|
28
|
+
post(message: string): void;
|
|
29
|
+
on(callback: (message: string) => void): () => void;
|
|
15
30
|
}
|
|
16
31
|
export interface PlatformLifecycle {
|
|
17
32
|
/** Subscribe to the app moving to the background. Used to flush analytics. */
|
|
@@ -54,6 +69,7 @@ export interface PlatformAdapter {
|
|
|
54
69
|
lifecycle: PlatformLifecycle;
|
|
55
70
|
info: PlatformInfo;
|
|
56
71
|
locks: PlatformLocks;
|
|
72
|
+
broadcast?: PlatformBroadcast;
|
|
57
73
|
/**
|
|
58
74
|
* The host's best persistent store for the auth session, or null if it has
|
|
59
75
|
* none worth the name. Used only when the app injects nothing through
|
|
@@ -3,6 +3,6 @@ export interface UseRecordResult extends RecordState {
|
|
|
3
3
|
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
4
4
|
refresh: () => Promise<void>;
|
|
5
5
|
}
|
|
6
|
-
export declare function createUseRecord(getDb: () => Pick<KoolbaseDatabase, 'get'>): (collection: string, id: string | null | undefined, options?: {
|
|
6
|
+
export declare function createUseRecord(getDb: () => Pick<KoolbaseDatabase, 'get'> & Partial<Pick<KoolbaseDatabase, 'getSaved'>>): (collection: string, id: string | null | undefined, options?: {
|
|
7
7
|
live?: boolean;
|
|
8
8
|
}) => UseRecordResult;
|
|
@@ -8,9 +8,14 @@ export interface RecordState {
|
|
|
8
8
|
readonly error: unknown;
|
|
9
9
|
/** True while refresh() runs. */
|
|
10
10
|
readonly refreshing: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True while the record shown is the device's saved copy and the server has
|
|
13
|
+
* not confirmed it on this load. Stays true if the server cannot be reached.
|
|
14
|
+
*/
|
|
15
|
+
readonly isSaved: boolean;
|
|
11
16
|
}
|
|
12
17
|
export declare const initialRecordState: RecordState;
|
|
13
|
-
type RecordReader = Pick<KoolbaseDatabase, 'get'
|
|
18
|
+
type RecordReader = Pick<KoolbaseDatabase, 'get'> & Partial<Pick<KoolbaseDatabase, 'getSaved'>>;
|
|
14
19
|
export declare class KoolbaseRecordController {
|
|
15
20
|
readonly collection: string;
|
|
16
21
|
readonly id: string;
|
|
@@ -31,6 +36,8 @@ export declare class KoolbaseRecordController {
|
|
|
31
36
|
subscribe(listener: () => void): () => void;
|
|
32
37
|
/** The first load. Call once; refresh() for later loads. */
|
|
33
38
|
load(): Promise<void>;
|
|
39
|
+
/** The device's saved copy, shown at once while the server is asked. */
|
|
40
|
+
private showSaved;
|
|
34
41
|
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
35
42
|
refresh(): Promise<void>;
|
|
36
43
|
/** Stops all updates. A result still in flight is dropped when it lands. */
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
// or landing after dispose(), is dropped.
|
|
16
16
|
// - live (the option): realtime events for THIS record only. An update
|
|
17
17
|
// re-reads it silently, once per burst; a delete is notFound at once.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// - saved-first, never saved-only: the device's saved copy (db.getSaved)
|
|
19
|
+
// shows at once with isSaved true while the server is asked. Success
|
|
20
|
+
// replaces it (isSaved false); failure keeps it (isSaved stays true); not
|
|
21
|
+
// found is notFound. No saved copy: the ordinary loading/error states.
|
|
20
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
23
|
exports.KoolbaseRecordController = exports.initialRecordState = void 0;
|
|
22
24
|
const database_errors_js_1 = require("./database-errors.js");
|
|
@@ -26,6 +28,7 @@ exports.initialRecordState = Object.freeze({
|
|
|
26
28
|
record: null,
|
|
27
29
|
error: null,
|
|
28
30
|
refreshing: false,
|
|
31
|
+
isSaved: false,
|
|
29
32
|
});
|
|
30
33
|
class KoolbaseRecordController {
|
|
31
34
|
constructor(db, collection, id, options = {}) {
|
|
@@ -61,8 +64,27 @@ class KoolbaseRecordController {
|
|
|
61
64
|
if (this.live && this.id) {
|
|
62
65
|
this.unlive ?? (this.unlive = (0, live_source_js_1.liveSource)()?.subscribe(this.collection, (event) => this.onLive(event)) ?? null);
|
|
63
66
|
}
|
|
67
|
+
await this.showSaved();
|
|
64
68
|
await this.fetch();
|
|
65
69
|
}
|
|
70
|
+
/** The device's saved copy, shown at once while the server is asked. */
|
|
71
|
+
async showSaved() {
|
|
72
|
+
if (!this.id || !this.db.getSaved)
|
|
73
|
+
return;
|
|
74
|
+
const gen = this.generation;
|
|
75
|
+
let saved = null;
|
|
76
|
+
try {
|
|
77
|
+
saved = await this.db.getSaved(this.id);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!saved || this.isStale(gen) || this.state.status !== 'loading')
|
|
83
|
+
return;
|
|
84
|
+
if (saved.collection !== undefined && saved.collection !== this.collection)
|
|
85
|
+
return;
|
|
86
|
+
this.set({ status: 'loaded', record: saved, error: null, isSaved: true });
|
|
87
|
+
}
|
|
66
88
|
/** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
67
89
|
async refresh() {
|
|
68
90
|
if (this.disposed)
|
|
@@ -102,7 +124,7 @@ class KoolbaseRecordController {
|
|
|
102
124
|
this.liveTimer = null;
|
|
103
125
|
}
|
|
104
126
|
this.generation += 1;
|
|
105
|
-
this.set({ status: 'notFound', record: null, error: null, refreshing: false });
|
|
127
|
+
this.set({ status: 'notFound', record: null, error: null, refreshing: false, isSaved: false });
|
|
106
128
|
return;
|
|
107
129
|
}
|
|
108
130
|
if (this.liveTimer)
|
|
@@ -117,7 +139,7 @@ class KoolbaseRecordController {
|
|
|
117
139
|
async fetch() {
|
|
118
140
|
const gen = ++this.generation;
|
|
119
141
|
if (!this.id) {
|
|
120
|
-
this.set({ status: 'notFound', record: null, error: null });
|
|
142
|
+
this.set({ status: 'notFound', record: null, error: null, isSaved: false });
|
|
121
143
|
return gen;
|
|
122
144
|
}
|
|
123
145
|
try {
|
|
@@ -125,17 +147,17 @@ class KoolbaseRecordController {
|
|
|
125
147
|
if (this.isStale(gen))
|
|
126
148
|
return gen;
|
|
127
149
|
if (record.collection !== undefined && record.collection !== this.collection) {
|
|
128
|
-
this.set({ status: 'notFound', record: null, error: null });
|
|
150
|
+
this.set({ status: 'notFound', record: null, error: null, isSaved: false });
|
|
129
151
|
}
|
|
130
152
|
else {
|
|
131
|
-
this.set({ status: 'loaded', record, error: null });
|
|
153
|
+
this.set({ status: 'loaded', record, error: null, isSaved: false });
|
|
132
154
|
}
|
|
133
155
|
}
|
|
134
156
|
catch (error) {
|
|
135
157
|
if (this.isStale(gen))
|
|
136
158
|
return gen;
|
|
137
159
|
if (error instanceof database_errors_js_1.KoolbaseNotFoundError) {
|
|
138
|
-
this.set({ status: 'notFound', record: null, error: null });
|
|
160
|
+
this.set({ status: 'notFound', record: null, error: null, isSaved: false });
|
|
139
161
|
}
|
|
140
162
|
else if (this.state.record === null) {
|
|
141
163
|
this.set({ status: 'error', error });
|
package/dist/cjs/shared.d.ts
CHANGED
package/dist/cjs/sync-engine.js
CHANGED
|
@@ -101,13 +101,21 @@ class SyncEngine {
|
|
|
101
101
|
// being online — would give the same input different outcomes. It clears
|
|
102
102
|
// the old key when done, so later calls find nothing and return.
|
|
103
103
|
await (0, offline_state_js_1.migrateLegacyQueue)(userId);
|
|
104
|
-
const { pending } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
104
|
+
const { pending, conflicts } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
105
105
|
if (pending.length === 0)
|
|
106
106
|
return;
|
|
107
107
|
// Records whose chain stopped this pass. Writes queued after a conflicted
|
|
108
108
|
// one were composed against the state it would have produced, so applying
|
|
109
109
|
// them now would write against a state their baseline never described.
|
|
110
110
|
const blocked = new Set();
|
|
111
|
+
// And records whose chain stopped in an EARLIER pass: a conflict waits
|
|
112
|
+
// for a decision across restarts, and so must the writes behind it.
|
|
113
|
+
// Seeded only from this pass's finds, the next pass replayed them
|
|
114
|
+
// against a state their baselines never described -- each becoming its
|
|
115
|
+
// own conflict. Deciding the conflict rebases and releases them.
|
|
116
|
+
for (const c of conflicts)
|
|
117
|
+
if (c.recordId)
|
|
118
|
+
blocked.add(c.recordId);
|
|
111
119
|
for (const queued of pending) {
|
|
112
120
|
if (queued.recordId && blocked.has(queued.recordId))
|
|
113
121
|
continue;
|
|
@@ -202,10 +210,15 @@ class SyncEngine {
|
|
|
202
210
|
// — cached at enqueue for a record the server refused to create.
|
|
203
211
|
// Left alone it is a phantom: it renders as saved, and an offline
|
|
204
212
|
// edit against it queues a write to a record that does not exist.
|
|
205
|
-
// Evict it
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
213
|
+
// Evict it from the record cache and from every cached query that
|
|
214
|
+
// holds it -- precisely, not by dropping the collection's saved
|
|
215
|
+
// results, which a device still offline needs. The conflict above
|
|
216
|
+
// keeps the user's data and the server's verdict; the cache stops
|
|
217
|
+
// testifying to a fiction.
|
|
218
|
+
if (write.operation === 'insert' && write.recordId) {
|
|
219
|
+
await (0, cache_store_js_1.removeCachedRecord)(userId, write.recordId);
|
|
220
|
+
await (0, cache_store_js_1.removeFromCachedQueries)(userId, write.collection, write.recordId);
|
|
221
|
+
}
|
|
209
222
|
if (write.recordId)
|
|
210
223
|
blocked.add(write.recordId);
|
|
211
224
|
continue;
|
|
@@ -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/esm/cache-store.js
CHANGED
|
@@ -196,3 +196,65 @@ export async function optimisticallyInsert(userId, collection, record) {
|
|
|
196
196
|
// ignore
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The reverse of optimisticallyInsert: takes one record out of every cached
|
|
201
|
+
* result of a collection.
|
|
202
|
+
*
|
|
203
|
+
* Precise on purpose. Dropping the collection's cached results instead would
|
|
204
|
+
* leave a device that is still offline with no saved lists to show at all,
|
|
205
|
+
* to get rid of one record.
|
|
206
|
+
*/
|
|
207
|
+
export async function removeFromCachedQueries(userId, collection, recordId) {
|
|
208
|
+
try {
|
|
209
|
+
const keys = await getPlatform().storage.getAllKeys();
|
|
210
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:${collection}:`;
|
|
211
|
+
for (const key of keys.filter(k => k.startsWith(prefix))) {
|
|
212
|
+
const raw = await getPlatform().storage.getItem(key);
|
|
213
|
+
if (!raw)
|
|
214
|
+
continue;
|
|
215
|
+
const cached = JSON.parse(raw);
|
|
216
|
+
if (!Array.isArray(cached.records))
|
|
217
|
+
continue;
|
|
218
|
+
const kept = cached.records.filter(r => r.id !== recordId);
|
|
219
|
+
const removed = cached.records.length - kept.length;
|
|
220
|
+
if (removed === 0)
|
|
221
|
+
continue;
|
|
222
|
+
cached.records = kept;
|
|
223
|
+
cached.total = Math.max(0, cached.total - removed);
|
|
224
|
+
await getPlatform().storage.setItem(key, JSON.stringify(cached));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// ignore
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Applies an offline edit to the record wherever a cached result holds it, so
|
|
233
|
+
* a saved list shows what the user just changed rather than what it was.
|
|
234
|
+
*/
|
|
235
|
+
export async function updateInCachedQueries(userId, collection, recordId, data) {
|
|
236
|
+
try {
|
|
237
|
+
const keys = await getPlatform().storage.getAllKeys();
|
|
238
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:${collection}:`;
|
|
239
|
+
for (const key of keys.filter(k => k.startsWith(prefix))) {
|
|
240
|
+
const raw = await getPlatform().storage.getItem(key);
|
|
241
|
+
if (!raw)
|
|
242
|
+
continue;
|
|
243
|
+
const cached = JSON.parse(raw);
|
|
244
|
+
if (!Array.isArray(cached.records))
|
|
245
|
+
continue;
|
|
246
|
+
let changed = false;
|
|
247
|
+
cached.records = cached.records.map(r => {
|
|
248
|
+
if (r.id !== recordId)
|
|
249
|
+
return r;
|
|
250
|
+
changed = true;
|
|
251
|
+
return { ...r, data: { ...(r.data ?? {}), ...data } };
|
|
252
|
+
});
|
|
253
|
+
if (changed)
|
|
254
|
+
await getPlatform().storage.setItem(key, JSON.stringify(cached));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// ignore
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -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,55 @@
|
|
|
1
|
+
import { getPlatform } from './platform.js';
|
|
2
|
+
/**
|
|
3
|
+
* The device's connectivity, as state an app can render: getState() for now,
|
|
4
|
+
* subscribe() for changes (the shape React's useSyncExternalStore takes).
|
|
5
|
+
*/
|
|
6
|
+
export class KoolbaseConnectivity {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.state = 'unknown';
|
|
9
|
+
this.listeners = new Set();
|
|
10
|
+
this.stop = null;
|
|
11
|
+
// A change event is fresher than the first read: once one arrives, a late
|
|
12
|
+
// answer to that read is ignored.
|
|
13
|
+
this.heard = false;
|
|
14
|
+
}
|
|
15
|
+
/** Starts following the platform. Called once by Koolbase.initialize. */
|
|
16
|
+
start() {
|
|
17
|
+
if (this.stop)
|
|
18
|
+
return;
|
|
19
|
+
const network = getPlatform().network;
|
|
20
|
+
this.stop = network.onChange((online) => {
|
|
21
|
+
this.heard = true;
|
|
22
|
+
this.set(online ? 'online' : 'offline');
|
|
23
|
+
});
|
|
24
|
+
if (network.current) {
|
|
25
|
+
network.current().then((online) => {
|
|
26
|
+
if (!this.heard && online !== null && online !== undefined)
|
|
27
|
+
this.set(online ? 'online' : 'offline');
|
|
28
|
+
}, () => { });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
getState() {
|
|
32
|
+
return this.state;
|
|
33
|
+
}
|
|
34
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
35
|
+
subscribe(listener) {
|
|
36
|
+
this.listeners.add(listener);
|
|
37
|
+
return () => { this.listeners.delete(listener); };
|
|
38
|
+
}
|
|
39
|
+
dispose() {
|
|
40
|
+
this.stop?.();
|
|
41
|
+
this.stop = null;
|
|
42
|
+
this.listeners.clear();
|
|
43
|
+
}
|
|
44
|
+
set(next) {
|
|
45
|
+
if (next === this.state)
|
|
46
|
+
return;
|
|
47
|
+
this.state = next;
|
|
48
|
+
for (const listener of [...this.listeners]) {
|
|
49
|
+
try {
|
|
50
|
+
listener();
|
|
51
|
+
}
|
|
52
|
+
catch { /* one broken listener must not stop the rest */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/esm/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
|