@evolu/common 8.10.0 → 8.12.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/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 +412 -213
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +181 -18
- 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 +106 -19
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +162 -60
- 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/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +346 -23
- 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 +195 -17
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +85 -22
- 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/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- 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 +20 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +994 -22
- package/src/local-first/Evolu.ts +625 -232
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +634 -10
- package/src/local-first/Protocol.ts +255 -109
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +376 -26
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +224 -36
- 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/Crypto.ts
CHANGED
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
zeroNonNegativeInt,
|
|
25
25
|
} from "./Type.ts";
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Cryptographically secure random bytes with length-branded results.
|
|
29
|
+
*
|
|
30
|
+
* @group Random bytes
|
|
31
|
+
*/
|
|
27
32
|
export interface RandomBytes {
|
|
28
33
|
/**
|
|
29
34
|
* Creates cryptographically secure random bytes with type-safe length
|
|
@@ -77,6 +82,11 @@ export interface RandomBytes {
|
|
|
77
82
|
create(bytesLength: number): Entropy;
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Dependency wrapper for {@link RandomBytes}.
|
|
87
|
+
*
|
|
88
|
+
* @group Random bytes
|
|
89
|
+
*/
|
|
80
90
|
export interface RandomBytesDep {
|
|
81
91
|
readonly randomBytes: RandomBytes;
|
|
82
92
|
}
|
|
@@ -84,23 +94,53 @@ export interface RandomBytesDep {
|
|
|
84
94
|
const Entropy = /*#__PURE__*/ brand("Entropy", Uint8Array);
|
|
85
95
|
type Entropy = typeof Entropy.Output;
|
|
86
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Cryptographic entropy of exactly 16 bytes.
|
|
99
|
+
*
|
|
100
|
+
* @group Random bytes
|
|
101
|
+
*/
|
|
87
102
|
export const Entropy16 = /*#__PURE__*/ length(16)(Entropy);
|
|
88
103
|
export type Entropy16 = typeof Entropy16.Output;
|
|
89
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Cryptographic entropy of exactly 24 bytes.
|
|
107
|
+
*
|
|
108
|
+
* @group Random bytes
|
|
109
|
+
*/
|
|
90
110
|
export const Entropy24 = /*#__PURE__*/ length(24)(Entropy);
|
|
91
111
|
export type Entropy24 = typeof Entropy24.Output;
|
|
92
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Cryptographic entropy of exactly 32 bytes.
|
|
115
|
+
*
|
|
116
|
+
* @group Random bytes
|
|
117
|
+
*/
|
|
93
118
|
export const Entropy32 = /*#__PURE__*/ length(32)(Entropy);
|
|
94
119
|
export type Entropy32 = typeof Entropy32.Output;
|
|
95
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Cryptographic entropy of exactly 64 bytes.
|
|
123
|
+
*
|
|
124
|
+
* @group Random bytes
|
|
125
|
+
*/
|
|
96
126
|
export const Entropy64 = /*#__PURE__*/ length(64)(Entropy);
|
|
97
127
|
export type Entropy64 = typeof Entropy64.Output;
|
|
98
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Creates {@link RandomBytes} backed by the platform's secure random number
|
|
131
|
+
* generator.
|
|
132
|
+
*
|
|
133
|
+
* @group Random bytes
|
|
134
|
+
*/
|
|
99
135
|
export const createRandomBytes = (): RandomBytes => ({
|
|
100
136
|
create: randomBytes as RandomBytes["create"],
|
|
101
137
|
});
|
|
102
138
|
|
|
103
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Creates seeded random bytes for deterministic tests.
|
|
141
|
+
*
|
|
142
|
+
* @group Testing
|
|
143
|
+
*/
|
|
104
144
|
export const testCreateRandomBytes = (deps: RandomLibDep): RandomBytes =>
|
|
105
145
|
({
|
|
106
146
|
create: (bytesLength: number) =>
|
|
@@ -113,6 +153,8 @@ export const testCreateRandomBytes = (deps: RandomLibDep): RandomBytes =>
|
|
|
113
153
|
* SLIP21.
|
|
114
154
|
*
|
|
115
155
|
* https://github.com/satoshilabs/slips/blob/master/slip-0021.md
|
|
156
|
+
*
|
|
157
|
+
* @group Key derivation
|
|
116
158
|
*/
|
|
117
159
|
export const createSlip21 = (
|
|
118
160
|
seed: Entropy16 | Entropy32 | Entropy64,
|
|
@@ -135,6 +177,7 @@ export const createSlip21 = (
|
|
|
135
177
|
/**
|
|
136
178
|
* Derives a single node in the SLIP-21 hierarchical key derivation.
|
|
137
179
|
*
|
|
180
|
+
* @group Key derivation
|
|
138
181
|
* @see {@link createSlip21}
|
|
139
182
|
*/
|
|
140
183
|
export const deriveSlip21Node = (
|
|
@@ -148,16 +191,25 @@ export const deriveSlip21Node = (
|
|
|
148
191
|
return hmac(sha512, parentNode.slice(0, 32), message) as Entropy64;
|
|
149
192
|
};
|
|
150
193
|
|
|
151
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* The encryption key for symmetric encryption.
|
|
196
|
+
*
|
|
197
|
+
* @group Encryption
|
|
198
|
+
*/
|
|
152
199
|
export const EncryptionKey = /*#__PURE__*/ brand("EncryptionKey", Entropy32);
|
|
153
200
|
export type EncryptionKey = typeof EncryptionKey.Output;
|
|
154
201
|
|
|
155
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* The nonce length for XChaCha20-Poly1305 encryption.
|
|
204
|
+
*
|
|
205
|
+
* @group Encryption
|
|
206
|
+
*/
|
|
156
207
|
export const xChaCha20Poly1305NonceLength = 24;
|
|
157
208
|
|
|
158
209
|
/**
|
|
159
210
|
* Branded Uint8Array for XChaCha20-Poly1305 encryption.
|
|
160
211
|
*
|
|
212
|
+
* @group Encryption
|
|
161
213
|
* @see {@link encryptWithXChaCha20Poly1305}
|
|
162
214
|
*/
|
|
163
215
|
export const XChaCha20Poly1305Ciphertext = /*#__PURE__*/ brand(
|
|
@@ -196,6 +248,7 @@ export type XChaCha20Poly1305Ciphertext =
|
|
|
196
248
|
* assertEqual(nonce.length, 24);
|
|
197
249
|
* ```
|
|
198
250
|
*
|
|
251
|
+
* @group Encryption
|
|
199
252
|
* @see https://github.com/paulmillr/noble-ciphers
|
|
200
253
|
*/
|
|
201
254
|
export const encryptWithXChaCha20Poly1305 =
|
|
@@ -211,6 +264,11 @@ export const encryptWithXChaCha20Poly1305 =
|
|
|
211
264
|
return [ciphertext, nonce];
|
|
212
265
|
};
|
|
213
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Error returned by {@link decryptWithXChaCha20Poly1305} when decryption fails.
|
|
269
|
+
*
|
|
270
|
+
* @group Encryption
|
|
271
|
+
*/
|
|
214
272
|
export interface DecryptWithXChaCha20Poly1305Error extends Typed<"DecryptWithXChaCha20Poly1305Error"> {
|
|
215
273
|
readonly error: unknown;
|
|
216
274
|
}
|
|
@@ -254,6 +312,8 @@ export interface DecryptWithXChaCha20Poly1305Error extends Typed<"DecryptWithXCh
|
|
|
254
312
|
*
|
|
255
313
|
* assertOk(decryptMessage(), "secret message");
|
|
256
314
|
* ```
|
|
315
|
+
*
|
|
316
|
+
* @group Encryption
|
|
257
317
|
*/
|
|
258
318
|
export const decryptWithXChaCha20Poly1305 = (
|
|
259
319
|
ciphertext: XChaCha20Poly1305Ciphertext,
|
|
@@ -275,6 +335,8 @@ export const decryptWithXChaCha20Poly1305 = (
|
|
|
275
335
|
* wide range of encrypted data sizes.
|
|
276
336
|
*
|
|
277
337
|
* See the PURBs paper for details: https://bford.info/pub/sec/purb.pdf
|
|
338
|
+
*
|
|
339
|
+
* @group Padding
|
|
278
340
|
*/
|
|
279
341
|
export const createPadmePaddedLength = (
|
|
280
342
|
length: NonNegativeInt,
|
|
@@ -287,7 +349,11 @@ export const createPadmePaddedLength = (
|
|
|
287
349
|
return NonNegativeInt.orThrow((length + mask) & ~mask);
|
|
288
350
|
};
|
|
289
351
|
|
|
290
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* Creates a PADMÉ padding array of zeros for the given input length.
|
|
354
|
+
*
|
|
355
|
+
* @group Padding
|
|
356
|
+
*/
|
|
291
357
|
export const createPadmePadding = (length: NonNegativeInt): Uint8Array => {
|
|
292
358
|
const paddedLength = createPadmePaddedLength(length);
|
|
293
359
|
const paddingLength = NonNegativeInt.orThrow(paddedLength - length);
|
|
@@ -299,10 +365,16 @@ export const createPadmePadding = (length: NonNegativeInt): Uint8Array => {
|
|
|
299
365
|
* are equal, false otherwise. Takes constant time regardless of where the
|
|
300
366
|
* arrays differ.
|
|
301
367
|
*
|
|
368
|
+
* @group Comparison
|
|
302
369
|
* @see https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b
|
|
303
370
|
*/
|
|
304
371
|
export type TimingSafeEqual = (a: Uint8Array, b: Uint8Array) => boolean;
|
|
305
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Dependency wrapper for {@link TimingSafeEqual}.
|
|
375
|
+
*
|
|
376
|
+
* @group Comparison
|
|
377
|
+
*/
|
|
306
378
|
export interface TimingSafeEqualDep {
|
|
307
379
|
readonly timingSafeEqual: TimingSafeEqual;
|
|
308
380
|
}
|
package/src/Eq.test.ts
CHANGED
|
@@ -293,13 +293,12 @@ test("eqData", () => {
|
|
|
293
293
|
}
|
|
294
294
|
const service: Service = { run: () => undefined };
|
|
295
295
|
const broadObject: NonNullable<unknown> = new WeakMap();
|
|
296
|
-
|
|
296
|
+
void (() => {
|
|
297
297
|
// @ts-expect-error ⛔ eqData error: Actual and expected values must consist only of Data.
|
|
298
298
|
eqData(service, service);
|
|
299
299
|
// @ts-expect-error ⛔ eqData error: Actual and expected values must consist only of Data.
|
|
300
300
|
eqData(broadObject, broadObject);
|
|
301
|
-
};
|
|
302
|
-
assertEqual(typeof compileTimeAssertions, "function");
|
|
301
|
+
});
|
|
303
302
|
});
|
|
304
303
|
|
|
305
304
|
test("eqData compares deeply nested Set and Map data", () => {
|
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);
|