@koolbase/core 12.13.1 → 12.15.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/auth.d.ts +31 -1
- package/dist/cjs/auth.js +167 -20
- package/dist/cjs/cache-store.d.ts +5 -0
- package/dist/cjs/cache-store.js +31 -0
- package/dist/cjs/connectivity.d.ts +25 -0
- package/dist/cjs/connectivity.js +59 -0
- package/dist/cjs/database.d.ts +32 -0
- package/dist/cjs/database.js +111 -16
- package/dist/cjs/fiscal.js +2 -2
- package/dist/cjs/functions.js +2 -2
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/network.d.ts +14 -0
- package/dist/cjs/network.js +60 -0
- package/dist/cjs/offline-state.d.ts +4 -0
- package/dist/cjs/offline-state.js +36 -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/storage.js +11 -11
- package/dist/cjs/sync-engine.js +3 -3
- package/dist/cjs/types.d.ts +7 -0
- package/dist/esm/auth.d.ts +31 -1
- package/dist/esm/auth.js +168 -21
- package/dist/esm/cache-store.d.ts +5 -0
- package/dist/esm/cache-store.js +30 -0
- package/dist/esm/connectivity.d.ts +25 -0
- package/dist/esm/connectivity.js +55 -0
- package/dist/esm/database.d.ts +32 -0
- package/dist/esm/database.js +101 -6
- package/dist/esm/fiscal.js +3 -3
- package/dist/esm/functions.js +3 -3
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/network.d.ts +14 -0
- package/dist/esm/network.js +58 -0
- package/dist/esm/offline-state.d.ts +4 -0
- package/dist/esm/offline-state.js +35 -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/storage.js +12 -12
- package/dist/esm/sync-engine.js +4 -4
- package/dist/esm/types.d.ts +7 -0
- package/package.json +1 -1
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
|
*
|
|
@@ -94,7 +97,7 @@ class KoolbaseDatabase {
|
|
|
94
97
|
};
|
|
95
98
|
}
|
|
96
99
|
async request(method, path, body) {
|
|
97
|
-
const res = await (0, network_js_1.
|
|
100
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}${path}`, {
|
|
98
101
|
method,
|
|
99
102
|
headers: await this.buildHeaders(),
|
|
100
103
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -132,7 +135,7 @@ class KoolbaseDatabase {
|
|
|
132
135
|
* none of them clearing a rejected session. One path, two shapes of result.
|
|
133
136
|
*/
|
|
134
137
|
async requestWithStatus(method, path, body) {
|
|
135
|
-
const res = await (0, network_js_1.
|
|
138
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}${path}`, {
|
|
136
139
|
method,
|
|
137
140
|
headers: await this.buildHeaders(),
|
|
138
141
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -207,12 +210,12 @@ class KoolbaseDatabase {
|
|
|
207
210
|
// query's identity, so both policies share one cache entry. onRefresh is
|
|
208
211
|
// who to tell, not what to read, so it stays out the same way.
|
|
209
212
|
const { cache: cachePolicy, onRefresh, ...queryOptions } = options;
|
|
210
|
-
const queryHash = (0,
|
|
211
|
-
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);
|
|
212
215
|
if (cached) {
|
|
213
216
|
this.runQuery(collection, queryOptions)
|
|
214
217
|
.then(async (result) => {
|
|
215
|
-
await (0,
|
|
218
|
+
await (0, cache_store_js_3.setCached)(userId, collection, queryHash, result);
|
|
216
219
|
// Told outside the catch below, which is for the network: an
|
|
217
220
|
// exception from the caller's callback is the caller's to see.
|
|
218
221
|
if (onRefresh)
|
|
@@ -224,7 +227,7 @@ class KoolbaseDatabase {
|
|
|
224
227
|
return { ...cached, isFromCache: true };
|
|
225
228
|
}
|
|
226
229
|
const result = await this.runQuery(collection, queryOptions);
|
|
227
|
-
await (0,
|
|
230
|
+
await (0, cache_store_js_3.setCached)(userId, collection, queryHash, result);
|
|
228
231
|
return { ...result, isFromCache: false };
|
|
229
232
|
}
|
|
230
233
|
/**
|
|
@@ -233,7 +236,7 @@ class KoolbaseDatabase {
|
|
|
233
236
|
* a server, or another user. The SDK's own writes already do this.
|
|
234
237
|
*/
|
|
235
238
|
async invalidate(collection) {
|
|
236
|
-
await (0,
|
|
239
|
+
await (0, cache_store_js_3.invalidateCache)(this.getUserId() ?? 'anonymous', collection);
|
|
237
240
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
238
241
|
}
|
|
239
242
|
// ─── Insert (online-first with offline fallback) ───────────────────────────
|
|
@@ -257,7 +260,7 @@ class KoolbaseDatabase {
|
|
|
257
260
|
// next query sees real data instead of a stale optimistic copy.
|
|
258
261
|
const raw = await this.request('POST', '/v1/sdk/db/insert', { collection, data });
|
|
259
262
|
const record = (0, record_js_1.recordFromWire)(raw);
|
|
260
|
-
await (0,
|
|
263
|
+
await (0, cache_store_js_3.invalidateCache)(userId, collection);
|
|
261
264
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
262
265
|
// The response carries a fresh revision, so caching it keeps the
|
|
263
266
|
// baseline current for whatever edits this record next.
|
|
@@ -293,7 +296,7 @@ class KoolbaseDatabase {
|
|
|
293
296
|
createdAt: new Date().toISOString(),
|
|
294
297
|
updatedAt: new Date().toISOString(),
|
|
295
298
|
};
|
|
296
|
-
await (0,
|
|
299
|
+
await (0, cache_store_js_3.optimisticallyInsert)(userId, collection, optimisticRecord);
|
|
297
300
|
// No baseline: an insert has no prior state, and the record does not
|
|
298
301
|
// exist on the server yet, so there is nothing to be conditional against.
|
|
299
302
|
// An offline edit to it composes against this queued write instead.
|
|
@@ -332,7 +335,7 @@ class KoolbaseDatabase {
|
|
|
332
335
|
const record = (0, record_js_1.recordFromWire)(body);
|
|
333
336
|
// Keep the cache fresh, same intent as insert's post-success invalidate.
|
|
334
337
|
const userId = this.getUserId() ?? 'anonymous';
|
|
335
|
-
await (0,
|
|
338
|
+
await (0, cache_store_js_3.invalidateCache)(userId, collection);
|
|
336
339
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
337
340
|
await (0, cache_store_js_1.cacheRecord)(userId, collection, record.id, record.data, record.revision);
|
|
338
341
|
return { record, created };
|
|
@@ -352,7 +355,7 @@ class KoolbaseDatabase {
|
|
|
352
355
|
async deleteWhere(collection, filters) {
|
|
353
356
|
const body = await this.request('POST', '/v1/sdk/db/delete-where', { collection, filters });
|
|
354
357
|
const userId = this.getUserId() ?? 'anonymous';
|
|
355
|
-
await (0,
|
|
358
|
+
await (0, cache_store_js_3.invalidateCache)(userId, collection);
|
|
356
359
|
void (0, query_refresh_js_1.refreshCollectionQueries)(collection);
|
|
357
360
|
return body.deleted ?? 0;
|
|
358
361
|
}
|
|
@@ -416,7 +419,7 @@ class KoolbaseDatabase {
|
|
|
416
419
|
}
|
|
417
420
|
}
|
|
418
421
|
for (const col of touched) {
|
|
419
|
-
await (0,
|
|
422
|
+
await (0, cache_store_js_3.invalidateCache)(userId, col);
|
|
420
423
|
void (0, query_refresh_js_1.refreshCollectionQueries)(col);
|
|
421
424
|
}
|
|
422
425
|
return results;
|
|
@@ -433,6 +436,29 @@ class KoolbaseDatabase {
|
|
|
433
436
|
}
|
|
434
437
|
return record;
|
|
435
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
|
+
}
|
|
436
462
|
// ─── Conflicts ──────────────────────────────────────────────────────────────
|
|
437
463
|
/**
|
|
438
464
|
* Writes that could not be applied, waiting for a decision.
|
|
@@ -453,6 +479,69 @@ class KoolbaseDatabase {
|
|
|
453
479
|
const { pending } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
454
480
|
return pending.map(pending_write_js_1.toPendingWrite);
|
|
455
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
|
+
}
|
|
456
545
|
/**
|
|
457
546
|
* Count and total over a whole collection, with the read rule applied
|
|
458
547
|
* inside the query.
|
|
@@ -630,7 +719,7 @@ class KoolbaseDatabase {
|
|
|
630
719
|
}
|
|
631
720
|
const landedRevision = landed && typeof landed.$revision === 'number' ? landed.$revision : undefined;
|
|
632
721
|
await this.settleConflict(c, landed ?? undefined, landedRevision);
|
|
633
|
-
await (0,
|
|
722
|
+
await (0, cache_store_js_3.invalidateCache)(this.getUserId() ?? 'anonymous', c.collection);
|
|
634
723
|
void (0, query_refresh_js_1.refreshCollectionQueries)(c.collection);
|
|
635
724
|
}
|
|
636
725
|
// ─── Update (online-first with offline fallback) ───────────────────────────
|
|
@@ -667,8 +756,11 @@ class KoolbaseDatabase {
|
|
|
667
756
|
for (const w of queued) {
|
|
668
757
|
if (w.operation === 'insert')
|
|
669
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.
|
|
670
762
|
else if (w.operation === 'update')
|
|
671
|
-
projected = { ...(projected ?? {}), ...(w.data ?? {}) };
|
|
763
|
+
projected = { ...(projected ?? w.baseline ?? {}), ...(w.data ?? {}) };
|
|
672
764
|
else if (w.operation === 'delete')
|
|
673
765
|
projected = null;
|
|
674
766
|
}
|
|
@@ -737,6 +829,8 @@ class KoolbaseDatabase {
|
|
|
737
829
|
});
|
|
738
830
|
const merged = { ...base.baseline, ...data };
|
|
739
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);
|
|
740
834
|
// Optimistic: durable locally and queued to send, not yet accepted.
|
|
741
835
|
return {
|
|
742
836
|
id: recordId,
|
|
@@ -797,8 +891,9 @@ class KoolbaseDatabase {
|
|
|
797
891
|
});
|
|
798
892
|
// The queued write holds its own copy of the baseline, so removing the
|
|
799
893
|
// cached record costs nothing and keeps local reads consistent with what
|
|
800
|
-
// the user just did.
|
|
894
|
+
// the user just did -- saved lists included.
|
|
801
895
|
await (0, cache_store_js_1.removeCachedRecord)(userId, recordId);
|
|
896
|
+
await (0, cache_store_js_2.removeFromCachedQueries)(userId, base.collection, recordId);
|
|
802
897
|
}
|
|
803
898
|
}
|
|
804
899
|
// ─── Vectors ────────────────────────────────────────────────────────────────
|
package/dist/cjs/fiscal.js
CHANGED
|
@@ -113,7 +113,7 @@ class KoolbaseFiscal {
|
|
|
113
113
|
const controller = new AbortController();
|
|
114
114
|
const timer = setTimeout(() => controller.abort(), params.timeoutMs ?? 30000);
|
|
115
115
|
try {
|
|
116
|
-
const res = await (0, network_js_1.
|
|
116
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/fiscal/submit`, {
|
|
117
117
|
method: 'POST',
|
|
118
118
|
headers: await this.buildHeaders(),
|
|
119
119
|
signal: controller.signal,
|
|
@@ -151,7 +151,7 @@ class KoolbaseFiscal {
|
|
|
151
151
|
device_id: params.deviceId,
|
|
152
152
|
client_ref: params.clientRef,
|
|
153
153
|
});
|
|
154
|
-
const res = await (0, network_js_1.
|
|
154
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/fiscal/status?${q.toString()}`, { method: 'GET', headers: await this.buildHeaders(), signal: controller.signal });
|
|
155
155
|
return await this.decode(res);
|
|
156
156
|
}
|
|
157
157
|
catch (e) {
|
package/dist/cjs/functions.js
CHANGED
|
@@ -14,7 +14,7 @@ class KoolbaseFunctions {
|
|
|
14
14
|
// ─── Deploy ────────────────────────────────────────────────────────────────
|
|
15
15
|
async deploy(options) {
|
|
16
16
|
const runtime = options.runtime ?? types_js_1.FunctionRuntime.Deno;
|
|
17
|
-
const res = await (0, network_js_1.
|
|
17
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
|
|
18
18
|
method: 'POST',
|
|
19
19
|
headers: {
|
|
20
20
|
'Content-Type': 'application/json',
|
|
@@ -58,7 +58,7 @@ class KoolbaseFunctions {
|
|
|
58
58
|
if (userToken) {
|
|
59
59
|
headers['Authorization'] = `Bearer ${userToken}`;
|
|
60
60
|
}
|
|
61
|
-
const res = await (0, network_js_1.
|
|
61
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
|
|
62
62
|
method: 'POST',
|
|
63
63
|
headers,
|
|
64
64
|
body: JSON.stringify({ body: body ?? {} }),
|
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");
|
package/dist/cjs/network.d.ts
CHANGED
|
@@ -19,3 +19,17 @@ export declare class KoolbaseNetworkError extends TypeError {
|
|
|
19
19
|
}
|
|
20
20
|
/** fetch, with network failures turned into a KoolbaseNetworkError. */
|
|
21
21
|
export declare function koolbaseFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
22
|
+
type Renewal = (refusedToken: string) => Promise<string | null>;
|
|
23
|
+
/** Installed by Koolbase.initialize: renews the session a request was refused with. */
|
|
24
|
+
export declare function setSessionRenewal(fn: Renewal | null): void;
|
|
25
|
+
/**
|
|
26
|
+
* koolbaseFetch for a data request. A request refused as unauthenticated
|
|
27
|
+
* (401) while carrying the signed-in person's token is the session needing
|
|
28
|
+
* renewal, not ending: the access token expired early, the clock is off, or
|
|
29
|
+
* another tab renewed it. The session is renewed once and the request sent
|
|
30
|
+
* again with the new token, so the caller never sees the first refusal.
|
|
31
|
+
* Only when renewal is refused does the 401 reach the caller, and with it the
|
|
32
|
+
* client's onSessionExpired, which ends the session.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sessionFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
35
|
+
export {};
|
package/dist/cjs/network.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.KoolbaseNetworkError = void 0;
|
|
4
4
|
exports.koolbaseFetch = koolbaseFetch;
|
|
5
|
+
exports.setSessionRenewal = setSessionRenewal;
|
|
6
|
+
exports.sessionFetch = sessionFetch;
|
|
5
7
|
/**
|
|
6
8
|
* Network failures, made actionable.
|
|
7
9
|
*
|
|
@@ -52,3 +54,61 @@ async function koolbaseFetch(input, init) {
|
|
|
52
54
|
throw new KoolbaseNetworkError(input, e);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
let renewal = null;
|
|
58
|
+
/** Installed by Koolbase.initialize: renews the session a request was refused with. */
|
|
59
|
+
function setSessionRenewal(fn) {
|
|
60
|
+
renewal = fn;
|
|
61
|
+
}
|
|
62
|
+
function headerOf(headers, name) {
|
|
63
|
+
if (!headers)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (typeof headers.get === 'function')
|
|
66
|
+
return headers.get(name) ?? undefined;
|
|
67
|
+
if (Array.isArray(headers))
|
|
68
|
+
return headers.find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
|
|
69
|
+
const rec = headers;
|
|
70
|
+
const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
71
|
+
return key ? rec[key] : undefined;
|
|
72
|
+
}
|
|
73
|
+
function withHeader(headers, name, value) {
|
|
74
|
+
const out = {};
|
|
75
|
+
if (headers && typeof headers.forEach === 'function' && !Array.isArray(headers)) {
|
|
76
|
+
headers.forEach((v, k) => { out[k] = v; });
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(headers)) {
|
|
79
|
+
for (const [k, v] of headers)
|
|
80
|
+
out[k] = v;
|
|
81
|
+
}
|
|
82
|
+
else if (headers) {
|
|
83
|
+
Object.assign(out, headers);
|
|
84
|
+
}
|
|
85
|
+
for (const k of Object.keys(out))
|
|
86
|
+
if (k.toLowerCase() === name.toLowerCase())
|
|
87
|
+
delete out[k];
|
|
88
|
+
out[name] = value;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* koolbaseFetch for a data request. A request refused as unauthenticated
|
|
93
|
+
* (401) while carrying the signed-in person's token is the session needing
|
|
94
|
+
* renewal, not ending: the access token expired early, the clock is off, or
|
|
95
|
+
* another tab renewed it. The session is renewed once and the request sent
|
|
96
|
+
* again with the new token, so the caller never sees the first refusal.
|
|
97
|
+
* Only when renewal is refused does the 401 reach the caller, and with it the
|
|
98
|
+
* client's onSessionExpired, which ends the session.
|
|
99
|
+
*/
|
|
100
|
+
async function sessionFetch(input, init) {
|
|
101
|
+
const res = await koolbaseFetch(input, init);
|
|
102
|
+
if (res.status !== 401 || !renewal)
|
|
103
|
+
return res;
|
|
104
|
+
const auth = headerOf(init?.headers, 'Authorization');
|
|
105
|
+
if (!auth || !auth.startsWith('Bearer '))
|
|
106
|
+
return res;
|
|
107
|
+
// Only a body that can be sent twice. Data requests send JSON text.
|
|
108
|
+
if (init?.body !== undefined && init.body !== null && typeof init.body !== 'string')
|
|
109
|
+
return res;
|
|
110
|
+
const fresh = await renewal(auth.slice('Bearer '.length));
|
|
111
|
+
if (!fresh)
|
|
112
|
+
return res;
|
|
113
|
+
return koolbaseFetch(input, { ...init, headers: withHeader(init?.headers, 'Authorization', `Bearer ${fresh}`) });
|
|
114
|
+
}
|
|
@@ -119,3 +119,7 @@ export declare function migrateLegacyQueue(userId: string): Promise<void>;
|
|
|
119
119
|
* be replayed through its projected states instead.
|
|
120
120
|
*/
|
|
121
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 {};
|
|
@@ -9,6 +9,7 @@ exports.mutateOfflineState = mutateOfflineState;
|
|
|
9
9
|
exports.queueWrite = queueWrite;
|
|
10
10
|
exports.migrateLegacyQueue = migrateLegacyQueue;
|
|
11
11
|
exports.rebaseHeldWrites = rebaseHeldWrites;
|
|
12
|
+
exports.onOfflineStateChange = onOfflineStateChange;
|
|
12
13
|
const platform_js_1 = require("./platform.js");
|
|
13
14
|
const shared_js_1 = require("./shared.js");
|
|
14
15
|
/**
|
|
@@ -142,6 +143,7 @@ async function mutateOfflineState(userId, mutate) {
|
|
|
142
143
|
}
|
|
143
144
|
await (0, platform_js_1.getPlatform)().storage.setItem(stateKey(userId), serialised);
|
|
144
145
|
});
|
|
146
|
+
announce(userId);
|
|
145
147
|
}
|
|
146
148
|
/** Adds a write to the queue, under the user's lock. */
|
|
147
149
|
async function queueWrite(userId, write) {
|
|
@@ -247,3 +249,37 @@ function rebaseHeldWrites(state, recordId, resolved, revision) {
|
|
|
247
249
|
break;
|
|
248
250
|
}
|
|
249
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 });
|