@evolu/common 8.8.0 → 8.10.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/Assert.d.ts +21 -1
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +31 -27
- package/dist/src/Bytes.d.ts +647 -0
- package/dist/src/Bytes.d.ts.map +1 -0
- package/dist/src/{Binary.js → Bytes.js} +266 -16
- package/dist/src/Config.d.ts +142 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +181 -0
- package/dist/src/Fs.d.ts +302 -0
- package/dist/src/Fs.d.ts.map +1 -0
- package/dist/src/Fs.js +111 -0
- package/dist/src/Number.d.ts +50 -7
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +47 -8
- package/dist/src/Object.d.ts +32 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +41 -0
- package/dist/src/Polyfills.d.ts +6 -0
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +6 -0
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/Time.d.ts +74 -12
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +40 -2
- package/dist/src/Type.d.ts +1451 -79
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1303 -96
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/_en.d.ts +24 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +20 -0
- package/dist/src/intl/ar.d.ts +24 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +20 -0
- package/dist/src/intl/bn.d.ts +24 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +20 -0
- package/dist/src/intl/ca.d.ts +24 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +20 -0
- package/dist/src/intl/cs.d.ts +24 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +20 -0
- package/dist/src/intl/da.d.ts +24 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +20 -0
- package/dist/src/intl/de.d.ts +24 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +20 -0
- package/dist/src/intl/el.d.ts +24 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +20 -0
- package/dist/src/intl/es.d.ts +24 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +20 -0
- package/dist/src/intl/fa.d.ts +24 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +20 -0
- package/dist/src/intl/fi.d.ts +24 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +20 -0
- package/dist/src/intl/fil.d.ts +24 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +20 -0
- package/dist/src/intl/fr.d.ts +24 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +20 -0
- package/dist/src/intl/he.d.ts +24 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +20 -0
- package/dist/src/intl/hi.d.ts +24 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +20 -0
- package/dist/src/intl/hr.d.ts +24 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +20 -0
- package/dist/src/intl/hu.d.ts +22 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +18 -0
- package/dist/src/intl/id.d.ts +24 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +20 -0
- package/dist/src/intl/it.d.ts +24 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +20 -0
- package/dist/src/intl/ja.d.ts +24 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +20 -0
- package/dist/src/intl/ko.d.ts +24 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +20 -0
- package/dist/src/intl/ml.d.ts +24 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +20 -0
- package/dist/src/intl/mr.d.ts +24 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +20 -0
- package/dist/src/intl/ms.d.ts +24 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +20 -0
- package/dist/src/intl/nb.d.ts +22 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +18 -0
- package/dist/src/intl/nl.d.ts +24 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +20 -0
- package/dist/src/intl/pa.d.ts +24 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +20 -0
- package/dist/src/intl/pl.d.ts +23 -0
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +20 -0
- package/dist/src/intl/pt-BR.d.ts +24 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +20 -0
- package/dist/src/intl/pt.d.ts +24 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +20 -0
- package/dist/src/intl/ro.d.ts +24 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +20 -0
- package/dist/src/intl/sk.d.ts +24 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +20 -0
- package/dist/src/intl/sl.d.ts +24 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +20 -0
- package/dist/src/intl/sv.d.ts +24 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +20 -0
- package/dist/src/intl/sw.d.ts +21 -0
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +18 -0
- package/dist/src/intl/ta.d.ts +24 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +20 -0
- package/dist/src/intl/te.d.ts +24 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +20 -0
- package/dist/src/intl/th.d.ts +24 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +20 -0
- package/dist/src/intl/tr.d.ts +24 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +20 -0
- package/dist/src/intl/uk.d.ts +80 -57
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +174 -149
- package/dist/src/intl/ur.d.ts +24 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +20 -0
- package/dist/src/intl/vi.d.ts +24 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +20 -0
- package/dist/src/intl/zh-CN.d.ts +24 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +20 -0
- package/dist/src/intl/zh-TW.d.ts +24 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +20 -0
- package/dist/src/local-first/Protocol.d.ts +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +1 -1
- package/dist/src/local-first/Schema.d.ts +10 -0
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +1 -1
- package/dist/src/local-first/Timestamp.js +1 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +51 -0
- package/src/Assert.ts +56 -4
- package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
- package/src/{Binary.ts → Bytes.ts} +652 -21
- package/src/Config.test.ts +672 -0
- package/src/Config.ts +410 -0
- package/src/Fs.test.ts +105 -0
- package/src/Fs.ts +414 -0
- package/src/Number.test.ts +82 -18
- package/src/Number.ts +76 -8
- package/src/Object.test.ts +115 -1
- package/src/Object.ts +44 -0
- package/src/Polyfills.ts +6 -0
- package/src/Sqlite.ts +1 -1
- package/src/Time.test.ts +82 -11
- package/src/Time.ts +141 -16
- package/src/Type.test.ts +3114 -157
- package/src/Type.ts +2553 -196
- package/src/index.ts +3 -1
- package/src/intl/_en.ts +70 -0
- package/src/intl/ar.ts +71 -0
- package/src/intl/bn.ts +70 -0
- package/src/intl/ca.ts +70 -0
- package/src/intl/cs.ts +70 -0
- package/src/intl/da.ts +70 -0
- package/src/intl/de.ts +70 -0
- package/src/intl/el.ts +70 -0
- package/src/intl/es.ts +70 -0
- package/src/intl/fa.ts +70 -0
- package/src/intl/fi.ts +70 -0
- package/src/intl/fil.ts +70 -0
- package/src/intl/fr.ts +70 -0
- package/src/intl/he.ts +70 -0
- package/src/intl/hi.ts +70 -0
- package/src/intl/hr.ts +70 -0
- package/src/intl/hu.ts +69 -0
- package/src/intl/id.ts +70 -0
- package/src/intl/intl.test.ts +819 -1
- package/src/intl/it.ts +70 -0
- package/src/intl/ja.ts +70 -0
- package/src/intl/ko.ts +68 -0
- package/src/intl/ml.ts +70 -0
- package/src/intl/mr.ts +70 -0
- package/src/intl/ms.ts +71 -0
- package/src/intl/nb.ts +69 -0
- package/src/intl/nl.ts +70 -0
- package/src/intl/pa.ts +70 -0
- package/src/intl/pl.ts +63 -0
- package/src/intl/pt-BR.ts +70 -0
- package/src/intl/pt.ts +71 -0
- package/src/intl/ro.ts +70 -0
- package/src/intl/sk.ts +71 -0
- package/src/intl/sl.ts +70 -0
- package/src/intl/sv.ts +70 -0
- package/src/intl/sw.ts +62 -0
- package/src/intl/ta.ts +70 -0
- package/src/intl/te.ts +70 -0
- package/src/intl/th.ts +68 -0
- package/src/intl/tr.ts +70 -0
- package/src/intl/uk.ts +228 -155
- package/src/intl/ur.ts +70 -0
- package/src/intl/vi.ts +70 -0
- package/src/intl/zh-CN.ts +68 -0
- package/src/intl/zh-TW.ts +68 -0
- package/src/local-first/Protocol.test.ts +1 -1
- package/src/local-first/Protocol.ts +1 -1
- package/src/local-first/Schema.ts +11 -0
- package/src/local-first/Storage.ts +1 -1
- package/src/local-first/Timestamp.ts +1 -1
- package/dist/src/Binary.d.ts +0 -254
- package/dist/src/Binary.d.ts.map +0 -1
package/src/Object.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import nodeAssert from "node:assert/strict";
|
|
2
|
+
import { describe, it, test } from "node:test";
|
|
2
3
|
import { assertEqual, assertFalse, assertSame, assertTrue } from "./Assert.ts";
|
|
3
4
|
|
|
4
5
|
import type { Brand } from "./Brand.ts";
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
createMutableRecord,
|
|
9
10
|
emptyRecord,
|
|
10
11
|
excludeProp,
|
|
12
|
+
filterObjectKeys,
|
|
11
13
|
getObjectKind,
|
|
12
14
|
getOwnProp,
|
|
13
15
|
isFunction,
|
|
@@ -205,6 +207,118 @@ test("mapObject", () => {
|
|
|
205
207
|
assertEqual(mapped, { u1: "u1:10", u2: "u2:20" });
|
|
206
208
|
});
|
|
207
209
|
|
|
210
|
+
describe("filterObjectKeys", () => {
|
|
211
|
+
it("preserves field types and makes selected fields optional", () => {
|
|
212
|
+
const symbol = Symbol("ignored");
|
|
213
|
+
const source = {
|
|
214
|
+
APP_PORT: "4000",
|
|
215
|
+
other: 1,
|
|
216
|
+
42: true,
|
|
217
|
+
[symbol]: "ignored",
|
|
218
|
+
};
|
|
219
|
+
const selected = filterObjectKeys(source, (key) => {
|
|
220
|
+
assertType<typeof key, string>();
|
|
221
|
+
return key.startsWith("APP_") || key === "42";
|
|
222
|
+
});
|
|
223
|
+
assertType<
|
|
224
|
+
typeof selected,
|
|
225
|
+
{
|
|
226
|
+
readonly APP_PORT?: string;
|
|
227
|
+
readonly other?: number;
|
|
228
|
+
readonly 42?: boolean;
|
|
229
|
+
}
|
|
230
|
+
>();
|
|
231
|
+
assertEqual(selected, { APP_PORT: "4000", 42: true });
|
|
232
|
+
assertFalse(selected === source);
|
|
233
|
+
assertEqual(
|
|
234
|
+
filterObjectKeys(source, () => false),
|
|
235
|
+
{},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
type UserId = string & Brand<"UserId">;
|
|
239
|
+
const users: ReadonlyRecord<UserId, number> = { ["u1" as UserId]: 1 };
|
|
240
|
+
const selectedUsers = filterObjectKeys(users, () => true);
|
|
241
|
+
assertType<
|
|
242
|
+
typeof selectedUsers,
|
|
243
|
+
Readonly<Partial<Record<UserId, number>>>
|
|
244
|
+
>();
|
|
245
|
+
assertEqual(selectedUsers, { u1: 1 });
|
|
246
|
+
|
|
247
|
+
const reject = () => {
|
|
248
|
+
// @ts-expect-error filterObjectKeys requires an object source.
|
|
249
|
+
filterObjectKeys("text", () => true);
|
|
250
|
+
// @ts-expect-error Selected properties are readonly.
|
|
251
|
+
selected.APP_PORT = "5000";
|
|
252
|
+
};
|
|
253
|
+
assertType<typeof reject, () => void>();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("preserves descriptors without reading getters", () => {
|
|
257
|
+
let reads = 0;
|
|
258
|
+
const getter = () => {
|
|
259
|
+
reads++;
|
|
260
|
+
return "value";
|
|
261
|
+
};
|
|
262
|
+
const source = Object.defineProperties(Object.create({ inherited: 1 }), {
|
|
263
|
+
visible: { value: { nested: true }, enumerable: true, writable: true },
|
|
264
|
+
hidden: { value: undefined },
|
|
265
|
+
accessor: { get: getter, enumerable: true, configurable: true },
|
|
266
|
+
excluded: { get: getter },
|
|
267
|
+
[Symbol("ignored")]: { get: getter },
|
|
268
|
+
});
|
|
269
|
+
const visited: Array<string> = [];
|
|
270
|
+
const selected = filterObjectKeys(source, (key) => {
|
|
271
|
+
visited.push(key);
|
|
272
|
+
return key !== "excluded";
|
|
273
|
+
});
|
|
274
|
+
assertEqual(visited, ["visible", "hidden", "accessor", "excluded"]);
|
|
275
|
+
assertEqual(Object.getOwnPropertyNames(selected), [
|
|
276
|
+
"visible",
|
|
277
|
+
"hidden",
|
|
278
|
+
"accessor",
|
|
279
|
+
]);
|
|
280
|
+
for (const key of Object.getOwnPropertyNames(selected)) {
|
|
281
|
+
nodeAssert.deepEqual(
|
|
282
|
+
Object.getOwnPropertyDescriptor(selected, key),
|
|
283
|
+
Object.getOwnPropertyDescriptor(source, key),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
assertEqual(reads, 0);
|
|
287
|
+
assertFalse(Object.hasOwn(selected, "inherited"));
|
|
288
|
+
assertEqual(Object.getOwnPropertySymbols(selected), []);
|
|
289
|
+
assertSame(Object.getPrototypeOf(selected), Object.prototype);
|
|
290
|
+
assertSame(
|
|
291
|
+
Reflect.get(selected, "visible"),
|
|
292
|
+
Reflect.get(source, "visible"),
|
|
293
|
+
);
|
|
294
|
+
assertSame(Reflect.get(selected, "accessor"), "value");
|
|
295
|
+
assertEqual(reads, 1);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("safely copies special keys and handles arrays as objects", () => {
|
|
299
|
+
const source = createMutableRecord();
|
|
300
|
+
source.__proto__ = "value";
|
|
301
|
+
Object.defineProperty(source, "constructor", {
|
|
302
|
+
value: "constructor",
|
|
303
|
+
enumerable: true,
|
|
304
|
+
});
|
|
305
|
+
const selected = filterObjectKeys(source, () => true);
|
|
306
|
+
assertTrue(Object.hasOwn(selected, "__proto__"));
|
|
307
|
+
assertSame(Object.getPrototypeOf(selected), Object.prototype);
|
|
308
|
+
assertEqual(selected, {
|
|
309
|
+
["__proto__"]: "value",
|
|
310
|
+
constructor: "constructor",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const values: ReadonlyArray<number> = [10, 20];
|
|
314
|
+
const array = filterObjectKeys(values, (key) => key === "0");
|
|
315
|
+
assertFalse(Array.isArray(array));
|
|
316
|
+
assertEqual(array, { 0: 10 });
|
|
317
|
+
assertType<(typeof array)[0], number | undefined>();
|
|
318
|
+
assertType<typeof array.map, ReadonlyArray<number>["map"] | undefined>();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
208
322
|
test("excludeProp", () => {
|
|
209
323
|
const obj = { a: 1, b: 2, c: 3 };
|
|
210
324
|
|
package/src/Object.ts
CHANGED
|
@@ -243,6 +243,50 @@ export const mapObject = <K extends string, V, U>(
|
|
|
243
243
|
return out;
|
|
244
244
|
};
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Selects own string-keyed properties while preserving their descriptors.
|
|
248
|
+
*
|
|
249
|
+
* Visits enumerable and non-enumerable properties, ignoring inherited and
|
|
250
|
+
* symbol properties. The predicate receives only the key; property values and
|
|
251
|
+
* getters are never read. The result is a new ordinary object with matching
|
|
252
|
+
* descriptors, including accessor functions and property flags. Declared
|
|
253
|
+
* properties are optional because the predicate may exclude any of them.
|
|
254
|
+
* Numeric property names are visited as strings.
|
|
255
|
+
*
|
|
256
|
+
* ### Example
|
|
257
|
+
*
|
|
258
|
+
* ```ts
|
|
259
|
+
* import {
|
|
260
|
+
* assertEqual,
|
|
261
|
+
* assertType,
|
|
262
|
+
* filterObjectKeys,
|
|
263
|
+
* } from "@evolu/common";
|
|
264
|
+
*
|
|
265
|
+
* const source = { APP_PORT: "4000", HOME: "/home/evolu" };
|
|
266
|
+
* const selected = filterObjectKeys(source, (key) =>
|
|
267
|
+
* key.startsWith("APP_"),
|
|
268
|
+
* );
|
|
269
|
+
*
|
|
270
|
+
* assertEqual(selected, { APP_PORT: "4000" });
|
|
271
|
+
* assertType<
|
|
272
|
+
* typeof selected,
|
|
273
|
+
* { readonly APP_PORT?: string; readonly HOME?: string }
|
|
274
|
+
* >();
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export const filterObjectKeys = <T extends object>(
|
|
278
|
+
source: T,
|
|
279
|
+
predicate: (key: string) => boolean,
|
|
280
|
+
): Readonly<Partial<Pick<T, Exclude<keyof T, symbol>>>> => {
|
|
281
|
+
const selected: Partial<Pick<T, Exclude<keyof T, symbol>>> = {};
|
|
282
|
+
for (const key of Object.getOwnPropertyNames(source)) {
|
|
283
|
+
if (!predicate(key)) continue;
|
|
284
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key)!;
|
|
285
|
+
Object.defineProperty(selected, key, descriptor);
|
|
286
|
+
}
|
|
287
|
+
return selected;
|
|
288
|
+
};
|
|
289
|
+
|
|
246
290
|
/** Conditionally excludes a property from an object. */
|
|
247
291
|
export const excludeProp = <T extends object, K extends keyof T>(
|
|
248
292
|
obj: T,
|
package/src/Polyfills.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* `Map.prototype.getOrInsertComputed`, `WeakMap.prototype.getOrInsert`, and
|
|
17
17
|
* `WeakMap.prototype.getOrInsertComputed`) for Node 24 and React Native
|
|
18
18
|
*
|
|
19
|
+
* The polyfills are implemented directly in the Evolu Library instead of being
|
|
20
|
+
* third-party runtime dependencies. We audit their behavior against
|
|
21
|
+
* specifications, conformance tests, and native implementations, and fix issues
|
|
22
|
+
* when necessary. This lets Evolu understand and control every line of this
|
|
23
|
+
* runtime code.
|
|
24
|
+
*
|
|
19
25
|
* `@evolu/react-native` has its own `Polyfills` module and its
|
|
20
26
|
* `installPolyfills` calls this function first, then installs React Native
|
|
21
27
|
* specific polyfills.
|
package/src/Sqlite.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Brand } from "./Brand.ts";
|
|
8
|
-
import { bytesToHex, hexToBytes } from "./
|
|
8
|
+
import { bytesToHex, hexToBytes } from "./Bytes.ts";
|
|
9
9
|
import type { EncryptionKey } from "./Crypto.ts";
|
|
10
10
|
import type { Eq } from "./Eq.ts";
|
|
11
11
|
import { createEqObject, eqString, eqUint8Array } from "./Eq.ts";
|
package/src/Time.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, it, mock } from "node:test";
|
|
2
2
|
import {
|
|
3
3
|
assertEqual,
|
|
4
|
+
assertErr,
|
|
5
|
+
assertOk,
|
|
4
6
|
assertFalse,
|
|
5
7
|
assertLength,
|
|
6
8
|
assertSame,
|
|
@@ -37,19 +39,21 @@ import {
|
|
|
37
39
|
saturateMillis,
|
|
38
40
|
testCreateTime,
|
|
39
41
|
} from "./Time.ts";
|
|
40
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
localizeTypes,
|
|
44
|
+
object,
|
|
45
|
+
typeErrorToIssues,
|
|
46
|
+
type InferErrors,
|
|
47
|
+
assertType,
|
|
48
|
+
type DateIso,
|
|
49
|
+
NonNaNNumber,
|
|
50
|
+
} from "./Type.ts";
|
|
41
51
|
|
|
42
52
|
const negativeMillisCause = {
|
|
43
53
|
type: "NonNegative",
|
|
44
54
|
value: -1,
|
|
45
55
|
};
|
|
46
56
|
|
|
47
|
-
const assertThrowsWithCause = (run: () => unknown, cause: unknown): void => {
|
|
48
|
-
const error = assertThrowsInstanceOf(run, Error);
|
|
49
|
-
assertEqual(error.message, "getOrThrow");
|
|
50
|
-
assertEqual(error.cause, cause);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
57
|
describe("Time", () => {
|
|
54
58
|
afterEach(() => {
|
|
55
59
|
mock.restoreAll();
|
|
@@ -218,14 +222,17 @@ describe("Time", () => {
|
|
|
218
222
|
);
|
|
219
223
|
mock.method(Date, "now", () => -1);
|
|
220
224
|
|
|
221
|
-
|
|
225
|
+
const error = assertThrowsInstanceOf(
|
|
222
226
|
() =>
|
|
223
227
|
createTime().setTimeout(
|
|
224
228
|
() => undefined,
|
|
225
229
|
PositiveMillis.orThrow(2 ** 31),
|
|
226
230
|
),
|
|
227
|
-
|
|
231
|
+
Error,
|
|
228
232
|
);
|
|
233
|
+
|
|
234
|
+
assertEqual(error.message, "The value -1 must be non-negative (>= 0).");
|
|
235
|
+
assertEqual(error.cause, negativeMillisCause);
|
|
229
236
|
assertEqual(setTimeout.mock.callCount(), 0);
|
|
230
237
|
});
|
|
231
238
|
|
|
@@ -247,7 +254,10 @@ describe("Time", () => {
|
|
|
247
254
|
);
|
|
248
255
|
now = -1;
|
|
249
256
|
|
|
250
|
-
|
|
257
|
+
const error = assertThrowsInstanceOf(() => callbacks[0](), Error);
|
|
258
|
+
|
|
259
|
+
assertEqual(error.message, "The value -1 must be non-negative (>= 0).");
|
|
260
|
+
assertEqual(error.cause, negativeMillisCause);
|
|
251
261
|
assertLength(callbacks, 1);
|
|
252
262
|
});
|
|
253
263
|
|
|
@@ -445,7 +455,16 @@ describe("Time", () => {
|
|
|
445
455
|
it("setTimeout rejects a deadline after maxMillis", () => {
|
|
446
456
|
const time = testCreateTime({ startAt: maxMillis });
|
|
447
457
|
|
|
448
|
-
|
|
458
|
+
const error = assertThrowsInstanceOf(
|
|
459
|
+
() => time.setTimeout(() => undefined, "1ms"),
|
|
460
|
+
Error,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
assertEqual(
|
|
464
|
+
error.message,
|
|
465
|
+
"The value 281474976710655 must be less than 281474976710655.",
|
|
466
|
+
);
|
|
467
|
+
assertEqual(error.cause, {
|
|
449
468
|
type: "LessThan281474976710655",
|
|
450
469
|
value: maxMillis + 1,
|
|
451
470
|
max: maxMillis + 1,
|
|
@@ -727,6 +746,58 @@ describe("Time", () => {
|
|
|
727
746
|
});
|
|
728
747
|
|
|
729
748
|
describe("DurationLiteral", () => {
|
|
749
|
+
it("reports a dedicated error with the original validation failure", () => {
|
|
750
|
+
for (const value of ["60s", "", 1, null, undefined]) {
|
|
751
|
+
const result = DurationLiteral.fromUnknown(value);
|
|
752
|
+
assertErr(result);
|
|
753
|
+
assertType<typeof result.error.type, "DurationLiteral">();
|
|
754
|
+
assertSame(result.error.value, value);
|
|
755
|
+
assertEqual(result.error.cause.type, "Union");
|
|
756
|
+
assertEqual(result.error.cause.errors.length, 1);
|
|
757
|
+
}
|
|
758
|
+
const result = DurationLiteral.fromUnknown("60s", { errors: "all" });
|
|
759
|
+
assertErr(result);
|
|
760
|
+
assertEqual(result.error.cause.errors.length, 7);
|
|
761
|
+
assertEqual(
|
|
762
|
+
DurationLiteral.formatError(result.error),
|
|
763
|
+
'The value "60s" is not a duration literal. Use a value such as "500ms" or "1.5s".',
|
|
764
|
+
);
|
|
765
|
+
assertType<
|
|
766
|
+
InferErrors<typeof DurationLiteral>["type"],
|
|
767
|
+
"DurationLiteral"
|
|
768
|
+
>();
|
|
769
|
+
assertOk(DurationLiteral.from("1.5s"), "1.5s");
|
|
770
|
+
assertEqual(DurationLiteral.to("1.5s"), "1.5s");
|
|
771
|
+
// @ts-expect-error DurationLiteral Input rejects "60s".
|
|
772
|
+
const _invalidInput: typeof DurationLiteral.Input = "60s";
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("localizes the named error after composition and preserves its path", async () => {
|
|
776
|
+
const { first } = localizeTypes(
|
|
777
|
+
{ Value: DurationLiteral },
|
|
778
|
+
{ first: { DurationLiteral: () => "First message." } },
|
|
779
|
+
);
|
|
780
|
+
const { second } = localizeTypes(
|
|
781
|
+
{ Settings: object({ value: first.Value }) },
|
|
782
|
+
{
|
|
783
|
+
second: {
|
|
784
|
+
Object: () => "Object.",
|
|
785
|
+
DurationLiteral: () => "Second message.",
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
);
|
|
789
|
+
const result = second.Settings.fromUnknown({ value: "60s" });
|
|
790
|
+
assertErr(result);
|
|
791
|
+
assertEqual(second.Settings.formatError(result.error), "Second message.");
|
|
792
|
+
assertEqual(typeErrorToIssues(second.Settings, result.error), [
|
|
793
|
+
{ path: ["value"], message: "Second message." },
|
|
794
|
+
]);
|
|
795
|
+
assertEqual(
|
|
796
|
+
await second.Settings["~standard"].validate({ value: "60s" }),
|
|
797
|
+
{ issues: [{ path: ["value"], message: "Second message." }] },
|
|
798
|
+
);
|
|
799
|
+
});
|
|
800
|
+
|
|
730
801
|
it("valid durations", () => {
|
|
731
802
|
// Milliseconds
|
|
732
803
|
assertType<"1ms" extends DurationLiteral ? true : false, true>();
|
package/src/Time.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Time representations, durations, and scheduling utilities.
|
|
3
3
|
*
|
|
4
|
+
* Durations follow a pattern that other quantities in Evolu repeat, such as
|
|
5
|
+
* sizes in Bytes.ts:
|
|
6
|
+
*
|
|
7
|
+
* - {@link Millis} is the canonical unit, a validated number of milliseconds.
|
|
8
|
+
* - {@link DurationLiteral} is the human-readable form, such as `"1.5s"`,
|
|
9
|
+
* validated at compile time and runtime.
|
|
10
|
+
* - {@link Duration} is what APIs accept: `DurationLiteral | Millis`.
|
|
11
|
+
* - {@link durationToMillis} normalizes a `Duration` to `Millis`.
|
|
12
|
+
*
|
|
4
13
|
* @module
|
|
5
14
|
*/
|
|
6
15
|
|
|
16
|
+
import { safelyStringifyUnknownValue } from "./String.ts";
|
|
7
17
|
import { assert } from "./Assert.ts";
|
|
8
18
|
import type { Brand } from "./Brand.ts";
|
|
9
19
|
import { exhaustiveCheck } from "./Function.ts";
|
|
10
20
|
import type { yieldNow } from "./Task.ts";
|
|
11
21
|
import {
|
|
12
22
|
brand,
|
|
23
|
+
createTypeWithError,
|
|
24
|
+
type Type,
|
|
25
|
+
type TypeError,
|
|
26
|
+
type UnionError,
|
|
27
|
+
type UnionType,
|
|
28
|
+
type TemplateLiteralType,
|
|
13
29
|
type DateIso,
|
|
14
30
|
Digit,
|
|
15
31
|
Digit1To23,
|
|
@@ -418,8 +434,18 @@ export type Duration = DurationLiteral | Millis;
|
|
|
418
434
|
*/
|
|
419
435
|
export type PositiveDuration = DurationLiteral | PositiveMillis;
|
|
420
436
|
|
|
437
|
+
// Keep these annotations concrete. Generic unit wrappers add thousands of
|
|
438
|
+
// compiler instantiations to pnpm bench:type.
|
|
421
439
|
/** Milliseconds duration: `"1ms"` to `"999ms"`. See {@link DurationLiteral}. */
|
|
422
|
-
export const DurationLiteralMilliseconds
|
|
440
|
+
export const DurationLiteralMilliseconds: UnionType<
|
|
441
|
+
readonly [
|
|
442
|
+
TemplateLiteralType<readonly [typeof Digit1To9, "ms"]>,
|
|
443
|
+
TemplateLiteralType<readonly [typeof Digit1To9, typeof Digit, "ms"]>,
|
|
444
|
+
TemplateLiteralType<
|
|
445
|
+
readonly [typeof Digit1To9, typeof Digit, typeof Digit, "ms"]
|
|
446
|
+
>,
|
|
447
|
+
]
|
|
448
|
+
> = /*#__PURE__*/ union(
|
|
423
449
|
/*#__PURE__*/ templateLiteral(Digit1To9, "ms"),
|
|
424
450
|
/*#__PURE__*/ templateLiteral(Digit1To9, Digit, "ms"),
|
|
425
451
|
/*#__PURE__*/ templateLiteral(Digit1To9, Digit, Digit, "ms"),
|
|
@@ -431,7 +457,14 @@ export type DurationLiteralMilliseconds =
|
|
|
431
457
|
* Seconds duration: `"1s"` to `"59s"` or `"1.1s"` to `"59.9s"`. See
|
|
432
458
|
* {@link DurationLiteral}.
|
|
433
459
|
*/
|
|
434
|
-
export const DurationLiteralSeconds
|
|
460
|
+
export const DurationLiteralSeconds: UnionType<
|
|
461
|
+
readonly [
|
|
462
|
+
TemplateLiteralType<readonly [typeof Digit1To59, "s"]>,
|
|
463
|
+
TemplateLiteralType<
|
|
464
|
+
readonly [typeof Digit1To59, ".", typeof Digit1To9, "s"]
|
|
465
|
+
>,
|
|
466
|
+
]
|
|
467
|
+
> = /*#__PURE__*/ union(
|
|
435
468
|
/*#__PURE__*/ templateLiteral(Digit1To59, "s"),
|
|
436
469
|
/*#__PURE__*/ templateLiteral(Digit1To59, ".", Digit1To9, "s"),
|
|
437
470
|
);
|
|
@@ -441,7 +474,14 @@ export type DurationLiteralSeconds = typeof DurationLiteralSeconds.Output;
|
|
|
441
474
|
* Minutes duration: `"1m"` to `"59m"` or `"1.1m"` to `"59.9m"`. See
|
|
442
475
|
* {@link DurationLiteral}.
|
|
443
476
|
*/
|
|
444
|
-
export const DurationLiteralMinutes
|
|
477
|
+
export const DurationLiteralMinutes: UnionType<
|
|
478
|
+
readonly [
|
|
479
|
+
TemplateLiteralType<readonly [typeof Digit1To59, "m"]>,
|
|
480
|
+
TemplateLiteralType<
|
|
481
|
+
readonly [typeof Digit1To59, ".", typeof Digit1To9, "m"]
|
|
482
|
+
>,
|
|
483
|
+
]
|
|
484
|
+
> = /*#__PURE__*/ union(
|
|
445
485
|
/*#__PURE__*/ templateLiteral(Digit1To59, "m"),
|
|
446
486
|
/*#__PURE__*/ templateLiteral(Digit1To59, ".", Digit1To9, "m"),
|
|
447
487
|
);
|
|
@@ -451,7 +491,14 @@ export type DurationLiteralMinutes = typeof DurationLiteralMinutes.Output;
|
|
|
451
491
|
* Hours duration: `"1h"` to `"23h"` or `"1.1h"` to `"23.9h"`. See
|
|
452
492
|
* {@link DurationLiteral}.
|
|
453
493
|
*/
|
|
454
|
-
export const DurationLiteralHours
|
|
494
|
+
export const DurationLiteralHours: UnionType<
|
|
495
|
+
readonly [
|
|
496
|
+
TemplateLiteralType<readonly [typeof Digit1To23, "h"]>,
|
|
497
|
+
TemplateLiteralType<
|
|
498
|
+
readonly [typeof Digit1To23, ".", typeof Digit1To9, "h"]
|
|
499
|
+
>,
|
|
500
|
+
]
|
|
501
|
+
> = /*#__PURE__*/ union(
|
|
455
502
|
/*#__PURE__*/ templateLiteral(Digit1To23, "h"),
|
|
456
503
|
/*#__PURE__*/ templateLiteral(Digit1To23, ".", Digit1To9, "h"),
|
|
457
504
|
);
|
|
@@ -461,7 +508,14 @@ export type DurationLiteralHours = typeof DurationLiteralHours.Output;
|
|
|
461
508
|
* Days duration: `"1d"` to `"6d"` or `"1.1d"` to `"6.9d"`. See
|
|
462
509
|
* {@link DurationLiteral}.
|
|
463
510
|
*/
|
|
464
|
-
export const DurationLiteralDays
|
|
511
|
+
export const DurationLiteralDays: UnionType<
|
|
512
|
+
readonly [
|
|
513
|
+
TemplateLiteralType<readonly [typeof Digit1To6, "d"]>,
|
|
514
|
+
TemplateLiteralType<
|
|
515
|
+
readonly [typeof Digit1To6, ".", typeof Digit1To9, "d"]
|
|
516
|
+
>,
|
|
517
|
+
]
|
|
518
|
+
> = /*#__PURE__*/ union(
|
|
465
519
|
/*#__PURE__*/ templateLiteral(Digit1To6, "d"),
|
|
466
520
|
/*#__PURE__*/ templateLiteral(Digit1To6, ".", Digit1To9, "d"),
|
|
467
521
|
);
|
|
@@ -471,7 +525,14 @@ export type DurationLiteralDays = typeof DurationLiteralDays.Output;
|
|
|
471
525
|
* Weeks duration: `"1w"` to `"51w"` or `"1.1w"` to `"51.9w"`. See
|
|
472
526
|
* {@link DurationLiteral}.
|
|
473
527
|
*/
|
|
474
|
-
export const DurationLiteralWeeks
|
|
528
|
+
export const DurationLiteralWeeks: UnionType<
|
|
529
|
+
readonly [
|
|
530
|
+
TemplateLiteralType<readonly [typeof Digit1To51, "w"]>,
|
|
531
|
+
TemplateLiteralType<
|
|
532
|
+
readonly [typeof Digit1To51, ".", typeof Digit1To9, "w"]
|
|
533
|
+
>,
|
|
534
|
+
]
|
|
535
|
+
> = /*#__PURE__*/ union(
|
|
475
536
|
/*#__PURE__*/ templateLiteral(Digit1To51, "w"),
|
|
476
537
|
/*#__PURE__*/ templateLiteral(Digit1To51, ".", Digit1To9, "w"),
|
|
477
538
|
);
|
|
@@ -481,12 +542,38 @@ export type DurationLiteralWeeks = typeof DurationLiteralWeeks.Output;
|
|
|
481
542
|
* Years duration: `"1y"` to `"99y"` or `"1.1y"` to `"99.9y"`. See
|
|
482
543
|
* {@link DurationLiteral}.
|
|
483
544
|
*/
|
|
484
|
-
export const DurationLiteralYears
|
|
545
|
+
export const DurationLiteralYears: UnionType<
|
|
546
|
+
readonly [
|
|
547
|
+
TemplateLiteralType<readonly [typeof Digit1To99, "y"]>,
|
|
548
|
+
TemplateLiteralType<
|
|
549
|
+
readonly [typeof Digit1To99, ".", typeof Digit1To9, "y"]
|
|
550
|
+
>,
|
|
551
|
+
]
|
|
552
|
+
> = /*#__PURE__*/ union(
|
|
485
553
|
/*#__PURE__*/ templateLiteral(Digit1To99, "y"),
|
|
486
554
|
/*#__PURE__*/ templateLiteral(Digit1To99, ".", Digit1To9, "y"),
|
|
487
555
|
);
|
|
488
556
|
export type DurationLiteralYears = typeof DurationLiteralYears.Output;
|
|
489
557
|
|
|
558
|
+
export type DurationLiteral =
|
|
559
|
+
| DurationLiteralMilliseconds
|
|
560
|
+
| DurationLiteralSeconds
|
|
561
|
+
| DurationLiteralMinutes
|
|
562
|
+
| DurationLiteralHours
|
|
563
|
+
| DurationLiteralDays
|
|
564
|
+
| DurationLiteralWeeks
|
|
565
|
+
| DurationLiteralYears;
|
|
566
|
+
|
|
567
|
+
const durationLiteralSyntax = /*#__PURE__*/ union(
|
|
568
|
+
DurationLiteralMilliseconds,
|
|
569
|
+
DurationLiteralSeconds,
|
|
570
|
+
DurationLiteralMinutes,
|
|
571
|
+
DurationLiteralHours,
|
|
572
|
+
DurationLiteralDays,
|
|
573
|
+
DurationLiteralWeeks,
|
|
574
|
+
DurationLiteralYears,
|
|
575
|
+
);
|
|
576
|
+
|
|
490
577
|
/**
|
|
491
578
|
* Duration literal Type with compile-time and runtime validation.
|
|
492
579
|
*
|
|
@@ -514,17 +601,55 @@ export type DurationLiteralYears = typeof DurationLiteralYears.Output;
|
|
|
514
601
|
*
|
|
515
602
|
* See {@link Duration} for a type that also accepts {@link Millis}. Use
|
|
516
603
|
* {@link durationToMillis} to convert to milliseconds.
|
|
604
|
+
*
|
|
605
|
+
* Invalid values produce a {@link DurationLiteralError}.
|
|
606
|
+
*
|
|
607
|
+
* ### Example
|
|
608
|
+
*
|
|
609
|
+
* ```ts
|
|
610
|
+
* import {
|
|
611
|
+
* assertFalse,
|
|
612
|
+
* assertOk,
|
|
613
|
+
* assertType,
|
|
614
|
+
* DurationLiteral,
|
|
615
|
+
* } from "@evolu/common";
|
|
616
|
+
*
|
|
617
|
+
* // The TypeScript type accepts valid spellings and rejects the rest.
|
|
618
|
+
* const literal: DurationLiteral = "1.5s";
|
|
619
|
+
* assertType<Extract<DurationLiteral, "1000ms" | "60s" | "0s">, never>();
|
|
620
|
+
*
|
|
621
|
+
* // The runtime Type validates the same grammar.
|
|
622
|
+
* assertOk(DurationLiteral.fromUnknown(literal), "1.5s");
|
|
623
|
+
* assertFalse(DurationLiteral.is("1000ms"));
|
|
624
|
+
* ```
|
|
517
625
|
*/
|
|
518
|
-
export const DurationLiteral
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
626
|
+
export const DurationLiteral: Type<
|
|
627
|
+
"DurationLiteral",
|
|
628
|
+
DurationLiteral,
|
|
629
|
+
DurationLiteral,
|
|
630
|
+
DurationLiteralError
|
|
631
|
+
> = /*#__PURE__*/ createTypeWithError(
|
|
632
|
+
"DurationLiteral",
|
|
633
|
+
durationLiteralSyntax,
|
|
634
|
+
(cause, value): DurationLiteralError => ({
|
|
635
|
+
type: "DurationLiteral",
|
|
636
|
+
value,
|
|
637
|
+
cause,
|
|
638
|
+
}),
|
|
639
|
+
(error) =>
|
|
640
|
+
`The value ${safelyStringifyUnknownValue(error.value)} is not a duration literal. Use a value such as "500ms" or "1.5s".`,
|
|
526
641
|
);
|
|
527
|
-
|
|
642
|
+
|
|
643
|
+
/** Error returned when {@link DurationLiteral} rejects a value. */
|
|
644
|
+
export interface DurationLiteralError extends TypeError<"DurationLiteral"> {
|
|
645
|
+
readonly value: unknown;
|
|
646
|
+
/**
|
|
647
|
+
* The underlying union failure, retained for diagnostics.
|
|
648
|
+
*
|
|
649
|
+
* With `{ errors: "all" }`, includes every failed alternative.
|
|
650
|
+
*/
|
|
651
|
+
readonly cause: UnionError;
|
|
652
|
+
}
|
|
528
653
|
|
|
529
654
|
/**
|
|
530
655
|
* Converts a duration to milliseconds.
|