@evolu/common 8.12.0 → 8.14.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.
Files changed (59) hide show
  1. package/dist/src/Callbacks.d.ts +12 -1
  2. package/dist/src/Callbacks.d.ts.map +1 -1
  3. package/dist/src/Callbacks.js +3 -0
  4. package/dist/src/Error.d.ts +10 -4
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +10 -4
  7. package/dist/src/Object.d.ts +65 -0
  8. package/dist/src/Object.d.ts.map +1 -1
  9. package/dist/src/Object.js +142 -0
  10. package/dist/src/Resource.d.ts +0 -5
  11. package/dist/src/Resource.d.ts.map +1 -1
  12. package/dist/src/Resource.js +6 -13
  13. package/dist/src/Sqlite.d.ts.map +1 -1
  14. package/dist/src/Sqlite.js +7 -0
  15. package/dist/src/Task.d.ts +10 -8
  16. package/dist/src/Task.d.ts.map +1 -1
  17. package/dist/src/Task.js +41 -5
  18. package/dist/src/Worker.d.ts +3 -3
  19. package/dist/src/local-first/Db.d.ts +8 -3
  20. package/dist/src/local-first/Db.d.ts.map +1 -1
  21. package/dist/src/local-first/Db.js +56 -19
  22. package/dist/src/local-first/Evolu.d.ts +147 -39
  23. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  24. package/dist/src/local-first/Evolu.js +53 -9
  25. package/dist/src/local-first/Owner.d.ts +9 -0
  26. package/dist/src/local-first/Owner.d.ts.map +1 -1
  27. package/dist/src/local-first/Owner.js +9 -0
  28. package/dist/src/local-first/Protocol.d.ts +6 -4
  29. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  30. package/dist/src/local-first/Protocol.js +1 -6
  31. package/dist/src/local-first/Schema.d.ts +7 -1
  32. package/dist/src/local-first/Schema.d.ts.map +1 -1
  33. package/dist/src/local-first/Shared.d.ts +550 -153
  34. package/dist/src/local-first/Shared.d.ts.map +1 -1
  35. package/dist/src/local-first/Shared.js +724 -273
  36. package/dist/src/local-first/Storage.d.ts +16 -12
  37. package/dist/src/local-first/Storage.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/Callbacks.test.ts +20 -0
  40. package/src/Callbacks.ts +17 -1
  41. package/src/Error.ts +10 -4
  42. package/src/Object.test.ts +296 -0
  43. package/src/Object.ts +163 -0
  44. package/src/Resource.test.ts +20 -16
  45. package/src/Resource.ts +6 -20
  46. package/src/Sqlite.ts +7 -0
  47. package/src/Task.test.ts +233 -62
  48. package/src/Task.ts +47 -13
  49. package/src/Worker.ts +3 -3
  50. package/src/local-first/Db.ts +88 -18
  51. package/src/local-first/Evolu.test.ts +381 -11
  52. package/src/local-first/Evolu.ts +219 -51
  53. package/src/local-first/Owner.ts +9 -0
  54. package/src/local-first/Protocol.test.ts +42 -60
  55. package/src/local-first/Protocol.ts +7 -9
  56. package/src/local-first/Schema.ts +8 -2
  57. package/src/local-first/Shared.test.ts +2714 -644
  58. package/src/local-first/Shared.ts +1245 -401
  59. package/src/local-first/Storage.ts +18 -19
@@ -5,7 +5,6 @@
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";
@@ -13,7 +12,6 @@ import { Boolean, Id, type InferType, NonNegativeInt, Null, type ObjectType, Str
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 can observe
40
- * a denied relay write as a `ProtocolQuotaError` by subscribing once to the
41
- * shared `EvoluErrorDep.evoluError` store returned by `createEvoluDeps`.
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
  *
@@ -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
- * Each implementation returns the members that apply to it. The built-in relay
156
- * storage currently stores opaque encrypted messages and rejects batches over
157
- * quota. The built-in client storage decrypts and validates incoming messages
158
- * before updating its clock and database tables. The contract permits quota
159
- * checks on either side.
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 = DecryptWithXChaCha20Poly1305Error | ProtocolInvalidDataError | ProtocolTimestampMismatchError | StorageQuotaError;
167
+ export type StorageWriteMessagesError = StorageQuotaError;
164
168
  /**
165
169
  * A cryptographic hash used for efficiently comparing collections of
166
170
  * {@link TimestampBytes}es.
@@ -1 +1 @@
1
- {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "8.12.0",
3
+ "version": "8.14.0",
4
4
  "description": "TypeScript library and local-first platform",
5
5
  "keywords": [
6
6
  "evolu",
@@ -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 objects cannot be directly structured-cloned (for worker messaging) or
35
- * JSON-serialized because their properties (`message`, `stack`, `cause`) are
36
- * non-enumerable. This function extracts those properties into a plain object.
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> => {
@@ -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;