@evolu/common 8.10.0 → 8.11.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 (144) hide show
  1. package/dist/src/Config.d.ts +22 -22
  2. package/dist/src/Config.d.ts.map +1 -1
  3. package/dist/src/Console.d.ts +62 -7
  4. package/dist/src/Console.d.ts.map +1 -1
  5. package/dist/src/Console.js +20 -4
  6. package/dist/src/Crypto.d.ts +76 -4
  7. package/dist/src/Crypto.d.ts.map +1 -1
  8. package/dist/src/Crypto.js +55 -4
  9. package/dist/src/Error.d.ts +45 -0
  10. package/dist/src/Error.d.ts.map +1 -1
  11. package/dist/src/Error.js +69 -0
  12. package/dist/src/Fs.d.ts +92 -18
  13. package/dist/src/Fs.d.ts.map +1 -1
  14. package/dist/src/Fs.js +2 -0
  15. package/dist/src/Identicon.d.ts +2 -2
  16. package/dist/src/Identicon.js +2 -2
  17. package/dist/src/LeakDetector.d.ts +22 -3
  18. package/dist/src/LeakDetector.d.ts.map +1 -1
  19. package/dist/src/LeakDetector.js +12 -2
  20. package/dist/src/LockManager.d.ts +8 -0
  21. package/dist/src/LockManager.d.ts.map +1 -1
  22. package/dist/src/LockManager.js +6 -0
  23. package/dist/src/Object.d.ts.map +1 -1
  24. package/dist/src/Object.js +5 -0
  25. package/dist/src/Platform.d.ts +47 -7
  26. package/dist/src/Platform.d.ts.map +1 -1
  27. package/dist/src/Platform.js +24 -5
  28. package/dist/src/Random.d.ts +25 -2
  29. package/dist/src/Random.d.ts.map +1 -1
  30. package/dist/src/Random.js +14 -2
  31. package/dist/src/Resource.d.ts +156 -1
  32. package/dist/src/Resource.d.ts.map +1 -1
  33. package/dist/src/Resource.js +201 -72
  34. package/dist/src/Schedule.d.ts +11 -10
  35. package/dist/src/Schedule.d.ts.map +1 -1
  36. package/dist/src/Schedule.js +1 -1
  37. package/dist/src/Sqlite.d.ts +132 -16
  38. package/dist/src/Sqlite.d.ts.map +1 -1
  39. package/dist/src/Sqlite.js +63 -9
  40. package/dist/src/Task.d.ts +15 -4
  41. package/dist/src/Task.d.ts.map +1 -1
  42. package/dist/src/Task.js +41 -15
  43. package/dist/src/Test.d.ts +9 -0
  44. package/dist/src/Test.d.ts.map +1 -1
  45. package/dist/src/Test.js +4 -0
  46. package/dist/src/Time.d.ts +106 -9
  47. package/dist/src/Time.d.ts.map +1 -1
  48. package/dist/src/Time.js +55 -4
  49. package/dist/src/Type.d.ts +1455 -1310
  50. package/dist/src/Type.d.ts.map +1 -1
  51. package/dist/src/Type.js +1274 -517
  52. package/dist/src/WebSocket.d.ts +164 -13
  53. package/dist/src/WebSocket.d.ts.map +1 -1
  54. package/dist/src/WebSocket.js +133 -24
  55. package/dist/src/Worker.d.ts +90 -8
  56. package/dist/src/Worker.d.ts.map +1 -1
  57. package/dist/src/Worker.js +28 -2
  58. package/dist/src/index.d.ts +6 -7
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +2 -3
  61. package/dist/src/local-first/Db.d.ts +52 -3
  62. package/dist/src/local-first/Db.d.ts.map +1 -1
  63. package/dist/src/local-first/Db.js +412 -137
  64. package/dist/src/local-first/Evolu.d.ts +336 -211
  65. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  66. package/dist/src/local-first/Evolu.js +102 -15
  67. package/dist/src/local-first/Owner.d.ts +13 -30
  68. package/dist/src/local-first/Owner.d.ts.map +1 -1
  69. package/dist/src/local-first/Owner.js +13 -30
  70. package/dist/src/local-first/Protocol.d.ts +94 -16
  71. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  72. package/dist/src/local-first/Protocol.js +118 -38
  73. package/dist/src/local-first/Query.d.ts +8 -15
  74. package/dist/src/local-first/Query.d.ts.map +1 -1
  75. package/dist/src/local-first/Schema.d.ts +335 -21
  76. package/dist/src/local-first/Schema.d.ts.map +1 -1
  77. package/dist/src/local-first/Schema.js +214 -17
  78. package/dist/src/local-first/Shared.d.ts +537 -22
  79. package/dist/src/local-first/Shared.d.ts.map +1 -1
  80. package/dist/src/local-first/Shared.js +1437 -234
  81. package/dist/src/local-first/Storage.d.ts +192 -14
  82. package/dist/src/local-first/Storage.d.ts.map +1 -1
  83. package/dist/src/local-first/Storage.js +81 -20
  84. package/dist/src/local-first/Timestamp.d.ts +392 -41
  85. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  86. package/dist/src/local-first/Timestamp.js +403 -81
  87. package/dist/src/local-first/index.d.ts +0 -1
  88. package/dist/src/local-first/index.d.ts.map +1 -1
  89. package/dist/src/local-first/index.js +0 -1
  90. package/package.json +1 -1
  91. package/src/Assert.test.ts +2 -5
  92. package/src/Config.test.ts +2 -6
  93. package/src/Config.ts +133 -133
  94. package/src/Console.ts +62 -7
  95. package/src/Crypto.ts +76 -4
  96. package/src/Eq.test.ts +2 -3
  97. package/src/Error.test.ts +76 -3
  98. package/src/Error.ts +71 -0
  99. package/src/Fs.ts +92 -18
  100. package/src/Identicon.ts +2 -2
  101. package/src/LeakDetector.ts +22 -3
  102. package/src/LockManager.ts +8 -0
  103. package/src/Object.test.ts +27 -12
  104. package/src/Object.ts +5 -0
  105. package/src/Platform.ts +50 -8
  106. package/src/Random.ts +25 -2
  107. package/src/Resource.test.ts +837 -0
  108. package/src/Resource.ts +235 -15
  109. package/src/Schedule.test.ts +50 -12
  110. package/src/Schedule.ts +24 -14
  111. package/src/Sqlite.ts +137 -17
  112. package/src/Task.test.ts +189 -8
  113. package/src/Task.ts +56 -17
  114. package/src/Test.ts +9 -0
  115. package/src/Time.ts +106 -9
  116. package/src/Type.test.ts +946 -1028
  117. package/src/Type.ts +4195 -3136
  118. package/src/Types.test.ts +4 -14
  119. package/src/WebSocket.ts +313 -40
  120. package/src/Worker.ts +90 -8
  121. package/src/index.ts +15 -6
  122. package/src/local-first/Db.ts +644 -339
  123. package/src/local-first/Evolu.test.ts +686 -21
  124. package/src/local-first/Evolu.ts +450 -228
  125. package/src/local-first/Owner.ts +13 -30
  126. package/src/local-first/Protocol.test.ts +617 -10
  127. package/src/local-first/Protocol.ts +196 -72
  128. package/src/local-first/Query.ts +8 -15
  129. package/src/local-first/Schema.test.ts +143 -0
  130. package/src/local-first/Schema.ts +363 -24
  131. package/src/local-first/Shared.test.ts +7731 -559
  132. package/src/local-first/Shared.ts +2036 -267
  133. package/src/local-first/Storage.ts +218 -32
  134. package/src/local-first/Timestamp.test.ts +344 -70
  135. package/src/local-first/Timestamp.ts +434 -118
  136. package/src/local-first/index.ts +0 -1
  137. package/dist/src/local-first/Error.d.ts +0 -12
  138. package/dist/src/local-first/Error.d.ts.map +0 -1
  139. package/dist/src/local-first/Error.js +0 -6
  140. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  141. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  142. package/dist/src/local-first/LocalAuth.js +0 -179
  143. package/src/local-first/Error.ts +0 -17
  144. package/src/local-first/LocalAuth.ts +0 -457
package/src/Error.test.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import { describe, it } from "node:test";
2
- import { assertEqual, assertFalse, assertSame, assertTrue } from "./Assert.ts";
3
-
4
- import { UnknownError, createUnknownError } from "./Error.ts";
2
+ import { runInNewContext } from "node:vm";
3
+ import {
4
+ assertEqual,
5
+ assertFalse,
6
+ assertInstanceOf,
7
+ assertSame,
8
+ assertTrue,
9
+ } from "./Assert.ts";
10
+
11
+ import { UnknownError, createUnknownError, defectToError } from "./Error.ts";
12
+ import { createRun } from "./Task.ts";
5
13
  import { assertType, Object, String } from "./Type.ts";
6
14
 
7
15
  describe("createUnknownError", () => {
@@ -116,3 +124,68 @@ describe("createUnknownError", () => {
116
124
  assertSame(actual.self, actual);
117
125
  });
118
126
  });
127
+
128
+ describe("defectToError", () => {
129
+ it("converts a panic to its Error defect", async () => {
130
+ const reported: Array<unknown> = [];
131
+ await using run = createRun({
132
+ reportDefect: (defect) => {
133
+ reported.push(defect);
134
+ },
135
+ });
136
+ const defect = new Error("boom");
137
+
138
+ run.panic(defect);
139
+
140
+ assertSame(defectToError(reported[0]), defect);
141
+ });
142
+
143
+ it("returns an Error from another realm as it is", () => {
144
+ const defect: unknown = runInNewContext(
145
+ 'new TypeError("other realm failed")',
146
+ );
147
+ assertFalse(defect instanceof Error);
148
+
149
+ assertSame(defectToError(defect), defect);
150
+ });
151
+
152
+ it("describes a DOMException with its name and message", () => {
153
+ const defect = new DOMException("dom failed", "NotFoundError");
154
+
155
+ const error = defectToError(defect);
156
+
157
+ assertEqual(error.message, "NotFoundError: dom failed");
158
+ assertSame(error.cause, defect);
159
+ });
160
+
161
+ it("describes a panic's defect that is not an Error", async () => {
162
+ const reported: Array<unknown> = [];
163
+ await using run = createRun({
164
+ reportDefect: (defect) => {
165
+ reported.push(defect);
166
+ },
167
+ });
168
+
169
+ const abortError = run.panic({ type: "UnexpectedState", count: 1 });
170
+
171
+ const error = defectToError(reported[0]);
172
+ assertInstanceOf(error, Error);
173
+ assertEqual(error.message, 'Defect: {"type":"UnexpectedState","count":1}');
174
+ assertSame(error.cause, abortError);
175
+ });
176
+
177
+ it("describes an AbortError that is not a panic", () => {
178
+ const abortError = {
179
+ type: "AbortError",
180
+ reason: { type: "OtherAbortReason" },
181
+ } as const;
182
+
183
+ const error = defectToError(abortError);
184
+
185
+ assertEqual(
186
+ error.message,
187
+ 'Defect: {"type":"AbortError","reason":{"type":"OtherAbortReason"}}',
188
+ );
189
+ assertSame(error.cause, abortError);
190
+ });
191
+ });
package/src/Error.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * @module
5
5
  */
6
6
 
7
+ import { safelyStringifyUnknownValue } from "./String.ts";
8
+ import { AbortError, type createRun, type ReportDefect } from "./Task.ts";
7
9
  import { type InferType, typed, type TypedType, Unknown } from "./Type.ts";
8
10
 
9
11
  /**
@@ -84,3 +86,72 @@ export const createUnknownError = (error: unknown): UnknownError => {
84
86
  }
85
87
  }
86
88
  };
89
+
90
+ /**
91
+ * Converts a reported defect to an `Error` that a host error reporter shows
92
+ * readably.
93
+ *
94
+ * Hosts such as browsers and React Native show a reported value that is not an
95
+ * `Error` only as text such as "[object Object]", and a worker's error reaches
96
+ * its page, including an error tracker listening there, as that text alone. A
97
+ * panic reports a plain {@link AbortError}, so its defect is converted instead.
98
+ * An `Error` is returned as it is, including one from another realm, such as an
99
+ * iframe. A `DOMException` is described in an `Error` with its name and
100
+ * message, because Chromium reports one from a worker without them. Any other
101
+ * value is described in an `Error` whose cause is what was reported.
102
+ *
103
+ * Platform {@link createRun} adapters use it for their default reporting. A
104
+ * custom {@link ReportDefect} can use it too, such as before passing a defect to
105
+ * an error tracker.
106
+ *
107
+ * ### Example
108
+ *
109
+ * ```ts
110
+ * import {
111
+ * assertEqual,
112
+ * assertSame,
113
+ * createRun,
114
+ * defectToError,
115
+ * } from "@evolu/common";
116
+ *
117
+ * const errors: Array<Error> = [];
118
+ * await using run = createRun({
119
+ * reportDefect: (reported) => {
120
+ * errors.push(defectToError(reported));
121
+ * },
122
+ * });
123
+ * const defect = new Error("boom");
124
+ *
125
+ * run.panic(defect);
126
+ *
127
+ * assertSame(errors[0], defect);
128
+ * assertEqual(
129
+ * defectToError({ type: "UnexpectedState" }).message,
130
+ * 'Defect: {"type":"UnexpectedState"}',
131
+ * );
132
+ * ```
133
+ */
134
+ export const defectToError = (reported: unknown): Error => {
135
+ const defect =
136
+ AbortError.is(reported) && reported.reason.type === "PanicAbortReason"
137
+ ? reported.reason.defect
138
+ : reported;
139
+ // The internal tag survives crossing realms, such as from an iframe, where
140
+ // instanceof fails.
141
+ const tag = Object.prototype.toString.call(defect);
142
+ // Chromium reports a DOMException from a worker without its name or message,
143
+ // so it is described in an Error.
144
+ if (tag === "[object DOMException]") {
145
+ const { name, message } = defect as Error;
146
+ return new Error(`${name}: ${message}`, { cause: defect });
147
+ }
148
+ if (defect instanceof Error || tag === "[object Error]") {
149
+ return defect as Error;
150
+ }
151
+ // A value with a cycle or a bigint falls back to String, often
152
+ // "[object Object]", and a nested Error shows as "{}". That is enough:
153
+ // Evolu's own defects are Errors, and the cause still holds the value.
154
+ return new Error(`Defect: ${safelyStringifyUnknownValue(defect)}`, {
155
+ cause: reported,
156
+ });
157
+ };
package/src/Fs.ts CHANGED
@@ -60,7 +60,11 @@ import type { ByteLength } from "./Bytes.ts";
60
60
  import type { createRun, Task } from "./Task.ts";
61
61
  import type { Typed } from "./Type.ts";
62
62
 
63
- /** Asynchronous file system operations. */
63
+ /**
64
+ * Asynchronous file system operations.
65
+ *
66
+ * @group Core
67
+ */
64
68
  export interface Fs {
65
69
  /**
66
70
  * Reads a whole file, as bytes by default or as a string with an encoding. An
@@ -163,15 +167,27 @@ export interface Fs {
163
167
  ) => Task<FsTempDirectory, FsError>;
164
168
  }
165
169
 
166
- /** Dependency wrapper for {@link Fs}. */
170
+ /**
171
+ * Dependency wrapper for {@link Fs}.
172
+ *
173
+ * @group Core
174
+ */
167
175
  export interface FsDep {
168
176
  readonly fs: Fs;
169
177
  }
170
178
 
171
- /** A file system path, or a `file:` URL. */
179
+ /**
180
+ * A file system path, or a `file:` URL.
181
+ *
182
+ * @group Core
183
+ */
172
184
  export type FsPath = string | URL;
173
185
 
174
- /** Supported text encodings. */
186
+ /**
187
+ * Supported text encodings.
188
+ *
189
+ * @group Core
190
+ */
175
191
  export type FsEncoding =
176
192
  | "ascii"
177
193
  | "utf8"
@@ -186,7 +202,11 @@ export type FsEncoding =
186
202
  | "binary"
187
203
  | "hex";
188
204
 
189
- /** Supported file opening modes for {@link Fs.writeFile}. */
205
+ /**
206
+ * Supported file opening modes for {@link Fs.writeFile}.
207
+ *
208
+ * @group Core
209
+ */
190
210
  export type FsOpenFlag =
191
211
  | "a"
192
212
  | "ax"
@@ -202,7 +222,11 @@ export type FsOpenFlag =
202
222
  | "w+"
203
223
  | "wx+";
204
224
 
205
- /** Reads bytes by default, or text when an encoding is specified. */
225
+ /**
226
+ * Reads bytes by default, or text when an encoding is specified.
227
+ *
228
+ * @group Core
229
+ */
206
230
  export interface FsReadFile {
207
231
  (path: FsPath): Task<Uint8Array, FsError>;
208
232
  (
@@ -211,7 +235,11 @@ export interface FsReadFile {
211
235
  ): Task<string, FsError>;
212
236
  }
213
237
 
214
- /** Options for {@link Fs.writeFile}. */
238
+ /**
239
+ * Options for {@link Fs.writeFile}.
240
+ *
241
+ * @group Options
242
+ */
215
243
  export interface FsWriteFileOptions {
216
244
  /** Encoding of string data. Defaults to `utf8`. */
217
245
  readonly encoding?: FsEncoding;
@@ -221,13 +249,21 @@ export interface FsWriteFileOptions {
221
249
  readonly flag?: FsOpenFlag;
222
250
  }
223
251
 
224
- /** Options for {@link Fs.readDirectory}. */
252
+ /**
253
+ * Options for {@link Fs.readDirectory}.
254
+ *
255
+ * @group Options
256
+ */
225
257
  export interface FsReadDirectoryOptions {
226
258
  /** Includes entries from nested directories. Defaults to `false`. */
227
259
  readonly recursive?: boolean;
228
260
  }
229
261
 
230
- /** Options for {@link Fs.createDirectory}. */
262
+ /**
263
+ * Options for {@link Fs.createDirectory}.
264
+ *
265
+ * @group Options
266
+ */
231
267
  export interface FsCreateDirectoryOptions {
232
268
  /** Creates missing parents and accepts an existing directory. */
233
269
  readonly recursive?: boolean;
@@ -235,7 +271,11 @@ export interface FsCreateDirectoryOptions {
235
271
  readonly mode?: number;
236
272
  }
237
273
 
238
- /** Options for {@link Fs.copy}. */
274
+ /**
275
+ * Options for {@link Fs.copy}.
276
+ *
277
+ * @group Options
278
+ */
239
279
  export interface FsCopyOptions {
240
280
  /**
241
281
  * Node's `force` option. Replaces existing files; `false` skips them unless
@@ -253,13 +293,21 @@ export interface FsCopyOptions {
253
293
  readonly preserveTimestamps?: boolean;
254
294
  }
255
295
 
256
- /** Options for {@link Fs.copyFile}. */
296
+ /**
297
+ * Options for {@link Fs.copyFile}.
298
+ *
299
+ * @group Options
300
+ */
257
301
  export interface FsCopyFileOptions {
258
302
  /** Replaces an existing destination file. Defaults to `false`. */
259
303
  readonly overwrite?: boolean;
260
304
  }
261
305
 
262
- /** Options for {@link Fs.remove}. */
306
+ /**
307
+ * Options for {@link Fs.remove}.
308
+ *
309
+ * @group Options
310
+ */
263
311
  export interface FsRemoveOptions {
264
312
  /** Removes directories and their contents. */
265
313
  readonly recursive?: boolean;
@@ -277,7 +325,11 @@ export interface FsRemoveOptions {
277
325
  readonly retryDelay?: number;
278
326
  }
279
327
 
280
- /** File metadata as data, with Node's numeric and timestamp field names. */
328
+ /**
329
+ * File metadata as data, with Node's numeric and timestamp field names.
330
+ *
331
+ * @group Core
332
+ */
281
333
  export interface FsMetadata {
282
334
  readonly type: FsEntryType;
283
335
  readonly dev: number;
@@ -300,7 +352,11 @@ export interface FsMetadata {
300
352
  readonly birthtime: Date;
301
353
  }
302
354
 
303
- /** The kind of file system entry described by {@link FsMetadata}. */
355
+ /**
356
+ * The kind of file system entry described by {@link FsMetadata}.
357
+ *
358
+ * @group Core
359
+ */
304
360
  export type FsEntryType =
305
361
  | "File"
306
362
  | "Directory"
@@ -311,7 +367,11 @@ export type FsEntryType =
311
367
  | "Socket"
312
368
  | "Unknown";
313
369
 
314
- /** Options for {@link Fs.createTempDirectory}. */
370
+ /**
371
+ * Options for {@link Fs.createTempDirectory}.
372
+ *
373
+ * @group Options
374
+ */
315
375
  export interface FsCreateTempDirectoryOptions {
316
376
  /** Existing parent directory. Defaults to the system temporary directory. */
317
377
  readonly directory?: string;
@@ -319,12 +379,20 @@ export interface FsCreateTempDirectoryOptions {
319
379
  readonly prefix?: string;
320
380
  }
321
381
 
322
- /** A temporary directory removed, with its contents, on asynchronous disposal. */
382
+ /**
383
+ * A temporary directory removed, with its contents, on asynchronous disposal.
384
+ *
385
+ * @group Core
386
+ */
323
387
  export interface FsTempDirectory extends AsyncDisposable {
324
388
  readonly path: string;
325
389
  }
326
390
 
327
- /** A failed file system operation. */
391
+ /**
392
+ * A failed file system operation.
393
+ *
394
+ * @group Errors
395
+ */
328
396
  export interface FsError extends Typed<"FsError"> {
329
397
  readonly reason: FsErrorReason;
330
398
  /**
@@ -345,7 +413,11 @@ export interface FsError extends Typed<"FsError"> {
345
413
  readonly message: string;
346
414
  }
347
415
 
348
- /** Why a file system operation failed, mapped from the platform's error code. */
416
+ /**
417
+ * Why a file system operation failed, mapped from the platform's error code.
418
+ *
419
+ * @group Errors
420
+ */
349
421
  export type FsErrorReason =
350
422
  | "NotFound"
351
423
  | "AlreadyExists"
@@ -392,6 +464,8 @@ export type FsErrorReason =
392
464
  *
393
465
  * assertOk(await run(saveMessage));
394
466
  * ```
467
+ *
468
+ * @group Testing
395
469
  */
396
470
  export const testCreateFs = (overrides: Partial<Fs> = {}): Fs => ({
397
471
  readFile: createUnexpectedFsOperation("readFile"),
package/src/Identicon.ts CHANGED
@@ -37,6 +37,7 @@ export type IdenticonStyle = "github" | "quadrant" | "gradient" | "sutnar";
37
37
  * assertTrue,
38
38
  * createIdFromString,
39
39
  * createIdenticon,
40
+ * testTodoId,
40
41
  * } from "@evolu/common";
41
42
  *
42
43
  * const id = createIdFromString("identicon-example");
@@ -47,8 +48,7 @@ export type IdenticonStyle = "github" | "quadrant" | "gradient" | "sutnar";
47
48
  * );
48
49
  *
49
50
  * // Branded IDs work too.
50
- * const todoId = createIdFromString<"Todo">("todo-1");
51
- * const todoSvg = createIdenticon(todoId);
51
+ * const todoSvg = createIdenticon(testTodoId);
52
52
  *
53
53
  * assertTrue(svg.startsWith("<svg"));
54
54
  * assertEqual(new Set([svg, ...alternativeSvgs]).size, 4);
@@ -22,6 +22,8 @@ import { constVoid } from "./Function.ts";
22
22
  * a warning may come late or, in short-lived processes, never. It is a
23
23
  * development canary, not a guarantee. Production uses
24
24
  * {@link noopLeakDetector}.
25
+ *
26
+ * @group Core
25
27
  */
26
28
  export interface LeakDetector {
27
29
  /**
@@ -38,7 +40,11 @@ export interface LeakDetector {
38
40
  readonly untrack: (unregisterToken: object) => void;
39
41
  }
40
42
 
41
- /** Describes a tracked handle for {@link LeakDetector.track}. */
43
+ /**
44
+ * Describes a tracked handle for {@link LeakDetector.track}.
45
+ *
46
+ * @group Core
47
+ */
42
48
  export interface Leak {
43
49
  /** Handle name used in the warning, for example `"Lease"`. */
44
50
  readonly name: string;
@@ -50,6 +56,7 @@ export interface Leak {
50
56
  /**
51
57
  * Dependency wrapper for {@link LeakDetector}.
52
58
  *
59
+ * @group Core
53
60
  * @see {@link LeakDetector}
54
61
  */
55
62
  export interface LeakDetectorDep {
@@ -61,6 +68,8 @@ export interface LeakDetectorDep {
61
68
  *
62
69
  * Capturing a stack per track call is too expensive for production; use
63
70
  * {@link noopLeakDetector} there.
71
+ *
72
+ * @group Core
64
73
  */
65
74
  export const createLeakDetector = (deps: ConsoleDep): LeakDetector => {
66
75
  if (typeof globalThis.FinalizationRegistry !== "function")
@@ -86,7 +95,11 @@ export const createLeakDetector = (deps: ConsoleDep): LeakDetector => {
86
95
  };
87
96
  };
88
97
 
89
- /** No-op {@link LeakDetector} for production. */
98
+ /**
99
+ * No-op {@link LeakDetector} for production.
100
+ *
101
+ * @group Core
102
+ */
90
103
  export const noopLeakDetector: LeakDetector = {
91
104
  track: constVoid,
92
105
  untrack: constVoid,
@@ -118,6 +131,7 @@ const reportLeak =
118
131
  /**
119
132
  * Test {@link LeakDetector} with deterministic collection.
120
133
  *
134
+ * @group Testing
121
135
  * @see {@link testCreateLeakDetector}
122
136
  */
123
137
  export interface TestLeakDetector extends LeakDetector {
@@ -136,13 +150,18 @@ export interface TestLeakDetector extends LeakDetector {
136
150
  /**
137
151
  * Dependency wrapper for {@link TestLeakDetector}.
138
152
  *
153
+ * @group Testing
139
154
  * @see {@link TestLeakDetector}
140
155
  */
141
156
  export interface TestLeakDetectorDep extends LeakDetectorDep {
142
157
  readonly leakDetector: TestLeakDetector;
143
158
  }
144
159
 
145
- /** Creates {@link TestLeakDetector}. */
160
+ /**
161
+ * Creates {@link TestLeakDetector}.
162
+ *
163
+ * @group Testing
164
+ */
146
165
  export const testCreateLeakDetector = (deps: ConsoleDep): TestLeakDetector => {
147
166
  const trackedLeaksByToken = new Map<object, ReadonlyArray<TrackedLeak>>();
148
167
  const report = reportLeak(deps);
@@ -47,6 +47,8 @@ import type { Callback } from "./Types.ts";
47
47
  * );
48
48
  * assertEqual(result, "example");
49
49
  * ```
50
+ *
51
+ * @group Core
50
52
  */
51
53
 
52
54
  export interface LockManagerDep {
@@ -65,6 +67,8 @@ export interface LockManagerDep {
65
67
  * via internal namespacing, so tests can reuse the same lock names without
66
68
  * contending through the global Web Locks. Query results are filtered to that
67
69
  * private namespace and returned with the original visible names.
70
+ *
71
+ * @group Testing
68
72
  */
69
73
  export const testCreateLockManager = (
70
74
  nativeLockManager: LockManager = navigator.locks,
@@ -119,6 +123,8 @@ export const testCreateLockManager = (
119
123
  * Leadership is held until the returned handle is disposed. Once released,
120
124
  * another waiting caller may become the next leader. Waiting for leadership is
121
125
  * abortable via the calling {@link Task}'s signal.
126
+ *
127
+ * @group Leader election
122
128
  */
123
129
  export const acquireLeaderLock =
124
130
  (name: string): Task<AsyncDisposable, never, LockManagerDep> =>
@@ -156,6 +162,8 @@ export const acquireLeaderLock =
156
162
  * Leadership is held until the returned handle is disposed. Once released,
157
163
  * another waiting caller may become the next leader. Waiting for leadership is
158
164
  * abortable by disposing the returned handle.
165
+ *
166
+ * @group Leader election
159
167
  */
160
168
  export const acquireLeaderLockCallback =
161
169
  (deps: LockManagerDep) =>
@@ -1,6 +1,12 @@
1
1
  import nodeAssert from "node:assert/strict";
2
2
  import { describe, it, test } from "node:test";
3
- import { assertEqual, assertFalse, assertSame, assertTrue } from "./Assert.ts";
3
+ import {
4
+ assertEqual,
5
+ assertFalse,
6
+ assertNonNullable,
7
+ assertSame,
8
+ assertTrue,
9
+ } from "./Assert.ts";
4
10
 
5
11
  import type { Brand } from "./Brand.ts";
6
12
  import type { ReadonlyRecord } from "./Object.ts";
@@ -105,6 +111,22 @@ test("isPlainObject", () => {
105
111
  assertFalse(isPlainObject(Object.create(partialObjectPrototype)));
106
112
  });
107
113
 
114
+ test("isPlainObject checks the root markers of Object.prototype", () => {
115
+ for (const key of ["hasOwnProperty", "isPrototypeOf"]) {
116
+ const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, key);
117
+ assertNonNullable(descriptor);
118
+ Reflect.deleteProperty(Object.prototype, key);
119
+ try {
120
+ assertFalse(isPlainObject({}));
121
+ assertTrue(isPlainObject(Object.create(null)));
122
+ } finally {
123
+ // oxlint-disable-next-line eslint/no-extend-native -- Restores the built-in property this test deleted.
124
+ Object.defineProperty(Object.prototype, key, descriptor);
125
+ }
126
+ }
127
+ assertTrue(isPlainObject({}));
128
+ });
129
+
108
130
  test("isFunction", () => {
109
131
  assertTrue(isFunction(() => {}));
110
132
  assertTrue(isFunction(function () {}));
@@ -244,13 +266,12 @@ describe("filterObjectKeys", () => {
244
266
  >();
245
267
  assertEqual(selectedUsers, { u1: 1 });
246
268
 
247
- const reject = () => {
269
+ void (() => {
248
270
  // @ts-expect-error filterObjectKeys requires an object source.
249
271
  filterObjectKeys("text", () => true);
250
272
  // @ts-expect-error Selected properties are readonly.
251
273
  selected.APP_PORT = "5000";
252
- };
253
- assertType<typeof reject, () => void>();
274
+ });
254
275
  });
255
276
 
256
277
  it("preserves descriptors without reading getters", () => {
@@ -360,16 +381,10 @@ test("createMutableRecord", () => {
360
381
  assertEqual(source, { name: "Ada" });
361
382
  assertSame(Object.getPrototypeOf(copy), null);
362
383
 
363
- const compileTimeAssertions = () => {
384
+ void (() => {
364
385
  // @ts-expect-error createMutableRecord source must be an object.
365
386
  createMutableRecord("Ada");
366
- };
367
- assertType<
368
- typeof compileTimeAssertions extends (...args: Array<never>) => unknown
369
- ? true
370
- : false,
371
- true
372
- >();
387
+ });
373
388
  });
374
389
 
375
390
  test("emptyRecord", () => {
package/src/Object.ts CHANGED
@@ -87,6 +87,11 @@ export const isPlainObject = (
87
87
 
88
88
  const prototype = Object.getPrototypeOf(value) as object | null;
89
89
  if (prototype === null) return true;
90
+ // This realm's Object.prototype has an immutable null prototype, so `in`
91
+ // checks the same own properties as the structural test below, faster.
92
+ if (prototype === Object.prototype) {
93
+ return "hasOwnProperty" in prototype && "isPrototypeOf" in prototype;
94
+ }
90
95
  return (
91
96
  Object.getPrototypeOf(prototype) === null &&
92
97
  Object.hasOwn(prototype, "hasOwnProperty") &&