@evolu/common 8.12.0 → 8.13.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/src/Callbacks.d.ts +12 -1
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +3 -0
- package/dist/src/Error.d.ts +10 -4
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +10 -4
- package/dist/src/Object.d.ts +65 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +142 -0
- package/dist/src/Resource.d.ts +0 -5
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +6 -13
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +7 -0
- package/dist/src/Task.d.ts +10 -8
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -5
- package/dist/src/Worker.d.ts +3 -3
- package/dist/src/local-first/Db.d.ts +8 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +56 -19
- package/dist/src/local-first/Evolu.d.ts +81 -37
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +31 -6
- package/dist/src/local-first/Owner.d.ts +9 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +9 -0
- package/dist/src/local-first/Protocol.d.ts +6 -4
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +1 -6
- package/dist/src/local-first/Schema.d.ts +7 -1
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +523 -133
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +696 -256
- package/dist/src/local-first/Storage.d.ts +16 -12
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Callbacks.test.ts +20 -0
- package/src/Callbacks.ts +17 -1
- package/src/Error.ts +10 -4
- package/src/Object.test.ts +296 -0
- package/src/Object.ts +163 -0
- package/src/Resource.test.ts +20 -16
- package/src/Resource.ts +6 -20
- package/src/Sqlite.ts +7 -0
- package/src/Task.test.ts +233 -62
- package/src/Task.ts +47 -13
- package/src/Worker.ts +3 -3
- package/src/local-first/Db.ts +88 -18
- package/src/local-first/Evolu.test.ts +281 -1
- package/src/local-first/Evolu.ts +124 -46
- package/src/local-first/Owner.ts +9 -0
- package/src/local-first/Protocol.test.ts +42 -60
- package/src/local-first/Protocol.ts +7 -9
- package/src/local-first/Schema.ts +8 -2
- package/src/local-first/Shared.test.ts +2633 -646
- package/src/local-first/Shared.ts +1192 -364
- package/src/local-first/Storage.ts +18 -19
package/src/Task.ts
CHANGED
|
@@ -485,7 +485,9 @@
|
|
|
485
485
|
* ## Glossary
|
|
486
486
|
*
|
|
487
487
|
* - **Defect** — a thrown or rejected value other than {@link AbortError}, rather
|
|
488
|
-
* than a declared {@link Result} error.
|
|
488
|
+
* than a declared {@link Result} error. A `SuppressedError` nesting only
|
|
489
|
+
* AbortErrors, thrown when `await using` cleanup observes the abort a Task is
|
|
490
|
+
* propagating, is an abort, like the same cleanup in `try`/`finally`.
|
|
489
491
|
* - **Outcome** — a Fiber's settlement: resolution with the Task {@link Result},
|
|
490
492
|
* or rejection with {@link AbortError}. The original defect is reported
|
|
491
493
|
* through {@link ReportDefectDep} whether or not the Fiber is observed; the
|
|
@@ -1536,7 +1538,9 @@ export interface DisposableRun<D = unknown>
|
|
|
1536
1538
|
* Sync disposal starts cleanup without waiting and does not throw finalizer
|
|
1537
1539
|
* defects synchronously. Async disposal awaits cleanup. If a finalizer
|
|
1538
1540
|
* defects, the defect is reported once, and every async disposal call rejects
|
|
1539
|
-
* with the same already-reported {@link AbortError}.
|
|
1541
|
+
* with the same already-reported {@link AbortError}. A finalizer that rejects
|
|
1542
|
+
* with an AbortError, for example by awaiting another DisposableRun whose
|
|
1543
|
+
* finalizer defected, is not reported; async disposal rejects with it.
|
|
1540
1544
|
*
|
|
1541
1545
|
* Calling `defer` after disposal starts is a programmer error.
|
|
1542
1546
|
*
|
|
@@ -2012,10 +2016,10 @@ export const testAbortError = /*#__PURE__*/ createAbortError(testAbortReason);
|
|
|
2012
2016
|
/**
|
|
2013
2017
|
* Abort reason recorded when a defect panics the root {@link Run}.
|
|
2014
2018
|
*
|
|
2015
|
-
* A defect is a thrown or rejected value other than {@link AbortError}
|
|
2016
|
-
* Recoverable domain errors belong
|
|
2017
|
-
* failures, such as storage engine
|
|
2018
|
-
* handle, may throw or reject.
|
|
2019
|
+
* A defect is a thrown or rejected value other than {@link AbortError} or a
|
|
2020
|
+
* `SuppressedError` nesting only AbortErrors. Recoverable domain errors belong
|
|
2021
|
+
* in {@link Result}. Bugs and unrecoverable failures, such as storage engine
|
|
2022
|
+
* errors the {@link Task} cannot usefully handle, may throw or reject.
|
|
2019
2023
|
*
|
|
2020
2024
|
* {@link Run.onEvent} handler defects are different: event handlers are
|
|
2021
2025
|
* monitoring code, so their defects are reported globally but do not panic the
|
|
@@ -2593,7 +2597,7 @@ const createRunInternal = <D extends object>(
|
|
|
2593
2597
|
try {
|
|
2594
2598
|
await finalizersToDispose.disposeAsync();
|
|
2595
2599
|
} catch (error: unknown) {
|
|
2596
|
-
finalizerAbortError = root.panic(error);
|
|
2600
|
+
finalizerAbortError = thrownToAbortError(error) ?? root.panic(error);
|
|
2597
2601
|
exit ??= err(finalizerAbortError);
|
|
2598
2602
|
}
|
|
2599
2603
|
return settle();
|
|
@@ -2738,7 +2742,7 @@ const createRunInternal = <D extends object>(
|
|
|
2738
2742
|
|
|
2739
2743
|
exit = ok(result as UnknownResult);
|
|
2740
2744
|
} catch (error: unknown) {
|
|
2741
|
-
exit = err(
|
|
2745
|
+
exit = err(thrownToAbortError(error) ?? root.panic(error));
|
|
2742
2746
|
}
|
|
2743
2747
|
|
|
2744
2748
|
// Internal Child Run disposal cannot reject; finalizer defects become Err
|
|
@@ -2893,7 +2897,7 @@ const createRunInternal = <D extends object>(
|
|
|
2893
2897
|
|
|
2894
2898
|
run[Symbol.asyncDispose] = async (): Promise<void> => {
|
|
2895
2899
|
await dispose();
|
|
2896
|
-
// oxlint-disable-next-line typescript/only-throw-error -- AbortError is Task abort control flow; rethrowing
|
|
2900
|
+
// oxlint-disable-next-line typescript/only-throw-error -- AbortError is Task abort control flow; rethrowing the finalizer AbortError avoids reporting a defect twice in Task code.
|
|
2897
2901
|
if (finalizerAbortError) throw finalizerAbortError;
|
|
2898
2902
|
};
|
|
2899
2903
|
|
|
@@ -2906,6 +2910,40 @@ const createRunInternal = <D extends object>(
|
|
|
2906
2910
|
return run;
|
|
2907
2911
|
};
|
|
2908
2912
|
|
|
2913
|
+
// Cleanup that observes the abort a Task is propagating, for example a
|
|
2914
|
+
// DisposableRun whose finalizer defected rejecting with its already-reported
|
|
2915
|
+
// AbortError, makes `await using` throw a SuppressedError. When every nested
|
|
2916
|
+
// value is an AbortError, it is abort control flow like the same cleanup in
|
|
2917
|
+
// try/finally, so the latest error wins: `error`, the last cleanup failure.
|
|
2918
|
+
const thrownToAbortError = (thrown: unknown): AbortError | null => {
|
|
2919
|
+
// Nothing here may throw out of runTask's catch. Inspecting the thrown value
|
|
2920
|
+
// can throw, for example from a getter, so the value is then a defect.
|
|
2921
|
+
try {
|
|
2922
|
+
// A loop, not recursion: each failing cleanup nests one more
|
|
2923
|
+
// SuppressedError, and a deep chain would overflow the stack.
|
|
2924
|
+
const values: Array<unknown> = [thrown];
|
|
2925
|
+
while (values.length > 0) {
|
|
2926
|
+
const value = values.pop();
|
|
2927
|
+
if (isSuppressedError(value)) values.push(value.error, value.suppressed);
|
|
2928
|
+
else if (!AbortError.is(value)) return null;
|
|
2929
|
+
}
|
|
2930
|
+
let latest = thrown;
|
|
2931
|
+
while (isSuppressedError(latest)) latest = latest.error;
|
|
2932
|
+
return latest as AbortError;
|
|
2933
|
+
} catch {
|
|
2934
|
+
return null;
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2937
|
+
|
|
2938
|
+
// The internal tag survives crossing realms, such as from an iframe, where
|
|
2939
|
+
// instanceof fails. The name also matches TypeScript's downleveled `using`,
|
|
2940
|
+
// which captures SuppressedError when a module evaluates, before
|
|
2941
|
+
// installPolyfills, and without a native one throws a plain Error named
|
|
2942
|
+
// SuppressedError.
|
|
2943
|
+
const isSuppressedError = (value: unknown): value is SuppressedError =>
|
|
2944
|
+
Object.prototype.toString.call(value) === "[object Error]" &&
|
|
2945
|
+
(value as Error).name === "SuppressedError";
|
|
2946
|
+
|
|
2909
2947
|
const withTaskMeta =
|
|
2910
2948
|
(meta: TaskMeta) =>
|
|
2911
2949
|
<T, E, D>(task: Task<T, E, D>): Task<T, E, D> => {
|
|
@@ -5713,9 +5751,6 @@ export interface SemaphoreSnapshot {
|
|
|
5713
5751
|
* is `0` until enough permits are released.
|
|
5714
5752
|
*/
|
|
5715
5753
|
readonly available: NonNegativeInt;
|
|
5716
|
-
|
|
5717
|
-
/** Whether no permits are held and no requests are queued. */
|
|
5718
|
-
readonly isIdle: boolean;
|
|
5719
5754
|
}
|
|
5720
5755
|
|
|
5721
5756
|
/**
|
|
@@ -5898,7 +5933,6 @@ export const createSemaphore = (
|
|
|
5898
5933
|
available: NonNegativeInt.orThrow(
|
|
5899
5934
|
!isGreedy && waiters.size > 0 ? 0 : Math.max(0, permits - taken),
|
|
5900
5935
|
),
|
|
5901
|
-
isIdle: isIdle(),
|
|
5902
5936
|
}),
|
|
5903
5937
|
isIdle,
|
|
5904
5938
|
};
|
package/src/Worker.ts
CHANGED
|
@@ -39,9 +39,9 @@ export interface SharedWorker<Input, Output = never> extends Disposable {
|
|
|
39
39
|
*
|
|
40
40
|
* Note: There is no reliable way to detect when a port is closed or
|
|
41
41
|
* disconnected. Calling `postMessage` on a disposed port does not throw — it
|
|
42
|
-
* silently fails. To detect
|
|
43
|
-
*
|
|
44
|
-
* timeout.
|
|
42
|
+
* silently fails. To detect that the other end is gone, let it hold a Web Lock
|
|
43
|
+
* for its lifetime and request the same lock: the browser grants it once the
|
|
44
|
+
* holder releases it or its context closes, with no timeout to tune.
|
|
45
45
|
*
|
|
46
46
|
* @group Core
|
|
47
47
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
|
package/src/local-first/Db.ts
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
import {
|
|
41
41
|
appendToArray,
|
|
42
42
|
firstInArray,
|
|
43
|
+
isNonEmptyArray,
|
|
43
44
|
type NonEmptyArray,
|
|
44
45
|
type NonEmptyReadonlyArray,
|
|
45
46
|
} from "../Array.ts";
|
|
@@ -50,12 +51,20 @@ import {
|
|
|
50
51
|
assertNotUndefined,
|
|
51
52
|
} from "../Assert.ts";
|
|
52
53
|
import type { ConsoleLevel } from "../Console.ts";
|
|
53
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
EncryptionKey,
|
|
56
|
+
type DecryptWithXChaCha20Poly1305Error,
|
|
57
|
+
type RandomBytesDep,
|
|
58
|
+
} from "../Crypto.ts";
|
|
59
|
+
import { createUnknownError } from "../Error.ts";
|
|
54
60
|
import { constFalse, constVoid } from "../Function.ts";
|
|
55
61
|
import type { LockManagerDep } from "../LockManager.ts";
|
|
56
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
acquireLeaderLock,
|
|
64
|
+
acquireLeaderLockCallback,
|
|
65
|
+
} from "../LockManager.ts";
|
|
57
66
|
import { createMutableRecord, getOwnProp, objectToEntries } from "../Object.ts";
|
|
58
|
-
import { err, getOk, ok, type Result } from "../Result.ts";
|
|
67
|
+
import { err, getOk, ok, trySync, type Result } from "../Result.ts";
|
|
59
68
|
import type {
|
|
60
69
|
CreateSqliteDriverDep,
|
|
61
70
|
SqliteDep,
|
|
@@ -107,7 +116,9 @@ import {
|
|
|
107
116
|
decryptAndDecodeDbChange,
|
|
108
117
|
encodeAndEncryptDbChange,
|
|
109
118
|
SubscriptionFlags,
|
|
119
|
+
type ProtocolInvalidDataError,
|
|
110
120
|
type ProtocolMessage,
|
|
121
|
+
type ProtocolTimestampMismatchError,
|
|
111
122
|
} from "./Protocol.ts";
|
|
112
123
|
import type { Query, RowsByQueryMap } from "./Query.ts";
|
|
113
124
|
import type { MutationChange, SqliteSchemaDep } from "./Schema.ts";
|
|
@@ -124,6 +135,7 @@ import type {
|
|
|
124
135
|
DbWorkerOutput,
|
|
125
136
|
DbWorkerQueuedResponse,
|
|
126
137
|
EvoluInput,
|
|
138
|
+
SharedWorkerId,
|
|
127
139
|
} from "./Shared.ts";
|
|
128
140
|
import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.ts";
|
|
129
141
|
import {
|
|
@@ -162,6 +174,11 @@ export interface DbWorkerInit {
|
|
|
162
174
|
readonly sqliteSchema: SqliteSchema;
|
|
163
175
|
readonly encryptionKey: EncryptionKey;
|
|
164
176
|
readonly memoryOnly: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* The SharedWorker that requested this DbWorker. It leads for this ID while
|
|
179
|
+
* it runs, so the DbWorker stops serving once it can lead for it.
|
|
180
|
+
*/
|
|
181
|
+
readonly sharedWorkerId: SharedWorkerId;
|
|
165
182
|
readonly port: NativeMessagePort<DbWorkerOutput, DbWorkerInput>;
|
|
166
183
|
}
|
|
167
184
|
|
|
@@ -197,8 +214,8 @@ export interface UnsupportedDbVersionError extends Typed<"UnsupportedDbVersionEr
|
|
|
197
214
|
|
|
198
215
|
/**
|
|
199
216
|
* Starts the platform-agnostic Evolu DbWorker and owns its resources until
|
|
200
|
-
* startup is refused, the worker receives a dispose message,
|
|
201
|
-
* is aborted.
|
|
217
|
+
* startup is refused, the worker receives a dispose message, the SharedWorker
|
|
218
|
+
* that requested it ends, or its {@link Run} is aborted.
|
|
202
219
|
*/
|
|
203
220
|
export const startDbWorker =
|
|
204
221
|
(self: WorkerSelf<DbWorkerInit>): Task<void, never, DbWorkerDeps> =>
|
|
@@ -289,7 +306,7 @@ export const startDbWorker =
|
|
|
289
306
|
);
|
|
290
307
|
if (!startup.ok) {
|
|
291
308
|
// Nothing was written. Returning lets the disposer close SQLite and
|
|
292
|
-
// release the database lock, which the
|
|
309
|
+
// release the database lock, which the next DbWorker for this database
|
|
293
310
|
// waits on. The tab leader lock is unaffected.
|
|
294
311
|
port.postMessage({
|
|
295
312
|
type: "LeaderRefused",
|
|
@@ -362,19 +379,30 @@ export const startDbWorker =
|
|
|
362
379
|
clock: context.clock.get(),
|
|
363
380
|
ownerId: owner.id,
|
|
364
381
|
didWriteMessages: storage.didWriteMessages(),
|
|
382
|
+
skippedError: storage.skippedError(),
|
|
365
383
|
result,
|
|
366
384
|
},
|
|
367
385
|
});
|
|
368
386
|
return ok();
|
|
369
387
|
});
|
|
370
388
|
} else {
|
|
389
|
+
// SQLite can fail a write the app cannot prevent, such as on a
|
|
390
|
+
// full disk. The transaction has rolled back, so the mutation is
|
|
391
|
+
// answered, or every later request of this database would wait.
|
|
392
|
+
const mutation = trySync(
|
|
393
|
+
() =>
|
|
394
|
+
handleMutation({
|
|
395
|
+
...dbDeps,
|
|
396
|
+
clock: context.clock,
|
|
397
|
+
})(request.message, now),
|
|
398
|
+
createUnknownError,
|
|
399
|
+
);
|
|
371
400
|
postQueuedResponse({
|
|
372
401
|
type: "ForEvolu",
|
|
373
402
|
id: request.id,
|
|
374
|
-
message:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
})(request.message, now),
|
|
403
|
+
message: mutation.ok
|
|
404
|
+
? mutation.value
|
|
405
|
+
: { type: "MutateFailed", error: mutation.error },
|
|
378
406
|
});
|
|
379
407
|
}
|
|
380
408
|
return;
|
|
@@ -441,8 +469,17 @@ export const startDbWorker =
|
|
|
441
469
|
}
|
|
442
470
|
};
|
|
443
471
|
|
|
472
|
+
// Stops once the SharedWorker no longer leads for its ID, which the
|
|
473
|
+
// platform releases when it closes, because Firefox can lose a Dispose
|
|
474
|
+
// posted right before that.
|
|
475
|
+
const sharedWorkerEnded = acquireLeaderLockCallback(deps)(
|
|
476
|
+
initMessage.sharedWorkerId,
|
|
477
|
+
() => resolve(ok()),
|
|
478
|
+
);
|
|
479
|
+
|
|
444
480
|
return () => {
|
|
445
481
|
port.onMessage = null;
|
|
482
|
+
sharedWorkerEnded[Symbol.dispose]();
|
|
446
483
|
};
|
|
447
484
|
}),
|
|
448
485
|
);
|
|
@@ -478,8 +515,10 @@ interface WriteContext extends ClockDep {
|
|
|
478
515
|
*
|
|
479
516
|
* Local-only mutations and sync requests that do not invoke `writeMessages`
|
|
480
517
|
* skip this. Successfully processed batches in `writeMessages` call this even
|
|
481
|
-
* when every message is duplicated or quarantined
|
|
482
|
-
*
|
|
518
|
+
* when every message is duplicated or quarantined, but not when every message
|
|
519
|
+
* was skipped, because a skipped message does not affect the clock. Duplicate
|
|
520
|
+
* receipts within the drift limit can advance the clock without storing new
|
|
521
|
+
* messages.
|
|
483
522
|
*/
|
|
484
523
|
const saveClock =
|
|
485
524
|
(deps: SqliteDep) =>
|
|
@@ -873,6 +912,15 @@ interface ClientStorage extends Storage, BaseSqliteStorage {
|
|
|
873
912
|
writeContext?: WriteContext,
|
|
874
913
|
) => void;
|
|
875
914
|
readonly didWriteMessages: () => boolean;
|
|
915
|
+
/**
|
|
916
|
+
* The first error of a message that {@link Storage.writeMessages} skipped in
|
|
917
|
+
* this request, or null.
|
|
918
|
+
*/
|
|
919
|
+
readonly skippedError: () =>
|
|
920
|
+
| DecryptWithXChaCha20Poly1305Error
|
|
921
|
+
| ProtocolInvalidDataError
|
|
922
|
+
| ProtocolTimestampMismatchError
|
|
923
|
+
| null;
|
|
876
924
|
}
|
|
877
925
|
|
|
878
926
|
const createClientStorage = (
|
|
@@ -884,6 +932,7 @@ const createClientStorage = (
|
|
|
884
932
|
): ClientStorage => {
|
|
885
933
|
let encryptionKey: EncryptionKey | null = null;
|
|
886
934
|
let didWriteMessages = false;
|
|
935
|
+
let skippedError: ReturnType<ClientStorage["skippedError"]> = null;
|
|
887
936
|
let writeContext: WriteContext | undefined;
|
|
888
937
|
|
|
889
938
|
const getEncryptionKey = (): EncryptionKey => {
|
|
@@ -903,9 +952,11 @@ const createClientStorage = (
|
|
|
903
952
|
encryptionKey = nextEncryptionKey;
|
|
904
953
|
writeContext = nextWriteContext;
|
|
905
954
|
didWriteMessages = false;
|
|
955
|
+
skippedError = null;
|
|
906
956
|
},
|
|
907
957
|
|
|
908
958
|
didWriteMessages: () => didWriteMessages,
|
|
959
|
+
skippedError: () => skippedError,
|
|
909
960
|
|
|
910
961
|
// Not implemented yet.
|
|
911
962
|
validateWriteKey: constFalse,
|
|
@@ -921,20 +972,41 @@ const createClientStorage = (
|
|
|
921
972
|
const messages: Array<CrdtMessage> = [];
|
|
922
973
|
const currentEncryptionKey = getEncryptionKey();
|
|
923
974
|
|
|
975
|
+
// Each message is authenticated on its own, so one this client cannot
|
|
976
|
+
// decrypt, verify, or decode is skipped while the rest are stored. A
|
|
977
|
+
// relay can serve anything, and anyone with the owner's write key can
|
|
978
|
+
// store anything on a relay, so Evolu cannot tell who is at fault; the
|
|
979
|
+
// SharedWorker shows the skip on the route.
|
|
980
|
+
//
|
|
981
|
+
// A skipped message leaves nothing, not even its timestamp. A stored
|
|
982
|
+
// timestamp would stop this client from fetching the real change with
|
|
983
|
+
// that timestamp from another relay, and quarantine rows are synced to
|
|
984
|
+
// other relays. The relay offers the message again on each sync, so once
|
|
985
|
+
// this client is fixed, the next sync stores it.
|
|
986
|
+
//
|
|
987
|
+
// The error is not returned, because that would end the round: the
|
|
988
|
+
// relay's ranges would go unanswered, so changes it lacks might never be
|
|
989
|
+
// uploaded to it, and every round would end the same way.
|
|
924
990
|
for (const message of encryptedMessages) {
|
|
925
991
|
const change = decryptAndDecodeDbChange(message, currentEncryptionKey);
|
|
926
|
-
if (!change.ok)
|
|
992
|
+
if (!change.ok) {
|
|
993
|
+
skippedError ??= change.error;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
927
996
|
messages.push({ timestamp: message.timestamp, change: change.value });
|
|
928
997
|
}
|
|
929
998
|
|
|
999
|
+
if (!isNonEmptyArray(messages)) return ok();
|
|
1000
|
+
|
|
930
1001
|
assertNonNullable(writeContext);
|
|
931
1002
|
const { clock, now } = writeContext;
|
|
932
1003
|
let clockTimestamp = clock.get();
|
|
933
1004
|
const receive = receiveTimestamp(deps);
|
|
934
1005
|
|
|
935
|
-
// The clock is computed over every message
|
|
936
|
-
// retry with the same inputs reports the same
|
|
937
|
-
// timestamps already in the owner's set are skipped by
|
|
1006
|
+
// The clock is computed over every message that was not skipped,
|
|
1007
|
+
// duplicates included, so a retry with the same inputs reports the same
|
|
1008
|
+
// clock. Writes for timestamps already in the owner's set are skipped by
|
|
1009
|
+
// applyMessages.
|
|
938
1010
|
for (const message of messages) {
|
|
939
1011
|
const nextTimestamp = receive(clockTimestamp, message.timestamp, now);
|
|
940
1012
|
if (!nextTimestamp.ok) {
|
|
@@ -943,8 +1015,6 @@ const createClientStorage = (
|
|
|
943
1015
|
} else clockTimestamp = nextTimestamp.value;
|
|
944
1016
|
}
|
|
945
1017
|
|
|
946
|
-
assertNonEmptyReadonlyArray(messages);
|
|
947
|
-
|
|
948
1018
|
let wroteNewMessages = false;
|
|
949
1019
|
deps.sqlite.transaction(() => {
|
|
950
1020
|
wroteNewMessages = applyMessages(deps)(
|