@evolu/common 8.11.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/Bytes.d.ts +39 -2
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +50 -2
- 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/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- 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 +142 -24
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +109 -8
- 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 +18 -7
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +45 -28
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +18 -3
- 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 +19 -15
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +4 -2
- package/package.json +1 -1
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- 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/index.ts +6 -1
- package/src/local-first/Db.ts +88 -18
- package/src/local-first/Evolu.test.ts +589 -2
- package/src/local-first/Evolu.ts +285 -36
- package/src/local-first/Owner.ts +9 -0
- package/src/local-first/Protocol.test.ts +59 -60
- package/src/local-first/Protocol.ts +67 -47
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.ts +20 -3
- package/src/local-first/Shared.test.ts +2633 -646
- package/src/local-first/Shared.ts +1192 -364
- package/src/local-first/Storage.ts +24 -23
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { NonEmptyReadonlyArray } from "../Array.ts";
|
|
7
7
|
import type { Brand } from "../Brand.ts";
|
|
8
|
-
import type { DecryptWithXChaCha20Poly1305Error } from "../Crypto.ts";
|
|
9
8
|
import type { RandomDep } from "../Random.ts";
|
|
10
9
|
import type { SqliteDep } from "../Sqlite.ts";
|
|
11
10
|
import type { Task } from "../Task.ts";
|
|
12
|
-
import { Boolean, Id, type InferType, NonNegativeInt, Null, type ObjectType,
|
|
11
|
+
import { Boolean, Id, type InferType, NonNegativeInt, Null, type ObjectType, String, type Typed, type TypeError, type UnionType } from "../Type.ts";
|
|
13
12
|
import type { Awaitable } from "../Types.ts";
|
|
14
13
|
import type { OwnerError, OwnerIdBytes } from "./Owner.ts";
|
|
15
14
|
import { OwnerId, OwnerWriteKey } from "./Owner.ts";
|
|
16
|
-
import type { ProtocolInvalidDataError, ProtocolTimestampMismatchError } from "./Protocol.ts";
|
|
17
15
|
import { Timestamp, TimestampBytes } from "./Timestamp.ts";
|
|
18
16
|
/**
|
|
19
17
|
* Configuration for {@link Storage}, such as quota checks.
|
|
@@ -36,9 +34,10 @@ export interface StorageConfig {
|
|
|
36
34
|
* The callback returns a boolean rather than an error because error handling
|
|
37
35
|
* and logging are the responsibility of the callback implementation.
|
|
38
36
|
*
|
|
39
|
-
* Relay deployments configure this callback. Client applications
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* Relay deployments configure this callback. Client applications observe a
|
|
38
|
+
* denied relay write as a {@link ProtocolQuotaError} in the `failure` of that
|
|
39
|
+
* relay's route, which {@link syncStateToOwnerSyncStatus} reports as the
|
|
40
|
+
* owner's `Error` status.
|
|
42
41
|
*
|
|
43
42
|
* ### Example
|
|
44
43
|
*
|
|
@@ -71,7 +70,7 @@ export interface StorageConfig {
|
|
|
71
70
|
* );
|
|
72
71
|
* ```
|
|
73
72
|
*/
|
|
74
|
-
readonly isOwnerWithinQuota: (ownerId: OwnerId, requiredBytes:
|
|
73
|
+
readonly isOwnerWithinQuota: (ownerId: OwnerId, requiredBytes: NonNegativeInt) => Awaitable<boolean>;
|
|
75
74
|
}
|
|
76
75
|
/**
|
|
77
76
|
* Replica storage used by Evolu's synchronization protocol.
|
|
@@ -123,7 +122,10 @@ export interface Storage {
|
|
|
123
122
|
* Write encrypted {@link CrdtMessage}s to storage.
|
|
124
123
|
*
|
|
125
124
|
* Stores none of the messages and returns the cause when the batch cannot be
|
|
126
|
-
* accepted.
|
|
125
|
+
* accepted. A client storage that cannot decrypt, verify, or decode a message
|
|
126
|
+
* skips it and stores the rest, as the built-in client storage does, because
|
|
127
|
+
* the peer offers the message again in every round, and rejecting the batch
|
|
128
|
+
* would end each round.
|
|
127
129
|
*
|
|
128
130
|
* Must use a mutex per ownerId to ensure sequential processing and proper
|
|
129
131
|
* protocol logic handling during sync operations.
|
|
@@ -152,15 +154,17 @@ export interface StorageQuotaError extends OwnerError, Typed<"StorageQuotaError"
|
|
|
152
154
|
/**
|
|
153
155
|
* Expected reasons why {@link Storage.writeMessages} stored none of a batch.
|
|
154
156
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
157
|
+
* The built-in relay storage stores opaque encrypted messages and rejects
|
|
158
|
+
* batches over quota. The built-in client storage decrypts and validates
|
|
159
|
+
* incoming messages before updating its clock and database tables. It skips a
|
|
160
|
+
* message that fails instead of rejecting its batch, and sync state shows it as
|
|
161
|
+
* the route's `skippedError` without ending the sync round, so a message is
|
|
162
|
+
* never a reason to reject a batch. The contract permits quota checks on either
|
|
163
|
+
* side.
|
|
160
164
|
*
|
|
161
165
|
* @group Core
|
|
162
166
|
*/
|
|
163
|
-
export type StorageWriteMessagesError =
|
|
167
|
+
export type StorageWriteMessagesError = StorageQuotaError;
|
|
164
168
|
/**
|
|
165
169
|
* A cryptographic hash used for efficiently comparing collections of
|
|
166
170
|
* {@link TimestampBytes}es.
|
|
@@ -465,5 +469,5 @@ export declare const readOwnerUsageOrDefault: (deps: SqliteDep) => (ownerIdBytes
|
|
|
465
469
|
*
|
|
466
470
|
* @group SQLite
|
|
467
471
|
*/
|
|
468
|
-
export declare const updateOwnerUsage: (deps: SqliteDep) => (ownerIdBytes: OwnerIdBytes, storedBytes:
|
|
472
|
+
export declare const updateOwnerUsage: (deps: SqliteDep) => (ownerIdBytes: OwnerIdBytes, storedBytes: NonNegativeInt, firstTimestamp: TimestampBytes, lastTimestamp: TimestampBytes) => void;
|
|
469
473
|
//# sourceMappingURL=Storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
7
7
|
import { firstInArray, isNonEmptyArray } from "../Array.js";
|
|
8
8
|
import { assert, assertNonNullable } from "../Assert.js";
|
|
9
|
-
import {
|
|
9
|
+
import { concatByteArrays } from "../Bytes.js";
|
|
10
10
|
import { decrement } from "../Number.js";
|
|
11
11
|
import { err, ok } from "../Result.js";
|
|
12
12
|
import { sql, SqliteValue } from "../Sqlite.js";
|
|
@@ -116,7 +116,9 @@ export const createBaseSqliteStorage = (deps) => ({
|
|
|
116
116
|
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
117
117
|
},
|
|
118
118
|
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
119
|
-
|
|
119
|
+
// A batch can hold hundreds of thousands of timestamps, too many to spread
|
|
120
|
+
// into concatBytes.
|
|
121
|
+
const concatenatedTimestamps = concatByteArrays(timestampsBytes);
|
|
120
122
|
const result = deps.sqlite.exec(sql `
|
|
121
123
|
with recursive
|
|
122
124
|
split_timestamps(timestampBytes, pos) as (
|
package/package.json
CHANGED
package/src/Bytes.test.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
ByteSizeLiteralTiB,
|
|
24
24
|
byteSizeToByteLength,
|
|
25
25
|
type ByteSize,
|
|
26
|
+
concatByteArrays,
|
|
26
27
|
createBuffer,
|
|
27
28
|
createRunLengthEncoder,
|
|
28
29
|
decodeFlags,
|
|
@@ -96,6 +97,32 @@ describe("BufferError", () => {
|
|
|
96
97
|
});
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
describe("concatByteArrays", () => {
|
|
101
|
+
it("copies arrays into one", () => {
|
|
102
|
+
const first = new Uint8Array([1]);
|
|
103
|
+
const result = concatByteArrays([
|
|
104
|
+
first,
|
|
105
|
+
new Uint8Array(),
|
|
106
|
+
new Uint8Array([2, 3]),
|
|
107
|
+
]);
|
|
108
|
+
assertEqual(result, new Uint8Array([1, 2, 3]));
|
|
109
|
+
assertEqual(concatByteArrays([]), new Uint8Array());
|
|
110
|
+
|
|
111
|
+
first[0] = 9;
|
|
112
|
+
assertEqual(result[0], 1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("copies more arrays than a call can spread", () => {
|
|
116
|
+
const arrays = Array.from(
|
|
117
|
+
{ length: 200_000 },
|
|
118
|
+
(_, index) => new Uint8Array([index % 256]),
|
|
119
|
+
);
|
|
120
|
+
const result = concatByteArrays(arrays);
|
|
121
|
+
assertEqual(result.length, 200_000);
|
|
122
|
+
assertEqual(result[199_999], 199_999 % 256);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
99
126
|
describe("Buffer", () => {
|
|
100
127
|
it("extends, shifts, and resets bytes", () => {
|
|
101
128
|
const buffer = createBuffer();
|
package/src/Bytes.ts
CHANGED
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
* @module
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
bytesToUtf8,
|
|
25
|
+
concatBytes as nobleConcatBytes,
|
|
26
|
+
utf8ToBytes,
|
|
27
|
+
} from "@noble/ciphers/utils.js";
|
|
24
28
|
import { assert } from "./Assert.ts";
|
|
25
29
|
import { err, ok, type Result } from "./Result.ts";
|
|
26
30
|
import { safelyStringifyUnknownValue } from "./String.ts";
|
|
@@ -44,9 +48,61 @@ import {
|
|
|
44
48
|
union,
|
|
45
49
|
type UnionError,
|
|
46
50
|
} from "./Type.ts";
|
|
47
|
-
export { bytesToHex,
|
|
51
|
+
export { bytesToHex, hexToBytes } from "@noble/ciphers/utils.js";
|
|
48
52
|
export { bytesToUtf8, utf8ToBytes };
|
|
49
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Copies several Uint8Arrays into one.
|
|
56
|
+
*
|
|
57
|
+
* Each array is a separate argument, so it is meant for a few arrays. Spreading
|
|
58
|
+
* many arrays into it overflows the call stack; use {@link concatByteArrays}
|
|
59
|
+
* when their count is not bounded.
|
|
60
|
+
*
|
|
61
|
+
* ### Example
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { assertEqual, concatBytes } from "@evolu/common";
|
|
65
|
+
*
|
|
66
|
+
* assertEqual(
|
|
67
|
+
* concatBytes(new Uint8Array([1]), new Uint8Array([2, 3])),
|
|
68
|
+
* new Uint8Array([1, 2, 3]),
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export const concatBytes: typeof nobleConcatBytes = nobleConcatBytes;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Copies an array of Uint8Arrays into one.
|
|
76
|
+
*
|
|
77
|
+
* Unlike {@link concatBytes}, it takes the arrays as one array, so it works for
|
|
78
|
+
* any number of them, such as the timestamps of a batch received from a peer.
|
|
79
|
+
*
|
|
80
|
+
* ### Example
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { assertEqual, concatByteArrays } from "@evolu/common";
|
|
84
|
+
*
|
|
85
|
+
* assertEqual(
|
|
86
|
+
* concatByteArrays([new Uint8Array([1]), new Uint8Array([2, 3])]),
|
|
87
|
+
* new Uint8Array([1, 2, 3]),
|
|
88
|
+
* );
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export const concatByteArrays = (
|
|
92
|
+
arrays: ReadonlyArray<Uint8Array>,
|
|
93
|
+
): Uint8Array => {
|
|
94
|
+
let length = 0;
|
|
95
|
+
for (const array of arrays) length += array.length;
|
|
96
|
+
|
|
97
|
+
const result = new Uint8Array(length);
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const array of arrays) {
|
|
100
|
+
result.set(array, offset);
|
|
101
|
+
offset += array.length;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
|
|
50
106
|
/**
|
|
51
107
|
* Custom error for {@link Buffer}-related failures like premature end of data.
|
|
52
108
|
* Provides better stack traces for debugging binary protocol issues.
|
package/src/Callbacks.test.ts
CHANGED
|
@@ -64,3 +64,23 @@ test("Callbacks dispose clears pending callbacks", () => {
|
|
|
64
64
|
|
|
65
65
|
assertFalse(called);
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
test("Callbacks unregister removes a callback without executing it", () => {
|
|
69
|
+
const deps = testCreateDeps();
|
|
70
|
+
const callbacks = createCallbacks(deps);
|
|
71
|
+
|
|
72
|
+
let calls = 0;
|
|
73
|
+
const unregisteredId = callbacks.register(() => {
|
|
74
|
+
calls++;
|
|
75
|
+
});
|
|
76
|
+
const keptId = callbacks.register(() => {
|
|
77
|
+
calls += 10;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
callbacks.unregister(unregisteredId);
|
|
81
|
+
callbacks.execute(unregisteredId);
|
|
82
|
+
assertSame(calls, 0);
|
|
83
|
+
|
|
84
|
+
callbacks.execute(keptId);
|
|
85
|
+
assertSame(calls, 10);
|
|
86
|
+
});
|
package/src/Callbacks.ts
CHANGED
|
@@ -13,7 +13,8 @@ import type { Callback } from "./Types.ts";
|
|
|
13
13
|
* Request-response correlation for callbacks across boundaries.
|
|
14
14
|
*
|
|
15
15
|
* Stores callbacks with unique IDs and executes them once with an optional
|
|
16
|
-
* argument. Executed callbacks are automatically removed.
|
|
16
|
+
* argument. Executed callbacks are automatically removed. A callback whose
|
|
17
|
+
* response will never come is removed with `unregister`.
|
|
17
18
|
*
|
|
18
19
|
* This is useful for correlating asynchronous request-response operations
|
|
19
20
|
* across boundaries where callback functions cannot be passed directly (e.g.,
|
|
@@ -43,6 +44,14 @@ import type { Callback } from "./Types.ts";
|
|
|
43
44
|
* callbacks.execute(noArgumentId);
|
|
44
45
|
* assertEqual(noArgumentCalls, 1);
|
|
45
46
|
*
|
|
47
|
+
* // A callback whose response will never come
|
|
48
|
+
* const failedId = callbacks.register(() => {
|
|
49
|
+
* noArgumentCalls++;
|
|
50
|
+
* });
|
|
51
|
+
* callbacks.unregister(failedId);
|
|
52
|
+
* callbacks.execute(failedId);
|
|
53
|
+
* assertEqual(noArgumentCalls, 1);
|
|
54
|
+
*
|
|
46
55
|
* // Typed callback
|
|
47
56
|
* using stringCallbacks = createCallbacks<string>(deps);
|
|
48
57
|
* let received = "";
|
|
@@ -71,6 +80,9 @@ export interface Callbacks<T = undefined> extends Disposable {
|
|
|
71
80
|
readonly execute: T extends undefined
|
|
72
81
|
? (id: Id) => undefined
|
|
73
82
|
: (id: Id, arg: T) => undefined;
|
|
83
|
+
|
|
84
|
+
/** Removes a callback associated with the given ID without executing it. */
|
|
85
|
+
readonly unregister: (id: Id) => void;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
/** Creates a {@link Callbacks} registry for managing callbacks. */
|
|
@@ -101,6 +113,10 @@ export const createCallbacks = <T = undefined>(
|
|
|
101
113
|
|
|
102
114
|
execute,
|
|
103
115
|
|
|
116
|
+
unregister: (id) => {
|
|
117
|
+
callbackMap.delete(id);
|
|
118
|
+
},
|
|
119
|
+
|
|
104
120
|
[Symbol.dispose]: () => {
|
|
105
121
|
callbackMap.clear();
|
|
106
122
|
},
|
package/src/Error.ts
CHANGED
|
@@ -18,7 +18,9 @@ import { type InferType, typed, type TypedType, Unknown } from "./Type.ts";
|
|
|
18
18
|
* The `error` property contains error details (including `message`, `stack`,
|
|
19
19
|
* and `cause` if available), a string, or a fallback value.
|
|
20
20
|
*
|
|
21
|
-
* Use {@link createUnknownError} to create instances.
|
|
21
|
+
* Use {@link createUnknownError} to create instances. It turns an `Error` into a
|
|
22
|
+
* plain object, so the error can be posted between workers, for example in sync
|
|
23
|
+
* state, with its message and stack.
|
|
22
24
|
*/
|
|
23
25
|
export const UnknownError: TypedType<
|
|
24
26
|
"UnknownError",
|
|
@@ -31,9 +33,13 @@ export interface UnknownError extends InferType<typeof UnknownError> {}
|
|
|
31
33
|
/**
|
|
32
34
|
* Creates an {@link UnknownError} from an unknown error.
|
|
33
35
|
*
|
|
34
|
-
* Error
|
|
35
|
-
*
|
|
36
|
-
*
|
|
36
|
+
* An `Error` becomes a plain object of its own properties, such as `message`
|
|
37
|
+
* and `stack`. A `cause` that is an `Error` is converted the same way, a
|
|
38
|
+
* function is dropped, and any other property is kept as it is. An `Error`'s
|
|
39
|
+
* properties are not enumerable, so JSON and comparisons by content miss them,
|
|
40
|
+
* and a structured clone creates a new `Error`. Any other value is
|
|
41
|
+
* structured-cloned, or described by `String` when it cannot be, or as
|
|
42
|
+
* `"[Unserializable Object]"` when even that throws.
|
|
37
43
|
*/
|
|
38
44
|
export const createUnknownError = (error: unknown): UnknownError => {
|
|
39
45
|
const convertError = (err: Error): Record<string, unknown> => {
|
package/src/Object.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
assertEqual,
|
|
5
5
|
assertFalse,
|
|
6
6
|
assertNonNullable,
|
|
7
|
+
assertNotSame,
|
|
7
8
|
assertSame,
|
|
8
9
|
assertTrue,
|
|
9
10
|
} from "./Assert.ts";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
objectFrom,
|
|
26
27
|
objectFromEntries,
|
|
27
28
|
objectToEntries,
|
|
29
|
+
shareStructure,
|
|
28
30
|
} from "./Object.ts";
|
|
29
31
|
import { assertType } from "./Type.ts";
|
|
30
32
|
|
|
@@ -356,6 +358,300 @@ test("excludeProp", () => {
|
|
|
356
358
|
assertEqual(excluded, { b: 2, c: 3 });
|
|
357
359
|
});
|
|
358
360
|
|
|
361
|
+
describe("shareStructure", () => {
|
|
362
|
+
it("returns the previous value when the next one is deep-equal", () => {
|
|
363
|
+
const previous = {
|
|
364
|
+
name: "Alice",
|
|
365
|
+
tags: ["a", "b", Number.NaN],
|
|
366
|
+
nested: { items: [{ id: 1 }, { id: 2 }], missing: null },
|
|
367
|
+
count: Number.NaN,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
assertSame(shareStructure(previous, structuredClone(previous)), previous);
|
|
371
|
+
assertSame(shareStructure(1, 1), 1);
|
|
372
|
+
const nullPrototype = Object.assign(Object.create(null) as object, {
|
|
373
|
+
a: 1,
|
|
374
|
+
});
|
|
375
|
+
assertSame(shareStructure(nullPrototype, { a: 1 }), nullPrototype);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("keeps every unchanged part of a changed value", () => {
|
|
379
|
+
const previous = {
|
|
380
|
+
user: { name: "Alice" },
|
|
381
|
+
todos: [{ title: "Buy milk" }, { title: "Walk the dog" }],
|
|
382
|
+
};
|
|
383
|
+
const next = structuredClone(previous);
|
|
384
|
+
next.todos[1] = { title: "Walk the cat" };
|
|
385
|
+
|
|
386
|
+
const shared = shareStructure(previous, next);
|
|
387
|
+
assertNotSame(shared, previous);
|
|
388
|
+
assertEqual(shared, next);
|
|
389
|
+
assertSame(shared.user, previous.user);
|
|
390
|
+
assertNotSame(shared.todos, previous.todos);
|
|
391
|
+
assertSame(shared.todos[0], previous.todos[0]);
|
|
392
|
+
assertNotSame(shared.todos[1], previous.todos[1]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("treats added and removed keys and resized arrays as changes", () => {
|
|
396
|
+
const previous: Record<string, { readonly id: number }> = {
|
|
397
|
+
a: { id: 1 },
|
|
398
|
+
b: { id: 2 },
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const added = shareStructure(previous, {
|
|
402
|
+
a: { id: 1 },
|
|
403
|
+
b: { id: 2 },
|
|
404
|
+
c: { id: 3 },
|
|
405
|
+
});
|
|
406
|
+
assertNotSame(added, previous);
|
|
407
|
+
assertSame(added.a, previous.a);
|
|
408
|
+
|
|
409
|
+
const removed = shareStructure(previous, { a: { id: 1 } });
|
|
410
|
+
assertNotSame(removed, previous);
|
|
411
|
+
assertEqual(removed, { a: { id: 1 } });
|
|
412
|
+
assertSame(removed.a, previous.a);
|
|
413
|
+
|
|
414
|
+
// A key whose value is undefined is still a key.
|
|
415
|
+
const withUndefined = shareStructure<Record<string, unknown>>(
|
|
416
|
+
{ a: 1 },
|
|
417
|
+
{ a: 1, b: undefined },
|
|
418
|
+
);
|
|
419
|
+
assertEqual(Object.keys(withUndefined), ["a", "b"]);
|
|
420
|
+
// So is a renamed key with the same number of keys.
|
|
421
|
+
const renamed = shareStructure<Record<string, unknown>>(
|
|
422
|
+
{ a: undefined },
|
|
423
|
+
{ b: undefined },
|
|
424
|
+
);
|
|
425
|
+
assertEqual(Object.keys(renamed), ["b"]);
|
|
426
|
+
|
|
427
|
+
const items = [{ id: 1 }, { id: 2 }];
|
|
428
|
+
const longer = shareStructure(items, [{ id: 1 }, { id: 2 }, { id: 3 }]);
|
|
429
|
+
assertNotSame(longer, items);
|
|
430
|
+
assertSame(longer[0], items[0]);
|
|
431
|
+
const shorter = shareStructure(items, [{ id: 1 }]);
|
|
432
|
+
assertNotSame(shorter, items);
|
|
433
|
+
assertEqual(shorter, [{ id: 1 }]);
|
|
434
|
+
// An item that is undefined is still an item.
|
|
435
|
+
assertEqual(shareStructure<ReadonlyArray<unknown>>([1], [1, undefined]), [
|
|
436
|
+
1,
|
|
437
|
+
undefined,
|
|
438
|
+
]);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("returns the next value when its kind differs", () => {
|
|
442
|
+
const object = { a: 1 };
|
|
443
|
+
const array = [1];
|
|
444
|
+
const bytes = Uint8Array.of(1);
|
|
445
|
+
const pairs: ReadonlyArray<readonly [unknown, unknown]> = [
|
|
446
|
+
[object, null],
|
|
447
|
+
[null, object],
|
|
448
|
+
[array, null],
|
|
449
|
+
[null, array],
|
|
450
|
+
[array, { 0: 1 }],
|
|
451
|
+
[bytes, null],
|
|
452
|
+
[null, bytes],
|
|
453
|
+
[array, bytes],
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
for (const [previous, next] of pairs)
|
|
457
|
+
assertSame(shareStructure(previous, next), next);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("compares the properties an object spread copies", () => {
|
|
461
|
+
const hidden = Object.defineProperty({ a: 1 }, "b", { value: 2 });
|
|
462
|
+
const shared = shareStructure<Record<string, unknown>>(hidden, { b: 2 });
|
|
463
|
+
assertNotSame(shared, hidden);
|
|
464
|
+
assertEqual(shared, { b: 2 });
|
|
465
|
+
|
|
466
|
+
const symbol = Symbol("symbol");
|
|
467
|
+
const plain = { a: 1 };
|
|
468
|
+
assertSame(
|
|
469
|
+
shareStructure<object>(
|
|
470
|
+
plain,
|
|
471
|
+
Object.defineProperty({ a: 1 }, symbol, { value: 2 }),
|
|
472
|
+
),
|
|
473
|
+
plain,
|
|
474
|
+
);
|
|
475
|
+
const previous = { a: 1, [symbol]: { id: 1 } };
|
|
476
|
+
assertSame(shareStructure(previous, { ...previous }), previous);
|
|
477
|
+
const changedSymbol = shareStructure(previous, {
|
|
478
|
+
a: 1,
|
|
479
|
+
[symbol]: { id: 2 },
|
|
480
|
+
});
|
|
481
|
+
assertNotSame(changedSymbol, previous);
|
|
482
|
+
assertEqual(changedSymbol[symbol], { id: 2 });
|
|
483
|
+
const changedString = shareStructure(previous, { ...previous, a: 2 });
|
|
484
|
+
assertSame(changedString[symbol], previous[symbol]);
|
|
485
|
+
const withoutSymbol = shareStructure<object>(previous, { a: 1 });
|
|
486
|
+
assertNotSame(withoutSymbol, previous);
|
|
487
|
+
assertFalse(symbol in withoutSymbol);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("returns a part that a cycle leads back to as it is in the next value", () => {
|
|
491
|
+
interface Node {
|
|
492
|
+
readonly id: number;
|
|
493
|
+
readonly items: Array<unknown>;
|
|
494
|
+
}
|
|
495
|
+
const createValue = () => {
|
|
496
|
+
const node: Node = { id: 1, items: [] };
|
|
497
|
+
node.items.push(node);
|
|
498
|
+
return { unchanged: { id: 2 }, node };
|
|
499
|
+
};
|
|
500
|
+
const previous = createValue();
|
|
501
|
+
const next = createValue();
|
|
502
|
+
|
|
503
|
+
const shared = shareStructure(previous, next);
|
|
504
|
+
assertSame(shared.unchanged, previous.unchanged);
|
|
505
|
+
assertSame(shared.node, next.node);
|
|
506
|
+
assertSame(shared.node.items[0], shared.node);
|
|
507
|
+
assertSame(shareStructure(previous.node, next.node), next.node);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("compares a part referenced from several places at each place", () => {
|
|
511
|
+
const shared = { id: 1 };
|
|
512
|
+
const previous = { a: shared, b: shared };
|
|
513
|
+
|
|
514
|
+
assertSame(shareStructure(previous, structuredClone(previous)), previous);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("compares an array item with the previous item of the same key", () => {
|
|
518
|
+
const itemToKey = (item: unknown): unknown =>
|
|
519
|
+
isPlainObject(item) ? item.id : undefined;
|
|
520
|
+
const previous = {
|
|
521
|
+
items: [
|
|
522
|
+
{ id: 1, meta: { tag: "a" } },
|
|
523
|
+
{ id: 2, meta: { tag: "b" } },
|
|
524
|
+
{ id: 3, meta: { tag: "c" } },
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Removing an item keeps the objects of the items after it.
|
|
529
|
+
const removed = shareStructure(
|
|
530
|
+
previous,
|
|
531
|
+
{ items: [structuredClone(previous.items[1]), { id: 3, meta: null }] },
|
|
532
|
+
itemToKey,
|
|
533
|
+
);
|
|
534
|
+
assertEqual(removed, {
|
|
535
|
+
items: [
|
|
536
|
+
{ id: 2, meta: { tag: "b" } },
|
|
537
|
+
{ id: 3, meta: null },
|
|
538
|
+
],
|
|
539
|
+
});
|
|
540
|
+
assertSame(removed.items[0], previous.items[1]);
|
|
541
|
+
|
|
542
|
+
// Moved items keep their objects in a new array.
|
|
543
|
+
const moved = shareStructure(
|
|
544
|
+
previous,
|
|
545
|
+
{ items: structuredClone(previous.items).toReversed() },
|
|
546
|
+
itemToKey,
|
|
547
|
+
);
|
|
548
|
+
assertNotSame(moved.items, previous.items);
|
|
549
|
+
assertSame(moved.items[0], previous.items[2]);
|
|
550
|
+
assertSame(moved.items[2], previous.items[0]);
|
|
551
|
+
|
|
552
|
+
// A changed item keeps its unchanged parts.
|
|
553
|
+
const changed = shareStructure<ReadonlyArray<Record<string, unknown>>>(
|
|
554
|
+
previous.items,
|
|
555
|
+
[{ id: 2, meta: { tag: "b" }, done: true }],
|
|
556
|
+
itemToKey,
|
|
557
|
+
);
|
|
558
|
+
assertSame(changed[0]?.meta, previous.items[1].meta);
|
|
559
|
+
|
|
560
|
+
// An item with a new key has no previous item to share with.
|
|
561
|
+
const added = shareStructure(
|
|
562
|
+
previous.items,
|
|
563
|
+
[{ id: 4, meta: { tag: "a" } }],
|
|
564
|
+
itemToKey,
|
|
565
|
+
);
|
|
566
|
+
assertNotSame(added[0]?.meta, previous.items[0].meta);
|
|
567
|
+
|
|
568
|
+
// An item without a key is compared with the item at its index.
|
|
569
|
+
const unkeyed = [{ tag: "a" }, { tag: "b" }];
|
|
570
|
+
assertSame(
|
|
571
|
+
shareStructure(unkeyed, structuredClone(unkeyed), itemToKey),
|
|
572
|
+
unkeyed,
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("compares Uint8Arrays by their bytes", () => {
|
|
577
|
+
const previous = { blob: Uint8Array.of(1, 2, 3) };
|
|
578
|
+
|
|
579
|
+
assertSame(shareStructure(previous, structuredClone(previous)), previous);
|
|
580
|
+
const changed = shareStructure(previous, { blob: Uint8Array.of(1, 2, 4) });
|
|
581
|
+
assertNotSame(changed, previous);
|
|
582
|
+
assertEqual(changed.blob, Uint8Array.of(1, 2, 4));
|
|
583
|
+
assertNotSame(
|
|
584
|
+
shareStructure(previous, { blob: Uint8Array.of(1, 2) }),
|
|
585
|
+
previous,
|
|
586
|
+
);
|
|
587
|
+
assertNotSame(
|
|
588
|
+
shareStructure(previous, { blob: Uint8Array.of(1, 2, 3, 4) }),
|
|
589
|
+
previous,
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("compares a hole in an array as a missing item", () => {
|
|
594
|
+
const previous = [1, 2, 3];
|
|
595
|
+
// oxlint-disable-next-line no-sparse-arrays -- The hole is what is tested.
|
|
596
|
+
const next = [1, , 3];
|
|
597
|
+
|
|
598
|
+
const shared = shareStructure<ReadonlyArray<number | undefined>>(
|
|
599
|
+
previous,
|
|
600
|
+
next,
|
|
601
|
+
);
|
|
602
|
+
assertNotSame(shared, previous);
|
|
603
|
+
assertSame(shared[1], undefined);
|
|
604
|
+
|
|
605
|
+
// A hole in a keyed previous array has no key.
|
|
606
|
+
// oxlint-disable-next-line no-sparse-arrays -- The hole is what is tested.
|
|
607
|
+
const sparse = [, { id: 1 }];
|
|
608
|
+
const keyed = shareStructure<ReadonlyArray<unknown>>(
|
|
609
|
+
sparse,
|
|
610
|
+
[{ id: 1 }],
|
|
611
|
+
(item) => (isPlainObject(item) ? item.id : undefined),
|
|
612
|
+
);
|
|
613
|
+
assertEqual(keyed, [{ id: 1 }]);
|
|
614
|
+
assertSame(keyed[0], sparse[1]);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("compares values other than plain objects and arrays by identity", () => {
|
|
618
|
+
const date = new Date(0);
|
|
619
|
+
assertNotSame(shareStructure(date, new Date(0)), date);
|
|
620
|
+
|
|
621
|
+
class Example {
|
|
622
|
+
readonly value = "example";
|
|
623
|
+
}
|
|
624
|
+
const example = new Example();
|
|
625
|
+
assertNotSame(shareStructure(example, new Example()), example);
|
|
626
|
+
|
|
627
|
+
assertTrue(Object.is(shareStructure(0, -0), -0));
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("keeps a __proto__ key as own data of an ordinary object", () => {
|
|
631
|
+
const next = JSON.parse(
|
|
632
|
+
'{"__proto__": {"polluted": true}, "b": 1}',
|
|
633
|
+
) as Record<string, unknown>;
|
|
634
|
+
|
|
635
|
+
const shared = shareStructure<Record<string, unknown>>({ b: 2 }, next);
|
|
636
|
+
assertSame(Object.getPrototypeOf(shared), Object.prototype);
|
|
637
|
+
assertTrue(Object.hasOwn(shared, "__proto__"));
|
|
638
|
+
assertEqual(Object.getOwnPropertyDescriptor(shared, "__proto__")?.value, {
|
|
639
|
+
polluted: true,
|
|
640
|
+
});
|
|
641
|
+
assertFalse("polluted" in {});
|
|
642
|
+
|
|
643
|
+
// An empty `__proto__` value is not mistaken for Object.prototype.
|
|
644
|
+
const empty = shareStructure<Record<string, unknown>>(
|
|
645
|
+
{},
|
|
646
|
+
JSON.parse('{"__proto__": {}}') as Record<string, unknown>,
|
|
647
|
+
);
|
|
648
|
+
assertNotSame(
|
|
649
|
+
Object.getOwnPropertyDescriptor(empty, "__proto__")?.value,
|
|
650
|
+
Object.prototype,
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
359
655
|
test("createMutableRecord", () => {
|
|
360
656
|
const values = createMutableRecord<string, number>();
|
|
361
657
|
values.__proto__ = 123;
|