@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.
- package/dist/src/Config.d.ts +22 -22
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +92 -18
- package/dist/src/Fs.d.ts.map +1 -1
- package/dist/src/Fs.js +2 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +63 -9
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +106 -9
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +55 -4
- package/dist/src/Type.d.ts +1455 -1310
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1274 -517
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +6 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +336 -211
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +102 -15
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +94 -16
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +118 -38
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +335 -21
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +192 -14
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +81 -20
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +403 -81
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/Config.test.ts +2 -6
- package/src/Config.ts +133 -133
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.ts +92 -18
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Object.test.ts +27 -12
- package/src/Object.ts +5 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +137 -17
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.ts +106 -9
- package/src/Type.test.ts +946 -1028
- package/src/Type.ts +4195 -3136
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +15 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +686 -21
- package/src/local-first/Evolu.ts +450 -228
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +617 -10
- package/src/local-first/Protocol.ts +196 -72
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +363 -24
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +218 -32
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +434 -118
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- 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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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);
|
package/src/LeakDetector.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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);
|
package/src/LockManager.ts
CHANGED
|
@@ -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) =>
|
package/src/Object.test.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import nodeAssert from "node:assert/strict";
|
|
2
2
|
import { describe, it, test } from "node:test";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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") &&
|