kaafil-js 0.2.0 → 0.3.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/client-entry.cjs +52 -4
- package/dist/client-entry.cjs.map +1 -1
- package/dist/client-entry.d.cts +67 -2
- package/dist/client-entry.d.ts +67 -2
- package/dist/client-entry.js +52 -4
- package/dist/client-entry.js.map +1 -1
- package/package.json +1 -1
package/dist/client-entry.d.cts
CHANGED
|
@@ -126,6 +126,27 @@ interface OutboxEntityRef {
|
|
|
126
126
|
readonly listName: string;
|
|
127
127
|
readonly id?: string;
|
|
128
128
|
readonly arrayKey?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Set ONLY by a create, and set to the same value as `id`: the id this
|
|
131
|
+
* device minted locally because the server had not assigned one yet.
|
|
132
|
+
*
|
|
133
|
+
* A create is the one write whose optimistic row cannot be keyed on the
|
|
134
|
+
* server's identity, and without a local key there is nothing to key it on
|
|
135
|
+
* at all — so before this existed, creates simply had no optimistic row.
|
|
136
|
+
* The manager added a checklist item out of signal and watched it not
|
|
137
|
+
* appear: queued, durable, landing on reconnect, and invisible until it
|
|
138
|
+
* did. Indistinguishable, from where they were standing, from having lost
|
|
139
|
+
* it.
|
|
140
|
+
*
|
|
141
|
+
* Its presence is what lets `applySuccess` clean up after itself. The
|
|
142
|
+
* canonical row comes back under the SERVER's id, so writing it would leave
|
|
143
|
+
* the local row beside it and show the same item twice; the drainer purges
|
|
144
|
+
* this id in the same step, and the swap is one `snapshot.updated`.
|
|
145
|
+
*
|
|
146
|
+
* A temporary id is NEVER sent to the server — it is not in `body`, not in
|
|
147
|
+
* `path`, and no request carries it. It exists only as a snapshot key.
|
|
148
|
+
*/
|
|
149
|
+
readonly temporaryId?: string;
|
|
129
150
|
}
|
|
130
151
|
/**
|
|
131
152
|
* WHY an op is waiting out a backoff rung. Recorded because the two causes
|
|
@@ -460,8 +481,21 @@ interface SnapshotUpdatedEvent {
|
|
|
460
481
|
* from the pull never appears here — absence is not a change, and it is
|
|
461
482
|
* emphatically not a purge (`06 §4.1`). */
|
|
462
483
|
readonly lists: readonly string[];
|
|
463
|
-
/**
|
|
464
|
-
|
|
484
|
+
/**
|
|
485
|
+
* The cursor now stored: the pull response's own `meta.serverTime`.
|
|
486
|
+
*
|
|
487
|
+
* ABSENT for a change that never involved the server — an optimistic create
|
|
488
|
+
* painted while offline, or a queued op the host discarded. Those move rows
|
|
489
|
+
* and must repaint, but they carry no server reading, and putting a device
|
|
490
|
+
* clock in this field would be exactly the lie `../delta/cursor.ts` exists
|
|
491
|
+
* to prevent: a subscriber deriving "now" from it would age every other row
|
|
492
|
+
* against a phone's wall clock.
|
|
493
|
+
*
|
|
494
|
+
* A subscriber that only needs "something changed" ignores this field. One
|
|
495
|
+
* that shows an "as of" time, or derives the current server time, must skip
|
|
496
|
+
* an event without it and keep the last reading it had.
|
|
497
|
+
*/
|
|
498
|
+
readonly serverTime?: string;
|
|
465
499
|
}
|
|
466
500
|
interface ShareInvalidatedEvent {
|
|
467
501
|
/** The server-authored code when one arrived; absent when local
|
|
@@ -1308,6 +1342,22 @@ interface EnqueueWriteOptions {
|
|
|
1308
1342
|
/** The row to paint immediately, rebased later from the server's canonical
|
|
1309
1343
|
* entity. Omit for a write with nothing to show optimistically. */
|
|
1310
1344
|
readonly optimistic?: SnapshotRow;
|
|
1345
|
+
/**
|
|
1346
|
+
* Set on a CREATE, where there is no server id yet to key the optimistic
|
|
1347
|
+
* row on. This engine mints a temporary one, writes `optimistic` under it,
|
|
1348
|
+
* and records it on the op so the drainer can swap it for the canonical row
|
|
1349
|
+
* the moment the write lands (`OutboxEntityRef.temporaryId`).
|
|
1350
|
+
*
|
|
1351
|
+
* The caller does not mint the id and cannot see it before this resolves:
|
|
1352
|
+
* identity comes from the engine's own `newId`, which keeps it injectable
|
|
1353
|
+
* and keeps a UI layer out of the business of inventing identifiers. Read
|
|
1354
|
+
* it back off the returned op's `entity.temporaryId` if you need it.
|
|
1355
|
+
*
|
|
1356
|
+
* `entity.listName` is required alongside it — without a list there is
|
|
1357
|
+
* nowhere to paint — and any `entity.id` passed here is ignored, because a
|
|
1358
|
+
* create has no server identity to pass.
|
|
1359
|
+
*/
|
|
1360
|
+
readonly creates?: boolean;
|
|
1311
1361
|
readonly idempotencyKey?: string;
|
|
1312
1362
|
readonly enqueuedAt?: string;
|
|
1313
1363
|
}
|
|
@@ -1342,6 +1392,21 @@ interface OfflineEngine {
|
|
|
1342
1392
|
open(): Promise<void>;
|
|
1343
1393
|
/** Write-ahead, optimistic apply, kick — in that order. */
|
|
1344
1394
|
enqueue(options: EnqueueWriteOptions): Promise<OutboxOp>;
|
|
1395
|
+
/**
|
|
1396
|
+
* Abandons a queued op AND undoes its optimistic paint — the host-initiated
|
|
1397
|
+
* "discard my change" that a conflict surface or a sync centre offers.
|
|
1398
|
+
*
|
|
1399
|
+
* Use this rather than `outbox.drop()` directly. `drop()` is a queue
|
|
1400
|
+
* operation and knows nothing about the snapshot, so dropping a CREATE
|
|
1401
|
+
* through it leaves the locally-keyed row on screen forever: an item the
|
|
1402
|
+
* manager chose to discard, which no longer exists anywhere, and which no
|
|
1403
|
+
* pull can ever remove because the server never had it.
|
|
1404
|
+
*
|
|
1405
|
+
* Only a create's own temporary row is purged. An update's optimistic row
|
|
1406
|
+
* belongs to a server row that still exists, and the next pull rebases it —
|
|
1407
|
+
* removing it here would blank a row that is really there.
|
|
1408
|
+
*/
|
|
1409
|
+
discard(opId: string): Promise<void>;
|
|
1345
1410
|
/** The structured op enqueues NOW with `receiptPending: true`; the bytes
|
|
1346
1411
|
* upload opportunistically and back-fill later (`06 §3.1`). */
|
|
1347
1412
|
enqueueWithBlob(options: EnqueueWithBlobOptions): Promise<OutboxOp>;
|
package/dist/client-entry.d.ts
CHANGED
|
@@ -126,6 +126,27 @@ interface OutboxEntityRef {
|
|
|
126
126
|
readonly listName: string;
|
|
127
127
|
readonly id?: string;
|
|
128
128
|
readonly arrayKey?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Set ONLY by a create, and set to the same value as `id`: the id this
|
|
131
|
+
* device minted locally because the server had not assigned one yet.
|
|
132
|
+
*
|
|
133
|
+
* A create is the one write whose optimistic row cannot be keyed on the
|
|
134
|
+
* server's identity, and without a local key there is nothing to key it on
|
|
135
|
+
* at all — so before this existed, creates simply had no optimistic row.
|
|
136
|
+
* The manager added a checklist item out of signal and watched it not
|
|
137
|
+
* appear: queued, durable, landing on reconnect, and invisible until it
|
|
138
|
+
* did. Indistinguishable, from where they were standing, from having lost
|
|
139
|
+
* it.
|
|
140
|
+
*
|
|
141
|
+
* Its presence is what lets `applySuccess` clean up after itself. The
|
|
142
|
+
* canonical row comes back under the SERVER's id, so writing it would leave
|
|
143
|
+
* the local row beside it and show the same item twice; the drainer purges
|
|
144
|
+
* this id in the same step, and the swap is one `snapshot.updated`.
|
|
145
|
+
*
|
|
146
|
+
* A temporary id is NEVER sent to the server — it is not in `body`, not in
|
|
147
|
+
* `path`, and no request carries it. It exists only as a snapshot key.
|
|
148
|
+
*/
|
|
149
|
+
readonly temporaryId?: string;
|
|
129
150
|
}
|
|
130
151
|
/**
|
|
131
152
|
* WHY an op is waiting out a backoff rung. Recorded because the two causes
|
|
@@ -460,8 +481,21 @@ interface SnapshotUpdatedEvent {
|
|
|
460
481
|
* from the pull never appears here — absence is not a change, and it is
|
|
461
482
|
* emphatically not a purge (`06 §4.1`). */
|
|
462
483
|
readonly lists: readonly string[];
|
|
463
|
-
/**
|
|
464
|
-
|
|
484
|
+
/**
|
|
485
|
+
* The cursor now stored: the pull response's own `meta.serverTime`.
|
|
486
|
+
*
|
|
487
|
+
* ABSENT for a change that never involved the server — an optimistic create
|
|
488
|
+
* painted while offline, or a queued op the host discarded. Those move rows
|
|
489
|
+
* and must repaint, but they carry no server reading, and putting a device
|
|
490
|
+
* clock in this field would be exactly the lie `../delta/cursor.ts` exists
|
|
491
|
+
* to prevent: a subscriber deriving "now" from it would age every other row
|
|
492
|
+
* against a phone's wall clock.
|
|
493
|
+
*
|
|
494
|
+
* A subscriber that only needs "something changed" ignores this field. One
|
|
495
|
+
* that shows an "as of" time, or derives the current server time, must skip
|
|
496
|
+
* an event without it and keep the last reading it had.
|
|
497
|
+
*/
|
|
498
|
+
readonly serverTime?: string;
|
|
465
499
|
}
|
|
466
500
|
interface ShareInvalidatedEvent {
|
|
467
501
|
/** The server-authored code when one arrived; absent when local
|
|
@@ -1308,6 +1342,22 @@ interface EnqueueWriteOptions {
|
|
|
1308
1342
|
/** The row to paint immediately, rebased later from the server's canonical
|
|
1309
1343
|
* entity. Omit for a write with nothing to show optimistically. */
|
|
1310
1344
|
readonly optimistic?: SnapshotRow;
|
|
1345
|
+
/**
|
|
1346
|
+
* Set on a CREATE, where there is no server id yet to key the optimistic
|
|
1347
|
+
* row on. This engine mints a temporary one, writes `optimistic` under it,
|
|
1348
|
+
* and records it on the op so the drainer can swap it for the canonical row
|
|
1349
|
+
* the moment the write lands (`OutboxEntityRef.temporaryId`).
|
|
1350
|
+
*
|
|
1351
|
+
* The caller does not mint the id and cannot see it before this resolves:
|
|
1352
|
+
* identity comes from the engine's own `newId`, which keeps it injectable
|
|
1353
|
+
* and keeps a UI layer out of the business of inventing identifiers. Read
|
|
1354
|
+
* it back off the returned op's `entity.temporaryId` if you need it.
|
|
1355
|
+
*
|
|
1356
|
+
* `entity.listName` is required alongside it — without a list there is
|
|
1357
|
+
* nowhere to paint — and any `entity.id` passed here is ignored, because a
|
|
1358
|
+
* create has no server identity to pass.
|
|
1359
|
+
*/
|
|
1360
|
+
readonly creates?: boolean;
|
|
1311
1361
|
readonly idempotencyKey?: string;
|
|
1312
1362
|
readonly enqueuedAt?: string;
|
|
1313
1363
|
}
|
|
@@ -1342,6 +1392,21 @@ interface OfflineEngine {
|
|
|
1342
1392
|
open(): Promise<void>;
|
|
1343
1393
|
/** Write-ahead, optimistic apply, kick — in that order. */
|
|
1344
1394
|
enqueue(options: EnqueueWriteOptions): Promise<OutboxOp>;
|
|
1395
|
+
/**
|
|
1396
|
+
* Abandons a queued op AND undoes its optimistic paint — the host-initiated
|
|
1397
|
+
* "discard my change" that a conflict surface or a sync centre offers.
|
|
1398
|
+
*
|
|
1399
|
+
* Use this rather than `outbox.drop()` directly. `drop()` is a queue
|
|
1400
|
+
* operation and knows nothing about the snapshot, so dropping a CREATE
|
|
1401
|
+
* through it leaves the locally-keyed row on screen forever: an item the
|
|
1402
|
+
* manager chose to discard, which no longer exists anywhere, and which no
|
|
1403
|
+
* pull can ever remove because the server never had it.
|
|
1404
|
+
*
|
|
1405
|
+
* Only a create's own temporary row is purged. An update's optimistic row
|
|
1406
|
+
* belongs to a server row that still exists, and the next pull rebases it —
|
|
1407
|
+
* removing it here would blank a row that is really there.
|
|
1408
|
+
*/
|
|
1409
|
+
discard(opId: string): Promise<void>;
|
|
1345
1410
|
/** The structured op enqueues NOW with `receiptPending: true`; the bytes
|
|
1346
1411
|
* upload opportunistically and back-fill later (`06 §3.1`). */
|
|
1347
1412
|
enqueueWithBlob(options: EnqueueWithBlobOptions): Promise<OutboxOp>;
|
package/dist/client-entry.js
CHANGED
|
@@ -2583,6 +2583,11 @@ function createDrainer(options) {
|
|
|
2583
2583
|
row: outcome.entity,
|
|
2584
2584
|
...outcome.serverTime !== void 0 ? { syncedAt: outcome.serverTime } : {}
|
|
2585
2585
|
});
|
|
2586
|
+
const temporaryId = entity.temporaryId;
|
|
2587
|
+
if (temporaryId !== void 0 && temporaryId !== outcome.entity.id) {
|
|
2588
|
+
await snapshot.purgeEntity(op.tripRef, entity.listName, temporaryId);
|
|
2589
|
+
changed = true;
|
|
2590
|
+
}
|
|
2586
2591
|
}
|
|
2587
2592
|
if (changed && outcome.serverTime !== void 0) {
|
|
2588
2593
|
events.emit("snapshot.updated", {
|
|
@@ -2682,6 +2687,16 @@ function createDrainer(options) {
|
|
|
2682
2687
|
switch (result.action) {
|
|
2683
2688
|
case "drop":
|
|
2684
2689
|
await outbox.drop(op.id);
|
|
2690
|
+
if (op.entity?.temporaryId !== void 0) {
|
|
2691
|
+
await snapshot.purgeEntity(op.tripRef, op.entity.listName, op.entity.temporaryId);
|
|
2692
|
+
if (outcome.serverTime !== void 0) {
|
|
2693
|
+
events.emit("snapshot.updated", {
|
|
2694
|
+
tripRef: op.tripRef,
|
|
2695
|
+
lists: [op.entity.listName],
|
|
2696
|
+
serverTime: outcome.serverTime
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2685
2700
|
return "terminal";
|
|
2686
2701
|
case "reissue":
|
|
2687
2702
|
await outbox.reissue(op.id, {
|
|
@@ -3515,6 +3530,7 @@ function createOfflineEngine(options) {
|
|
|
3515
3530
|
});
|
|
3516
3531
|
const snapshot = createSnapshotStore({ storage: options.storage, keyPrefix });
|
|
3517
3532
|
const reads = createReadCacheStore({ storage: options.storage, keyPrefix });
|
|
3533
|
+
const mintEntityId = () => `tmp_${options.newId?.() ?? newIdempotencyKey()}`;
|
|
3518
3534
|
const outbox = createOutbox({
|
|
3519
3535
|
storage: options.storage,
|
|
3520
3536
|
keyPrefix,
|
|
@@ -3563,6 +3579,13 @@ function createOfflineEngine(options) {
|
|
|
3563
3579
|
let unbind;
|
|
3564
3580
|
async function enqueue(input) {
|
|
3565
3581
|
share?.assertWritable();
|
|
3582
|
+
let entity = input.entity;
|
|
3583
|
+
let temporaryId;
|
|
3584
|
+
if (input.creates === true && input.entity !== void 0) {
|
|
3585
|
+
const { temporaryId: _ignored, ...ref } = input.entity;
|
|
3586
|
+
temporaryId = mintEntityId();
|
|
3587
|
+
entity = { ...ref, id: temporaryId, temporaryId };
|
|
3588
|
+
}
|
|
3566
3589
|
const op = await outbox.enqueue({
|
|
3567
3590
|
tripRef: input.tripRef,
|
|
3568
3591
|
method: input.method,
|
|
@@ -3570,16 +3593,25 @@ function createOfflineEngine(options) {
|
|
|
3570
3593
|
operationId: input.operationId,
|
|
3571
3594
|
...input.body !== void 0 ? { body: input.body } : {},
|
|
3572
3595
|
...input.ifMatch !== void 0 ? { ifMatch: input.ifMatch } : {},
|
|
3573
|
-
...
|
|
3596
|
+
...entity !== void 0 ? { entity } : {},
|
|
3574
3597
|
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
|
|
3575
3598
|
...input.enqueuedAt !== void 0 ? { enqueuedAt: input.enqueuedAt } : {}
|
|
3576
3599
|
});
|
|
3577
|
-
if (input.optimistic !== void 0 &&
|
|
3600
|
+
if (input.optimistic !== void 0 && entity !== void 0) {
|
|
3578
3601
|
await snapshot.upsert({
|
|
3579
3602
|
tripRef: input.tripRef,
|
|
3580
|
-
listName:
|
|
3581
|
-
row
|
|
3603
|
+
listName: entity.listName,
|
|
3604
|
+
// A create's row is keyed on the minted id rather than on whatever
|
|
3605
|
+
// `id` the caller happened to put in the optimistic row — those two
|
|
3606
|
+
// disagreeing is what would leave an orphan the drainer cannot find.
|
|
3607
|
+
row: temporaryId === void 0 ? input.optimistic : { ...input.optimistic, id: temporaryId }
|
|
3582
3608
|
});
|
|
3609
|
+
if (temporaryId !== void 0) {
|
|
3610
|
+
events.emit("snapshot.updated", {
|
|
3611
|
+
tripRef: input.tripRef,
|
|
3612
|
+
lists: [entity.listName]
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3583
3615
|
}
|
|
3584
3616
|
drainer.kick();
|
|
3585
3617
|
return op;
|
|
@@ -3604,6 +3636,22 @@ function createOfflineEngine(options) {
|
|
|
3604
3636
|
drainer.kick();
|
|
3605
3637
|
},
|
|
3606
3638
|
enqueue,
|
|
3639
|
+
async discard(opId) {
|
|
3640
|
+
const op = outbox.get(opId);
|
|
3641
|
+
if (op === void 0) {
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
await outbox.drop(opId);
|
|
3645
|
+
const temporaryId = op.entity?.temporaryId;
|
|
3646
|
+
if (op.entity === void 0 || temporaryId === void 0) {
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
await snapshot.purgeEntity(op.tripRef, op.entity.listName, temporaryId);
|
|
3650
|
+
events.emit("snapshot.updated", {
|
|
3651
|
+
tripRef: op.tripRef,
|
|
3652
|
+
lists: [op.entity.listName]
|
|
3653
|
+
});
|
|
3654
|
+
},
|
|
3607
3655
|
async enqueueWithBlob(input) {
|
|
3608
3656
|
const { blob, ...rest } = input;
|
|
3609
3657
|
share?.assertWritable();
|