@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/Object.ts
CHANGED
|
@@ -305,6 +305,169 @@ export const excludeProp = <T extends object, K extends keyof T>(
|
|
|
305
305
|
return rest;
|
|
306
306
|
};
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Returns `next` with every part deep-equal to the same part of `previous`
|
|
310
|
+
* replaced by that part, or `previous` itself when the two are deep-equal.
|
|
311
|
+
*
|
|
312
|
+
* It keeps the identity of data that arrives as a new copy, such as a
|
|
313
|
+
* structured clone posted between workers, so unchanged parts can be compared
|
|
314
|
+
* with `===`, and a UI updates only what changed. Plain objects, arrays, and
|
|
315
|
+
* `Uint8Array`s are compared by their contents, and any other value by
|
|
316
|
+
* identity. The contents of a plain object are the properties an object spread
|
|
317
|
+
* copies. A changed object or array is returned as a new ordinary one. It walks
|
|
318
|
+
* the data as a tree, so a part referenced from several places is compared at
|
|
319
|
+
* each place, and a part that a cycle leads back to is returned as it is in
|
|
320
|
+
* `next`.
|
|
321
|
+
*
|
|
322
|
+
* An array item is compared with the previous item at its index, so removing or
|
|
323
|
+
* inserting an item compares every item after it with a different one. When
|
|
324
|
+
* `itemToKey` returns a key for an item, such as its ID, the item is compared
|
|
325
|
+
* with the previous item of the same key instead. Keys are compared as `Map`
|
|
326
|
+
* keys are, so each should be unique in its array; an item whose key several
|
|
327
|
+
* previous items share is compared with the last of them.
|
|
328
|
+
*
|
|
329
|
+
* ### Example
|
|
330
|
+
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* import {
|
|
333
|
+
* assertNotSame,
|
|
334
|
+
* assertSame,
|
|
335
|
+
* isPlainObject,
|
|
336
|
+
* shareStructure,
|
|
337
|
+
* } from "@evolu/common";
|
|
338
|
+
*
|
|
339
|
+
* const previous = {
|
|
340
|
+
* user: { name: "Alice" },
|
|
341
|
+
* todos: [{ title: "Buy milk" }],
|
|
342
|
+
* };
|
|
343
|
+
*
|
|
344
|
+
* // An equal copy gives the previous value back.
|
|
345
|
+
* assertSame(
|
|
346
|
+
* shareStructure(previous, structuredClone(previous)),
|
|
347
|
+
* previous,
|
|
348
|
+
* );
|
|
349
|
+
*
|
|
350
|
+
* // A change keeps every unchanged part.
|
|
351
|
+
* const next = shareStructure(previous, {
|
|
352
|
+
* user: { name: "Alice" },
|
|
353
|
+
* todos: [{ title: "Buy bread" }],
|
|
354
|
+
* });
|
|
355
|
+
* assertNotSame(next, previous);
|
|
356
|
+
* assertSame(next.user, previous.user);
|
|
357
|
+
* assertNotSame(next.todos, previous.todos);
|
|
358
|
+
*
|
|
359
|
+
* // A key compares each todo with the previous todo of the same ID.
|
|
360
|
+
* const todos = [
|
|
361
|
+
* { id: 1, title: "Buy milk" },
|
|
362
|
+
* { id: 2, title: "Walk the dog" },
|
|
363
|
+
* ];
|
|
364
|
+
* const remaining = shareStructure(
|
|
365
|
+
* todos,
|
|
366
|
+
* [{ id: 2, title: "Walk the dog" }],
|
|
367
|
+
* (todo) => (isPlainObject(todo) ? todo.id : undefined),
|
|
368
|
+
* );
|
|
369
|
+
* assertSame(remaining[0], todos[1]);
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
export const shareStructure = <T>(
|
|
373
|
+
previous: T,
|
|
374
|
+
next: T,
|
|
375
|
+
itemToKey?: (item: unknown) => unknown,
|
|
376
|
+
): T => {
|
|
377
|
+
// The arrays and plain objects of `next` being compared, each with whether a
|
|
378
|
+
// cycle led back to it.
|
|
379
|
+
const isCycledByPart = new Map<object, boolean>();
|
|
380
|
+
|
|
381
|
+
const share = (previous: unknown, next: unknown): unknown => {
|
|
382
|
+
if (Object.is(previous, next)) return previous;
|
|
383
|
+
|
|
384
|
+
if (Array.isArray(previous) && Array.isArray(next)) {
|
|
385
|
+
const previousItems: ReadonlyArray<unknown> = previous;
|
|
386
|
+
return visit(next, () => {
|
|
387
|
+
// Array.from visits holes, which map would skip or keep as holes.
|
|
388
|
+
const previousByKey =
|
|
389
|
+
itemToKey &&
|
|
390
|
+
new Map(Array.from(previousItems, (item) => [itemToKey(item), item]));
|
|
391
|
+
let isEqual = previousItems.length === next.length;
|
|
392
|
+
const shared = Array.from(next, (item: unknown, index) => {
|
|
393
|
+
const key = itemToKey?.(item);
|
|
394
|
+
const previousItem =
|
|
395
|
+
key === undefined ? previousItems[index] : previousByKey?.get(key);
|
|
396
|
+
const sharedItem = share(previousItem, item);
|
|
397
|
+
if (!Object.is(sharedItem, previousItems[index])) isEqual = false;
|
|
398
|
+
return sharedItem;
|
|
399
|
+
});
|
|
400
|
+
return isEqual ? previousItems : shared;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (isPlainObject(previous) && isPlainObject(next))
|
|
405
|
+
return visit(next, () => {
|
|
406
|
+
const keys = ownEnumerableKeys(next);
|
|
407
|
+
let isEqual = keys.length === ownEnumerableKeys(previous).length;
|
|
408
|
+
// Object.fromEntries defines each key, so `__proto__` stays own data.
|
|
409
|
+
const shared = Object.fromEntries(
|
|
410
|
+
keys.map((key) => {
|
|
411
|
+
const hasPrevious = Object.prototype.propertyIsEnumerable.call(
|
|
412
|
+
previous,
|
|
413
|
+
key,
|
|
414
|
+
);
|
|
415
|
+
const previousValue = hasPrevious
|
|
416
|
+
? (previous as Record<PropertyKey, unknown>)[key]
|
|
417
|
+
: undefined;
|
|
418
|
+
const value = share(
|
|
419
|
+
previousValue,
|
|
420
|
+
(next as Record<PropertyKey, unknown>)[key],
|
|
421
|
+
);
|
|
422
|
+
if (!hasPrevious || !Object.is(value, previousValue))
|
|
423
|
+
isEqual = false;
|
|
424
|
+
return [key, value];
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
return isEqual ? previous : shared;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// An indexed loop compares bytes far faster than `every`.
|
|
431
|
+
if (previous instanceof Uint8Array && next instanceof Uint8Array) {
|
|
432
|
+
if (previous.length !== next.length) return next;
|
|
433
|
+
for (let index = 0; index < previous.length; index++)
|
|
434
|
+
if (previous[index] !== next[index]) return next;
|
|
435
|
+
return previous;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return next;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// A cycle refers back to the part of `next` itself, so the part it leads back
|
|
442
|
+
// to is returned as it is, which keeps the cycle intact.
|
|
443
|
+
const visit = (part: object, compare: () => unknown): unknown => {
|
|
444
|
+
if (isCycledByPart.has(part)) {
|
|
445
|
+
isCycledByPart.set(part, true);
|
|
446
|
+
return part;
|
|
447
|
+
}
|
|
448
|
+
isCycledByPart.set(part, false);
|
|
449
|
+
const shared = compare();
|
|
450
|
+
const isCycled = isCycledByPart.get(part);
|
|
451
|
+
isCycledByPart.delete(part);
|
|
452
|
+
return isCycled ? part : shared;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return share(previous, next) as T;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// The keys an object spread copies: own enumerable strings and symbols.
|
|
459
|
+
const ownEnumerableKeys = (object: object): ReadonlyArray<PropertyKey> => {
|
|
460
|
+
const symbols = Object.getOwnPropertySymbols(object);
|
|
461
|
+
return symbols.length === 0
|
|
462
|
+
? Object.keys(object)
|
|
463
|
+
: [
|
|
464
|
+
...Object.keys(object),
|
|
465
|
+
...symbols.filter((symbol) =>
|
|
466
|
+
Object.prototype.propertyIsEnumerable.call(object, symbol),
|
|
467
|
+
),
|
|
468
|
+
];
|
|
469
|
+
};
|
|
470
|
+
|
|
308
471
|
/**
|
|
309
472
|
* Creates a mutable Record.
|
|
310
473
|
*
|
package/src/Resource.test.ts
CHANGED
|
@@ -77,7 +77,6 @@ const idleMutexSnapshot = {
|
|
|
77
77
|
taken: 0,
|
|
78
78
|
waiters: [],
|
|
79
79
|
available: 1,
|
|
80
|
-
isIdle: true,
|
|
81
80
|
};
|
|
82
81
|
|
|
83
82
|
describe("BorrowedResource", () => {
|
|
@@ -672,7 +671,6 @@ describe("SharedResource", () => {
|
|
|
672
671
|
);
|
|
673
672
|
|
|
674
673
|
assertEqual(sharedResource.snapshot(), {
|
|
675
|
-
isIdle: true,
|
|
676
674
|
leaseCount: 0,
|
|
677
675
|
hasResource: false,
|
|
678
676
|
idleDisposePending: false,
|
|
@@ -682,7 +680,6 @@ describe("SharedResource", () => {
|
|
|
682
680
|
const lease = await run.ok(sharedResource.acquire);
|
|
683
681
|
|
|
684
682
|
assertEqual(sharedResource.snapshot(), {
|
|
685
|
-
isIdle: false,
|
|
686
683
|
leaseCount: 1,
|
|
687
684
|
hasResource: true,
|
|
688
685
|
idleDisposePending: false,
|
|
@@ -692,7 +689,6 @@ describe("SharedResource", () => {
|
|
|
692
689
|
lease.release();
|
|
693
690
|
|
|
694
691
|
assertEqual(sharedResource.snapshot(), {
|
|
695
|
-
isIdle: false,
|
|
696
692
|
leaseCount: 0,
|
|
697
693
|
hasResource: true,
|
|
698
694
|
idleDisposePending: true,
|
|
@@ -713,7 +709,6 @@ describe("SharedResource", () => {
|
|
|
713
709
|
lease.release();
|
|
714
710
|
|
|
715
711
|
assertEqual(sharedResource.snapshot(), {
|
|
716
|
-
isIdle: false,
|
|
717
712
|
leaseCount: 0,
|
|
718
713
|
hasResource: true,
|
|
719
714
|
idleDisposePending: false,
|
|
@@ -723,14 +718,13 @@ describe("SharedResource", () => {
|
|
|
723
718
|
taken: 1,
|
|
724
719
|
waiters: [],
|
|
725
720
|
available: 0,
|
|
726
|
-
isIdle: false,
|
|
727
721
|
},
|
|
728
722
|
});
|
|
729
723
|
|
|
730
724
|
await disposed;
|
|
731
725
|
});
|
|
732
726
|
|
|
733
|
-
it("reports
|
|
727
|
+
it("reports the held mutex while creating the first resource", async () => {
|
|
734
728
|
await using run = testCreateRun();
|
|
735
729
|
const gate = createGate();
|
|
736
730
|
|
|
@@ -742,11 +736,22 @@ describe("SharedResource", () => {
|
|
|
742
736
|
);
|
|
743
737
|
|
|
744
738
|
const acquire = run.ok(sharedResource.acquire);
|
|
745
|
-
const
|
|
739
|
+
const snapshotWhileCreating = sharedResource.snapshot();
|
|
746
740
|
gate.open();
|
|
747
741
|
await acquire;
|
|
748
742
|
|
|
749
|
-
|
|
743
|
+
assertEqual(snapshotWhileCreating, {
|
|
744
|
+
leaseCount: 0,
|
|
745
|
+
hasResource: false,
|
|
746
|
+
idleDisposePending: false,
|
|
747
|
+
mutex: {
|
|
748
|
+
policy: "fifo",
|
|
749
|
+
permits: 1,
|
|
750
|
+
taken: 1,
|
|
751
|
+
waiters: [],
|
|
752
|
+
available: 0,
|
|
753
|
+
},
|
|
754
|
+
});
|
|
750
755
|
});
|
|
751
756
|
|
|
752
757
|
it("reports mutex waiters during a contended acquire", async () => {
|
|
@@ -953,9 +958,12 @@ describe("SharedResource", () => {
|
|
|
953
958
|
await sharedResource[Symbol.asyncDispose]();
|
|
954
959
|
|
|
955
960
|
assertEqual(resources.getDisposeCount(), 1);
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
961
|
+
assertEqual(sharedResource.snapshot(), {
|
|
962
|
+
leaseCount: 0,
|
|
963
|
+
hasResource: false,
|
|
964
|
+
idleDisposePending: false,
|
|
965
|
+
mutex: idleMutexSnapshot,
|
|
966
|
+
});
|
|
959
967
|
});
|
|
960
968
|
|
|
961
969
|
it("awaits async resource disposal", async () => {
|
|
@@ -2774,7 +2782,6 @@ describe("SharedResourceByKey", () => {
|
|
|
2774
2782
|
[
|
|
2775
2783
|
"a",
|
|
2776
2784
|
{
|
|
2777
|
-
isIdle: false,
|
|
2778
2785
|
leaseCount: 2,
|
|
2779
2786
|
hasResource: true,
|
|
2780
2787
|
idleDisposePending: false,
|
|
@@ -2784,7 +2791,6 @@ describe("SharedResourceByKey", () => {
|
|
|
2784
2791
|
[
|
|
2785
2792
|
"b",
|
|
2786
2793
|
{
|
|
2787
|
-
isIdle: false,
|
|
2788
2794
|
leaseCount: 1,
|
|
2789
2795
|
hasResource: true,
|
|
2790
2796
|
idleDisposePending: false,
|
|
@@ -5412,7 +5418,6 @@ describe("SharedResourceByKeyWithClaims", () => {
|
|
|
5412
5418
|
[
|
|
5413
5419
|
"a",
|
|
5414
5420
|
{
|
|
5415
|
-
isIdle: false,
|
|
5416
5421
|
leaseCount: 3,
|
|
5417
5422
|
hasResource: true,
|
|
5418
5423
|
idleDisposePending: false,
|
|
@@ -5422,7 +5427,6 @@ describe("SharedResourceByKeyWithClaims", () => {
|
|
|
5422
5427
|
[
|
|
5423
5428
|
"b",
|
|
5424
5429
|
{
|
|
5425
|
-
isIdle: false,
|
|
5426
5430
|
leaseCount: 1,
|
|
5427
5431
|
hasResource: true,
|
|
5428
5432
|
idleDisposePending: false,
|
package/src/Resource.ts
CHANGED
|
@@ -279,12 +279,6 @@ export interface SharedResource<T extends Resource> extends AsyncDisposable {
|
|
|
279
279
|
|
|
280
280
|
/** Snapshot returned by {@link SharedResource.snapshot}. */
|
|
281
281
|
export interface SharedResourceSnapshot {
|
|
282
|
-
/**
|
|
283
|
-
* Whether the resource has no current value, no leases, no pending idle
|
|
284
|
-
* disposal, and no acquisition in progress.
|
|
285
|
-
*/
|
|
286
|
-
readonly isIdle: boolean;
|
|
287
|
-
|
|
288
282
|
/** Current active lease count. */
|
|
289
283
|
readonly leaseCount: NonNegativeInt;
|
|
290
284
|
|
|
@@ -498,20 +492,12 @@ export const createSharedResource =
|
|
|
498
492
|
return await run(callback(lease.resource, lease.created));
|
|
499
493
|
},
|
|
500
494
|
|
|
501
|
-
snapshot: () => {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
!idleDisposeFiber &&
|
|
508
|
-
mutexSnapshot.isIdle,
|
|
509
|
-
leaseCount: NonNegativeInt.orThrow(heldLeases.size),
|
|
510
|
-
hasResource: current !== undefined,
|
|
511
|
-
idleDisposePending: idleDisposeFiber !== undefined,
|
|
512
|
-
mutex: mutexSnapshot,
|
|
513
|
-
};
|
|
514
|
-
},
|
|
495
|
+
snapshot: () => ({
|
|
496
|
+
leaseCount: NonNegativeInt.orThrow(heldLeases.size),
|
|
497
|
+
hasResource: current !== undefined,
|
|
498
|
+
idleDisposePending: idleDisposeFiber !== undefined,
|
|
499
|
+
mutex: mutex.snapshot(),
|
|
500
|
+
}),
|
|
515
501
|
|
|
516
502
|
[Symbol.asyncDispose]: () => sharedResourceRun[Symbol.asyncDispose](),
|
|
517
503
|
};
|
package/src/Sqlite.ts
CHANGED
|
@@ -375,6 +375,13 @@ export const createSqlite =
|
|
|
375
375
|
console.debug("begin");
|
|
376
376
|
driver.exec(sql`begin;`);
|
|
377
377
|
|
|
378
|
+
// After errors such as SQLITE_FULL or SQLITE_IOERR, SQLite may roll
|
|
379
|
+
// back the transaction itself, and this rollback then fails with
|
|
380
|
+
// "no transaction is active". Disposal wraps both in a
|
|
381
|
+
// SuppressedError whose `suppressed` holds the original error. The
|
|
382
|
+
// data is consistent and nothing is lost.
|
|
383
|
+
// TODO: Rethink this when Evolu detects SQLITE_FULL, which needs
|
|
384
|
+
// the original error, not the failed rollback.
|
|
378
385
|
using rollback = new DisposableStack();
|
|
379
386
|
let shouldRollback = true;
|
|
380
387
|
rollback.defer(() => {
|