@octoii-works/octoii-std 0.0.0-stage → 0.0.1
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES +27 -0
- package/dist/async/backoff.d.ts +13 -0
- package/dist/async/backoff.js +9 -0
- package/dist/async/clock.d.ts +14 -0
- package/dist/async/clock.js +29 -0
- package/dist/async/index.d.ts +4 -0
- package/dist/async/index.js +4 -0
- package/dist/async/signals.d.ts +23 -0
- package/dist/async/signals.js +58 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +30 -0
- package/dist/delta.d.ts +19 -0
- package/dist/delta.js +116 -0
- package/dist/encoding.d.ts +35 -0
- package/dist/encoding.js +140 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +27 -0
- package/dist/hash.d.ts +25 -0
- package/dist/hash.js +167 -0
- package/dist/id.d.ts +13 -0
- package/dist/id.js +48 -0
- package/dist/json-rpc/index.d.ts +3 -0
- package/dist/json-rpc/index.js +3 -0
- package/dist/json-rpc/messages.d.ts +36 -0
- package/dist/json-rpc/messages.js +45 -0
- package/dist/json.d.ts +41 -0
- package/dist/json.js +155 -0
- package/dist/schema.d.ts +26 -0
- package/dist/schema.js +67 -0
- package/dist/sqlite/bun.d.ts +7 -0
- package/dist/sqlite/bun.js +61 -0
- package/dist/sqlite/file-pragmas.d.ts +7 -0
- package/dist/sqlite/file-pragmas.js +11 -0
- package/dist/sqlite/index.d.ts +46 -0
- package/dist/sqlite/index.js +22 -0
- package/dist/sqlite/node.d.ts +3 -0
- package/dist/sqlite/node.js +62 -0
- package/dist/sqlite/worker.d.ts +24 -0
- package/dist/sqlite/worker.js +46 -0
- package/dist/sse.d.ts +35 -0
- package/dist/sse.js +101 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/portability.d.ts +29 -0
- package/dist/testing/portability.js +167 -0
- package/dist/text/diff.d.ts +24 -0
- package/dist/text/diff.js +197 -0
- package/dist/text/edit-text.d.ts +38 -0
- package/dist/text/edit-text.js +112 -0
- package/dist/text/index.d.ts +6 -0
- package/dist/text/index.js +6 -0
- package/dist/text/lines.d.ts +64 -0
- package/dist/text/lines.js +160 -0
- package/dist/text/text.d.ts +22 -0
- package/dist/text/text.js +89 -0
- package/dist/url.d.ts +39 -0
- package/dist/url.js +238 -0
- package/package.json +102 -4
- package/README.md +0 -3
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 octoii
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@octoii-works/octoii-std includes code adapted from the following project.
|
|
2
|
+
|
|
3
|
+
--------------------------------------------------------------------------------
|
|
4
|
+
pi (https://github.com/earendil-works/pi)
|
|
5
|
+
Adapted in: src/text/truncate.ts (truncation by lines and bytes)
|
|
6
|
+
|
|
7
|
+
MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 Mario Zechner
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BackoffOptions = {
|
|
2
|
+
/** The delay before the second attempt, which doubles with each attempt after it. */
|
|
3
|
+
readonly baseDelayMs: number;
|
|
4
|
+
/** The longest the doubling delay grows. */
|
|
5
|
+
readonly maxDelayMs: number;
|
|
6
|
+
/** How far a delay strays, from 0 to 1: 0.2 places it within ±20%. */
|
|
7
|
+
readonly jitter: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The delay before the attempt after attempt `attempt` (from 1), in whole ms: doubling from `baseDelayMs` up to
|
|
11
|
+
* `maxDelayMs`, strayed by `random()` in [0, 1); never below `floorMs`, such as a server's Retry-After.
|
|
12
|
+
*/
|
|
13
|
+
export declare function backoffDelay(attempt: number, options: BackoffOptions, random: () => number, floorMs?: number): number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delay before the attempt after attempt `attempt` (from 1), in whole ms: doubling from `baseDelayMs` up to
|
|
3
|
+
* `maxDelayMs`, strayed by `random()` in [0, 1); never below `floorMs`, such as a server's Retry-After.
|
|
4
|
+
*/
|
|
5
|
+
export function backoffDelay(attempt, options, random, floorMs = 0) {
|
|
6
|
+
const doubled = Math.min(options.baseDelayMs * 2 ** (attempt - 1), options.maxDelayMs);
|
|
7
|
+
const strayed = doubled * (1 - options.jitter + 2 * options.jitter * random());
|
|
8
|
+
return Math.ceil(Math.max(strayed, floorMs));
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Time and timers, injected: the host's clock in production, a simulated one in tests. */
|
|
2
|
+
export type Clock = {
|
|
3
|
+
/** The time, in ms since the epoch. */
|
|
4
|
+
now(): number;
|
|
5
|
+
/** Runs `callback` after `delayMs`, never synchronously, and returns a function that cancels it. */
|
|
6
|
+
setTimer(callback: () => void, delayMs: number): () => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* `Date.now` and `setTimeout`: the one object in `std` that reads the platform's clock and timers, for a program's edge,
|
|
10
|
+
* such as a host, to pass in. No function falls back to it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const systemClock: Clock;
|
|
13
|
+
/** Resolves after `ms` on `clock`'s timer; rejects with the signal's reason once `signal` aborts, cancelling the timer. */
|
|
14
|
+
export declare function sleep(clock: Clock, ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Date.now` and `setTimeout`: the one object in `std` that reads the platform's clock and timers, for a program's edge,
|
|
3
|
+
* such as a host, to pass in. No function falls back to it.
|
|
4
|
+
*/
|
|
5
|
+
export const systemClock = {
|
|
6
|
+
now: () => Date.now(),
|
|
7
|
+
setTimer: (callback, delayMs) => {
|
|
8
|
+
const timer = setTimeout(callback, delayMs);
|
|
9
|
+
return () => clearTimeout(timer);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
/** Resolves after `ms` on `clock`'s timer; rejects with the signal's reason once `signal` aborts, cancelling the timer. */
|
|
13
|
+
export function sleep(clock, ms, signal) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (signal?.aborted === true) {
|
|
16
|
+
reject(signal.reason);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
cancel();
|
|
21
|
+
reject(signal?.reason);
|
|
22
|
+
};
|
|
23
|
+
const cancel = clock.setTimer(() => {
|
|
24
|
+
signal?.removeEventListener("abort", onAbort);
|
|
25
|
+
resolve();
|
|
26
|
+
}, ms);
|
|
27
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Time, waits, cancellation and backoff, on the clock and signals a caller passes. */
|
|
2
|
+
export { type BackoffOptions, backoffDelay } from "./backoff.js";
|
|
3
|
+
export { type Clock, sleep, systemClock } from "./clock.js";
|
|
4
|
+
export { abortable, linkedSignal, withinLimit } from "./signals.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Clock } from "./clock.js";
|
|
2
|
+
/**
|
|
3
|
+
* A signal that aborts as soon as any of `signals` does, with its reason; `release` stops listening to them once the
|
|
4
|
+
* work is done. Written out because `AbortSignal.any` is not everywhere.
|
|
5
|
+
*/
|
|
6
|
+
export declare function linkedSignal(signals: readonly (AbortSignal | undefined)[]): {
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
release(): void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Waits for `promise` until it settles or `signal` aborts. Aborting rejects only this wait with the signal's reason; it
|
|
12
|
+
* does not cancel the work behind the promise.
|
|
13
|
+
*/
|
|
14
|
+
export declare function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Waits for `promise` at most `ms` by `clock`: its value, or `timedOut` once the time is up, while the work goes on
|
|
17
|
+
* unobserved. A promise that rejects in time rejects the wait.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withinLimit<T>(promise: Promise<T>, ms: number, clock: Clock): Promise<{
|
|
20
|
+
readonly value: T;
|
|
21
|
+
} | {
|
|
22
|
+
readonly timedOut: true;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { sleep } from "./clock.js";
|
|
2
|
+
/**
|
|
3
|
+
* A signal that aborts as soon as any of `signals` does, with its reason; `release` stops listening to them once the
|
|
4
|
+
* work is done. Written out because `AbortSignal.any` is not everywhere.
|
|
5
|
+
*/
|
|
6
|
+
export function linkedSignal(signals) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const present = signals.filter((signal) => signal !== undefined);
|
|
9
|
+
const aborted = present.find((signal) => signal.aborted);
|
|
10
|
+
if (aborted !== undefined) {
|
|
11
|
+
controller.abort(aborted.reason);
|
|
12
|
+
return { signal: controller.signal, release: () => undefined };
|
|
13
|
+
}
|
|
14
|
+
const listeners = present.map((signal) => {
|
|
15
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
16
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
17
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
18
|
+
});
|
|
19
|
+
return { signal: controller.signal, release: () => listeners.forEach((stop) => stop()) };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Waits for `promise` until it settles or `signal` aborts. Aborting rejects only this wait with the signal's reason; it
|
|
23
|
+
* does not cancel the work behind the promise.
|
|
24
|
+
*/
|
|
25
|
+
export function abortable(promise, signal) {
|
|
26
|
+
if (signal === undefined)
|
|
27
|
+
return promise;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const onAbort = () => reject(signal.reason);
|
|
30
|
+
promise.then((value) => {
|
|
31
|
+
signal.removeEventListener("abort", onAbort);
|
|
32
|
+
resolve(value);
|
|
33
|
+
}, (error) => {
|
|
34
|
+
signal.removeEventListener("abort", onAbort);
|
|
35
|
+
reject(error);
|
|
36
|
+
});
|
|
37
|
+
if (signal.aborted)
|
|
38
|
+
onAbort();
|
|
39
|
+
else
|
|
40
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Waits for `promise` at most `ms` by `clock`: its value, or `timedOut` once the time is up, while the work goes on
|
|
45
|
+
* unobserved. A promise that rejects in time rejects the wait.
|
|
46
|
+
*/
|
|
47
|
+
export async function withinLimit(promise, ms, clock) {
|
|
48
|
+
const timer = new AbortController();
|
|
49
|
+
const timeout = sleep(clock, ms, timer.signal).then(() => ({ timedOut: true }),
|
|
50
|
+
// The sleep is aborted once the promise settles; the race is decided by then.
|
|
51
|
+
() => ({ timedOut: true }));
|
|
52
|
+
try {
|
|
53
|
+
return await Promise.race([promise.then((value) => ({ value })), timeout]);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
timer.abort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* std's constants, all of them: the codes and names its modules give, and their limits. Each subpath gives those of its
|
|
3
|
+
* own module.
|
|
4
|
+
*/
|
|
5
|
+
/** JSON-RPC 2.0's error codes. */
|
|
6
|
+
export declare const JSON_RPC_ERRORS: {
|
|
7
|
+
readonly parseError: -32700;
|
|
8
|
+
readonly invalidRequest: -32600;
|
|
9
|
+
readonly methodNotFound: -32601;
|
|
10
|
+
readonly invalidParams: -32602;
|
|
11
|
+
readonly internalError: -32603;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The key a SQLite worker keeps a database's schema version under, in the object's key-value storage, since the SQL API
|
|
15
|
+
* takes no `PRAGMA user_version`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SQLITE_SCHEMA_VERSION_KEY = "std.sqlite.schema-version";
|
|
18
|
+
/** The unchanged lines around each change of a diff. */
|
|
19
|
+
export declare const DIFF_CONTEXT_LINES = 3;
|
|
20
|
+
/**
|
|
21
|
+
* The most edits Myers' search tries before it takes the changed middle as removed and added whole: it bounds the time
|
|
22
|
+
* a diff of two very different files takes, at the cost of a longer diff.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DIFF_MAX_EDITS = 1000;
|
|
25
|
+
/** How long a statement waits for another connection's lock before it fails, in ms. */
|
|
26
|
+
export declare const BUSY_TIMEOUT_MS = 5000;
|
|
27
|
+
/** The random bytes of a PKCE verifier: 32, which base64url writes as 43 characters, the fewest RFC 7636 allows. */
|
|
28
|
+
export declare const PKCE_VERIFIER_BYTES = 32;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* std's constants, all of them: the codes and names its modules give, and their limits. Each subpath gives those of its
|
|
3
|
+
* own module.
|
|
4
|
+
*/
|
|
5
|
+
// Codes and names.
|
|
6
|
+
/** JSON-RPC 2.0's error codes. */
|
|
7
|
+
export const JSON_RPC_ERRORS = {
|
|
8
|
+
parseError: -32700,
|
|
9
|
+
invalidRequest: -32600,
|
|
10
|
+
methodNotFound: -32601,
|
|
11
|
+
invalidParams: -32602,
|
|
12
|
+
internalError: -32603,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The key a SQLite worker keeps a database's schema version under, in the object's key-value storage, since the SQL API
|
|
16
|
+
* takes no `PRAGMA user_version`.
|
|
17
|
+
*/
|
|
18
|
+
export const SQLITE_SCHEMA_VERSION_KEY = "std.sqlite.schema-version";
|
|
19
|
+
// Numbers.
|
|
20
|
+
/** The unchanged lines around each change of a diff. */
|
|
21
|
+
export const DIFF_CONTEXT_LINES = 3;
|
|
22
|
+
/**
|
|
23
|
+
* The most edits Myers' search tries before it takes the changed middle as removed and added whole: it bounds the time
|
|
24
|
+
* a diff of two very different files takes, at the cost of a longer diff.
|
|
25
|
+
*/
|
|
26
|
+
export const DIFF_MAX_EDITS = 1_000;
|
|
27
|
+
/** How long a statement waits for another connection's lock before it fails, in ms. */
|
|
28
|
+
export const BUSY_TIMEOUT_MS = 5000;
|
|
29
|
+
/** The random bytes of a PKCE verifier: 32, which base64url writes as 43 characters, the fewest RFC 7636 allows. */
|
|
30
|
+
export const PKCE_VERIFIER_BYTES = 32;
|
package/dist/delta.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** How a JSON value changed, as a list of operations, and the value those operations give. */
|
|
2
|
+
import { type JsonValue } from "./json.js";
|
|
3
|
+
/** Where in a JSON value: object keys and array indexes from the root. */
|
|
4
|
+
export type Path = readonly (string | number)[];
|
|
5
|
+
/**
|
|
6
|
+
* A change to a JSON value; small and plain, so it can be stored and sent too.
|
|
7
|
+
* - `set`: the value at the path, which may be a new key or the index just past an array's end; the empty path
|
|
8
|
+
* replaces the whole value.
|
|
9
|
+
* - `delete`: removes an object key.
|
|
10
|
+
* - `append`: appends text to the string at the path.
|
|
11
|
+
*/
|
|
12
|
+
export type Op = readonly ["set", Path, JsonValue] | readonly ["delete", Path] | readonly ["append", Path, string];
|
|
13
|
+
/**
|
|
14
|
+
* The operations that turn `before` into `after`: nothing when they are equal; a string that only grew becomes an
|
|
15
|
+
* `append`; objects and growing arrays are compared member by member; anything else is a `set`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function diffJson(before: JsonValue, after: JsonValue): Op[];
|
|
18
|
+
/** `value` with `ops` applied; the input is not changed, and unchanged parts are shared. */
|
|
19
|
+
export declare function applyOps(value: JsonValue | undefined, ops: readonly Op[]): JsonValue;
|
package/dist/delta.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** How a JSON value changed, as a list of operations, and the value those operations give. */
|
|
2
|
+
import { isObject, jsonEntries, jsonEqual } from "./json.js";
|
|
3
|
+
/**
|
|
4
|
+
* The operations that turn `before` into `after`: nothing when they are equal; a string that only grew becomes an
|
|
5
|
+
* `append`; objects and growing arrays are compared member by member; anything else is a `set`.
|
|
6
|
+
*/
|
|
7
|
+
export function diffJson(before, after) {
|
|
8
|
+
const ops = [];
|
|
9
|
+
diffInto(before, after, [], ops);
|
|
10
|
+
return ops;
|
|
11
|
+
}
|
|
12
|
+
/** `value` with `ops` applied; the input is not changed, and unchanged parts are shared. */
|
|
13
|
+
export function applyOps(value, ops) {
|
|
14
|
+
let result = value;
|
|
15
|
+
for (const op of ops)
|
|
16
|
+
result = applyOp(result, op);
|
|
17
|
+
if (result === undefined)
|
|
18
|
+
throw new Error("The ops leave no value");
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function diffInto(before, after, path, ops) {
|
|
22
|
+
if (jsonEqual(before, after))
|
|
23
|
+
return;
|
|
24
|
+
if (typeof before === "string" && typeof after === "string" && before.length > 0 && after.startsWith(before)) {
|
|
25
|
+
ops.push(["append", path, after.slice(before.length)]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(before) && Array.isArray(after) && after.length >= before.length) {
|
|
29
|
+
before.forEach((item, index) => diffInto(item, after[index] ?? null, [...path, index], ops));
|
|
30
|
+
for (let index = before.length; index < after.length; index++)
|
|
31
|
+
ops.push(["set", [...path, index], after[index] ?? null]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (isObject(before) && isObject(after)) {
|
|
35
|
+
for (const [key] of jsonEntries(before))
|
|
36
|
+
if (ownMember(after, key) === undefined)
|
|
37
|
+
ops.push(["delete", [...path, key]]);
|
|
38
|
+
for (const [key, value] of jsonEntries(after)) {
|
|
39
|
+
const previous = ownMember(before, key);
|
|
40
|
+
if (previous === undefined)
|
|
41
|
+
ops.push(["set", [...path, key], value]);
|
|
42
|
+
else
|
|
43
|
+
diffInto(previous, value, [...path, key], ops);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
ops.push(["set", path, after]);
|
|
48
|
+
}
|
|
49
|
+
function applyOp(value, op) {
|
|
50
|
+
const path = op[1];
|
|
51
|
+
if (path.length === 0) {
|
|
52
|
+
if (op[0] === "set")
|
|
53
|
+
return op[2];
|
|
54
|
+
if (op[0] === "append" && typeof value === "string")
|
|
55
|
+
return value + op[2];
|
|
56
|
+
throw new Error(`Cannot ${op[0]} the whole value`);
|
|
57
|
+
}
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
throw new Error(`No value at ${JSON.stringify(path)}`);
|
|
60
|
+
return updateAt(value, path, (current) => {
|
|
61
|
+
switch (op[0]) {
|
|
62
|
+
case "set":
|
|
63
|
+
return op[2];
|
|
64
|
+
case "delete":
|
|
65
|
+
return undefined;
|
|
66
|
+
case "append":
|
|
67
|
+
if (typeof current !== "string")
|
|
68
|
+
throw new Error(`No string to append to at ${JSON.stringify(path)}`);
|
|
69
|
+
return current + op[2];
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** The object's own member under `key`; undefined for a key it inherits, such as `constructor` or `__proto__`. */
|
|
74
|
+
function ownMember(object, key) {
|
|
75
|
+
return Object.hasOwn(object, key) ? object[key] : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Sets the object's own member under `key`, also for `__proto__`, which an assignment would take as its prototype. */
|
|
78
|
+
function setOwn(object, key, value) {
|
|
79
|
+
Object.defineProperty(object, key, { value, writable: true, enumerable: true, configurable: true });
|
|
80
|
+
}
|
|
81
|
+
/** A copy of `value` with the member at `path` replaced by `update`'s result; undefined removes an object key. */
|
|
82
|
+
function updateAt(value, path, update) {
|
|
83
|
+
const [segment, ...rest] = path;
|
|
84
|
+
if (segment === undefined)
|
|
85
|
+
throw new Error("An update needs a path");
|
|
86
|
+
const child = (current) => {
|
|
87
|
+
if (rest.length === 0)
|
|
88
|
+
return update(current);
|
|
89
|
+
if (current === undefined)
|
|
90
|
+
throw new Error(`No value at ${JSON.stringify(path)}`);
|
|
91
|
+
return updateAt(current, rest, update);
|
|
92
|
+
};
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
if (typeof segment !== "number" || !Number.isInteger(segment) || segment < 0 || segment > value.length) {
|
|
95
|
+
throw new Error(`Index ${String(segment)} is outside an array of ${value.length}`);
|
|
96
|
+
}
|
|
97
|
+
const next = child(value[segment]);
|
|
98
|
+
if (next === undefined)
|
|
99
|
+
throw new Error("Array items cannot be deleted; set the array instead");
|
|
100
|
+
const copy = [...value];
|
|
101
|
+
copy[segment] = next;
|
|
102
|
+
return copy;
|
|
103
|
+
}
|
|
104
|
+
if (isObject(value)) {
|
|
105
|
+
if (typeof segment !== "string")
|
|
106
|
+
throw new Error(`Index ${segment} used on an object`);
|
|
107
|
+
const next = child(ownMember(value, segment));
|
|
108
|
+
const copy = { ...value };
|
|
109
|
+
if (next === undefined)
|
|
110
|
+
delete copy[segment];
|
|
111
|
+
else
|
|
112
|
+
setOwn(copy, segment, next);
|
|
113
|
+
return copy;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`No object or array at ${JSON.stringify(path)}`);
|
|
116
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTF-8 and base64, the same on every platform. Decoding is strict when asked: invalid bytes throw rather than becoming
|
|
3
|
+
* U+FFFD, so a value read back is the value written.
|
|
4
|
+
*/
|
|
5
|
+
/** How decoding treats bytes that are no UTF-8: as U+FFFD, or, with `fatal`, as a `TypeError`. */
|
|
6
|
+
export type Utf8Options = {
|
|
7
|
+
readonly fatal?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** Decodes UTF-8 a chunk at a time: a character whose bytes are split between chunks is decoded once all have arrived. */
|
|
10
|
+
export interface Utf8Decoder {
|
|
11
|
+
/** The text of `bytes` after what earlier chunks left unfinished; `final` decodes everything that is left. */
|
|
12
|
+
decode(bytes: Uint8Array, final: boolean): string;
|
|
13
|
+
}
|
|
14
|
+
/** `text` as UTF-8; a lone surrogate becomes U+FFFD. */
|
|
15
|
+
export declare function utf8Encode(text: string): Uint8Array;
|
|
16
|
+
/** UTF-8 `bytes` as text; with `fatal`, bytes that are no UTF-8 throw a `TypeError`. */
|
|
17
|
+
export declare function utf8Decode(bytes: Uint8Array, options?: Utf8Options): string;
|
|
18
|
+
/** A streaming decoder: bytes may split a character across chunks. */
|
|
19
|
+
export declare function utf8Decoder(options?: Utf8Options): Utf8Decoder;
|
|
20
|
+
/** The UTF-8 length of `text`, without encoding it; a lone surrogate counts as the three bytes of U+FFFD. */
|
|
21
|
+
export declare function utf8ByteLength(text: string): number;
|
|
22
|
+
/** Whether `text` has no lone surrogates, so it encodes to UTF-8 and back unchanged. */
|
|
23
|
+
export declare function isWellFormed(text: string): boolean;
|
|
24
|
+
/** `text` with each lone surrogate replaced by U+FFFD. */
|
|
25
|
+
export declare function toWellFormed(text: string): string;
|
|
26
|
+
/** `bytes` as base64 with padding. */
|
|
27
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
28
|
+
/** The bytes padded base64 `text` stands for; anything else throws a `SyntaxError`. */
|
|
29
|
+
export declare function fromBase64(text: string): Uint8Array;
|
|
30
|
+
/** `bytes` as lowercase hex, two digits a byte. */
|
|
31
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
32
|
+
/** `bytes` as base64url, without padding, as URLs and PKCE take it. */
|
|
33
|
+
export declare function toBase64Url(bytes: Uint8Array): string;
|
|
34
|
+
/** The bytes base64url `text` stands for, padded or not; anything else throws a `SyntaxError`. */
|
|
35
|
+
export declare function fromBase64Url(text: string): Uint8Array<ArrayBuffer>;
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTF-8 and base64, the same on every platform. Decoding is strict when asked: invalid bytes throw rather than becoming
|
|
3
|
+
* U+FFFD, so a value read back is the value written.
|
|
4
|
+
*/
|
|
5
|
+
const ENCODER = new TextEncoder();
|
|
6
|
+
/** `text` as UTF-8; a lone surrogate becomes U+FFFD. */
|
|
7
|
+
export function utf8Encode(text) {
|
|
8
|
+
return ENCODER.encode(text);
|
|
9
|
+
}
|
|
10
|
+
/** UTF-8 `bytes` as text; with `fatal`, bytes that are no UTF-8 throw a `TypeError`. */
|
|
11
|
+
export function utf8Decode(bytes, options = {}) {
|
|
12
|
+
// `ignoreBOM: false` is the default, named because the Workers types require every option.
|
|
13
|
+
return new TextDecoder("utf-8", { fatal: options.fatal === true, ignoreBOM: false }).decode(bytes);
|
|
14
|
+
}
|
|
15
|
+
/** A streaming decoder: bytes may split a character across chunks. */
|
|
16
|
+
export function utf8Decoder(options = {}) {
|
|
17
|
+
const decoder = new TextDecoder("utf-8", { fatal: options.fatal === true, ignoreBOM: false });
|
|
18
|
+
return { decode: (bytes, final) => decoder.decode(bytes, { stream: !final }) };
|
|
19
|
+
}
|
|
20
|
+
/** The UTF-8 length of `text`, without encoding it; a lone surrogate counts as the three bytes of U+FFFD. */
|
|
21
|
+
export function utf8ByteLength(text) {
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
for (let index = 0; index < text.length; index++) {
|
|
24
|
+
const code = text.charCodeAt(index);
|
|
25
|
+
if (code <= 0x7f)
|
|
26
|
+
bytes += 1;
|
|
27
|
+
else if (code <= 0x7ff)
|
|
28
|
+
bytes += 2;
|
|
29
|
+
else if (isHighSurrogate(code) && index + 1 < text.length && isLowSurrogate(text.charCodeAt(index + 1))) {
|
|
30
|
+
bytes += 4;
|
|
31
|
+
index++;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
bytes += 3;
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
/** Whether `text` has no lone surrogates, so it encodes to UTF-8 and back unchanged. */
|
|
39
|
+
export function isWellFormed(text) {
|
|
40
|
+
for (let index = 0; index < text.length; index++) {
|
|
41
|
+
const code = text.charCodeAt(index);
|
|
42
|
+
if (isHighSurrogate(code) && index + 1 < text.length && isLowSurrogate(text.charCodeAt(index + 1)))
|
|
43
|
+
index++;
|
|
44
|
+
else if (isHighSurrogate(code) || isLowSurrogate(code))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** `text` with each lone surrogate replaced by U+FFFD. */
|
|
50
|
+
export function toWellFormed(text) {
|
|
51
|
+
if (isWellFormed(text))
|
|
52
|
+
return text;
|
|
53
|
+
let result = "";
|
|
54
|
+
for (let index = 0; index < text.length; index++) {
|
|
55
|
+
const code = text.charCodeAt(index);
|
|
56
|
+
if (isHighSurrogate(code) && index + 1 < text.length && isLowSurrogate(text.charCodeAt(index + 1))) {
|
|
57
|
+
result += text.slice(index, index + 2);
|
|
58
|
+
index++;
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
result += isHighSurrogate(code) || isLowSurrogate(code) ? "�" : text.charAt(index);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/** Base64 written out here, since not every platform has `btoa`, `atob`, `Buffer` or `Uint8Array`'s base64 methods. */
|
|
66
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
67
|
+
const SEXTETS = new Map(Array.from(ALPHABET, (character, index) => [character, index]));
|
|
68
|
+
/** How long a piece of the text grows before it is set aside: one string grown a character at a time is slow for megabytes. */
|
|
69
|
+
const PIECE_LENGTH = 8192;
|
|
70
|
+
/** `bytes` as base64 with padding. */
|
|
71
|
+
export function toBase64(bytes) {
|
|
72
|
+
const pieces = [];
|
|
73
|
+
let piece = "";
|
|
74
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
75
|
+
const first = bytes[index] ?? 0;
|
|
76
|
+
const second = bytes[index + 1];
|
|
77
|
+
const third = bytes[index + 2];
|
|
78
|
+
const group = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);
|
|
79
|
+
piece += ALPHABET.charAt(group >> 18) + ALPHABET.charAt((group >> 12) & 63);
|
|
80
|
+
piece += second === undefined ? "=" : ALPHABET.charAt((group >> 6) & 63);
|
|
81
|
+
piece += third === undefined ? "=" : ALPHABET.charAt(group & 63);
|
|
82
|
+
if (piece.length >= PIECE_LENGTH) {
|
|
83
|
+
pieces.push(piece);
|
|
84
|
+
piece = "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
pieces.push(piece);
|
|
88
|
+
return pieces.join("");
|
|
89
|
+
}
|
|
90
|
+
/** The bytes padded base64 `text` stands for; anything else throws a `SyntaxError`. */
|
|
91
|
+
export function fromBase64(text) {
|
|
92
|
+
if (text.length % 4 !== 0)
|
|
93
|
+
throw notBase64();
|
|
94
|
+
const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
95
|
+
const bytes = new Uint8Array((text.length / 4) * 3 - padding);
|
|
96
|
+
let written = 0;
|
|
97
|
+
for (let index = 0; index < text.length; index += 4) {
|
|
98
|
+
let group = 0;
|
|
99
|
+
for (let offset = 0; offset < 4; offset++) {
|
|
100
|
+
const character = text.charAt(index + offset);
|
|
101
|
+
const isPadding = character === "=" && index + offset >= text.length - padding;
|
|
102
|
+
const sextet = isPadding ? 0 : SEXTETS.get(character);
|
|
103
|
+
if (sextet === undefined)
|
|
104
|
+
throw notBase64();
|
|
105
|
+
group = (group << 6) | sextet;
|
|
106
|
+
}
|
|
107
|
+
for (const shift of [16, 8, 0]) {
|
|
108
|
+
if (written < bytes.length)
|
|
109
|
+
bytes[written++] = (group >> shift) & 255;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return bytes;
|
|
113
|
+
}
|
|
114
|
+
/** `bytes` as lowercase hex, two digits a byte. */
|
|
115
|
+
export function toHex(bytes) {
|
|
116
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
117
|
+
}
|
|
118
|
+
/** `bytes` as base64url, without padding, as URLs and PKCE take it. */
|
|
119
|
+
export function toBase64Url(bytes) {
|
|
120
|
+
return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
121
|
+
}
|
|
122
|
+
/** The bytes base64url `text` stands for, padded or not; anything else throws a `SyntaxError`. */
|
|
123
|
+
export function fromBase64Url(text) {
|
|
124
|
+
if (/[+/]/.test(text))
|
|
125
|
+
throw new SyntaxError("The text is not base64url");
|
|
126
|
+
const standard = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
127
|
+
const padded = standard.endsWith("=") ? standard : standard + "=".repeat((4 - (standard.length % 4)) % 4);
|
|
128
|
+
return new Uint8Array(fromBase64(padded));
|
|
129
|
+
}
|
|
130
|
+
function notBase64() {
|
|
131
|
+
return new SyntaxError("The text is not padded base64");
|
|
132
|
+
}
|
|
133
|
+
/** Whether a UTF-16 code unit is the first half of a surrogate pair. */
|
|
134
|
+
function isHighSurrogate(code) {
|
|
135
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
136
|
+
}
|
|
137
|
+
/** Whether a UTF-16 code unit is the second half of a surrogate pair. */
|
|
138
|
+
function isLowSurrogate(code) {
|
|
139
|
+
return code >= 0xdc00 && code <= 0xdfff;
|
|
140
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every package needs to report an unknown error. Each package defines its own error classes and codes.
|
|
3
|
+
*/
|
|
4
|
+
/** A readable message for anything thrown: an `Error`'s message, a string, or the value as JSON. */
|
|
5
|
+
export declare function errorMessage(error: unknown): string;
|
|
6
|
+
/**
|
|
7
|
+
* Throws for a value the type checker proved cannot exist, such as the case a `switch` over a discriminated union does
|
|
8
|
+
* not name: placed at the end of the `switch`, it makes a missing case a type error.
|
|
9
|
+
*/
|
|
10
|
+
export declare function unreachable(value: never): never;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every package needs to report an unknown error. Each package defines its own error classes and codes.
|
|
3
|
+
*/
|
|
4
|
+
/** A readable message for anything thrown: an `Error`'s message, a string, or the value as JSON. */
|
|
5
|
+
export function errorMessage(error) {
|
|
6
|
+
if (error instanceof Error)
|
|
7
|
+
return error.message;
|
|
8
|
+
if (typeof error === "string")
|
|
9
|
+
return error;
|
|
10
|
+
return asJson(error) ?? String(error);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Throws for a value the type checker proved cannot exist, such as the case a `switch` over a discriminated union does
|
|
14
|
+
* not name: placed at the end of the `switch`, it makes a missing case a type error.
|
|
15
|
+
*/
|
|
16
|
+
export function unreachable(value) {
|
|
17
|
+
throw new Error(`Unreachable: ${errorMessage(value)}`);
|
|
18
|
+
}
|
|
19
|
+
/** The JSON text of `value`, or undefined for a value JSON cannot hold, such as undefined, a cycle or a bigint. */
|
|
20
|
+
function asJson(value) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|