@octoii-works/octoii-std 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +85 -2
  4. package/THIRD_PARTY_NOTICES +27 -0
  5. package/dist/async/backoff.d.ts +13 -0
  6. package/dist/async/backoff.js +9 -0
  7. package/dist/async/clock.d.ts +14 -0
  8. package/dist/async/clock.js +29 -0
  9. package/dist/async/index.d.ts +4 -0
  10. package/dist/async/index.js +4 -0
  11. package/dist/async/signals.d.ts +23 -0
  12. package/dist/async/signals.js +58 -0
  13. package/dist/constants.d.ts +28 -0
  14. package/dist/constants.js +30 -0
  15. package/dist/delta.d.ts +19 -0
  16. package/dist/delta.js +116 -0
  17. package/dist/encoding.d.ts +35 -0
  18. package/dist/encoding.js +140 -0
  19. package/dist/errors.d.ts +10 -0
  20. package/dist/errors.js +27 -0
  21. package/dist/hash.d.ts +25 -0
  22. package/dist/hash.js +167 -0
  23. package/dist/id.d.ts +13 -0
  24. package/dist/id.js +48 -0
  25. package/dist/json-rpc/index.d.ts +3 -0
  26. package/dist/json-rpc/index.js +3 -0
  27. package/dist/json-rpc/messages.d.ts +36 -0
  28. package/dist/json-rpc/messages.js +45 -0
  29. package/dist/json.d.ts +41 -0
  30. package/dist/json.js +155 -0
  31. package/dist/schema.d.ts +26 -0
  32. package/dist/schema.js +67 -0
  33. package/dist/sqlite/bun.d.ts +7 -0
  34. package/dist/sqlite/bun.js +61 -0
  35. package/dist/sqlite/file-pragmas.d.ts +7 -0
  36. package/dist/sqlite/file-pragmas.js +11 -0
  37. package/dist/sqlite/index.d.ts +46 -0
  38. package/dist/sqlite/index.js +22 -0
  39. package/dist/sqlite/node.d.ts +3 -0
  40. package/dist/sqlite/node.js +62 -0
  41. package/dist/sqlite/worker.d.ts +24 -0
  42. package/dist/sqlite/worker.js +46 -0
  43. package/dist/sse.d.ts +35 -0
  44. package/dist/sse.js +101 -0
  45. package/dist/testing/index.d.ts +2 -0
  46. package/dist/testing/index.js +2 -0
  47. package/dist/testing/portability.d.ts +29 -0
  48. package/dist/testing/portability.js +167 -0
  49. package/dist/text/diff.d.ts +24 -0
  50. package/dist/text/diff.js +197 -0
  51. package/dist/text/edit-text.d.ts +38 -0
  52. package/dist/text/edit-text.js +112 -0
  53. package/dist/text/index.d.ts +6 -0
  54. package/dist/text/index.js +6 -0
  55. package/dist/text/lines.d.ts +64 -0
  56. package/dist/text/lines.js +160 -0
  57. package/dist/text/text.d.ts +22 -0
  58. package/dist/text/text.js +89 -0
  59. package/dist/url.d.ts +39 -0
  60. package/dist/url.js +238 -0
  61. package/package.json +102 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ The notable changes of `@octoii-works/octoii-std`, newest first.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.0.1] - 2026-10-31
8
+
9
+ - First release: The general-purpose base library of Octoii: JSON, deltas, encodings, ids, schemas, async helpers, bounded text, SQLite, SSE and JSON-RPC.
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.
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # @octoii-works/octoii-std
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The base every Octoii package stands on: JSON values and their changes, text encodings, hashes, ids, TypeBox
4
+ validation, time and cancellation, a portable SQLite connection, Server-Sent Events, JSON-RPC 2.0, addresses and text
5
+ diffs. It knows no agent, model or tool, and the same code runs on Bun, Node, browsers, workerd and the Hermes of
6
+ React Native.
7
+
8
+ Contents: [Where it sits](#where-it-sits) · [The subpaths](#the-subpaths) · [Everything from the caller](#everything-from-the-caller) ·
9
+ [SQLite](#sqlite) · [Constants](#constants) · [Testing](#testing).
10
+
11
+ ## Where it sits
12
+
13
+ ```sh
14
+ bun add @octoii-works/octoii-std
15
+ ```
16
+
17
+ - **At the bottom.** std depends on TypeBox alone, and every other Octoii package depends on it.
18
+ - **No root entry.** Every import names its subpath, so a program loads only what it uses:
19
+
20
+ ```ts
21
+ import { parseJson, type JsonValue } from "@octoii-works/octoii-std/json";
22
+ import { matches } from "@octoii-works/octoii-std/schema";
23
+ ```
24
+
25
+ ## The subpaths
26
+
27
+ | Subpath | What it gives |
28
+ | --- | --- |
29
+ | `/json` | The JSON value model every package shares, `JsonValue` and `JsonObject`, with checks, `jsonEqual`, `copyJson` and a `parseJson` whose keys are own data only |
30
+ | `/delta` | How a JSON value changed, as operations: `diffJson` and `applyOps` |
31
+ | `/encoding` | UTF-8, base64, base64url and hex, strict when asked, so a value read back is the value written |
32
+ | `/hash` | SHA-256 in plain JavaScript, synchronous on every platform, and HMAC, PKCE and the check of an RSA signature on it |
33
+ | `/id` | UUIDs of versions 4 and 7, from the randomness and the time the caller passes |
34
+ | `/schema` | TypeBox validators compiled once per schema, `matches`, `parsed` and `validationProblems`, with the same results where code cannot be generated |
35
+ | `/async` | Clocks, `sleep`, timeouts (`withinLimit`), cancellation (`abortable`, `linkedSignal`) and backoff |
36
+ | `/errors` | `errorMessage` for an unknown error, and `unreachable` |
37
+ | `/sqlite` | A synchronous SQLite connection, `SqlDriver`, and consecutive schema migrations; its adapters are `/sqlite/bun`, `/sqlite/node` and `/sqlite/worker` |
38
+ | `/sse` | An incremental Server-Sent Events decoder that does no I/O |
39
+ | `/json-rpc` | JSON-RPC 2.0 messages, their checks, and the error codes as `JSON_RPC_ERRORS` |
40
+ | `/url` | Absolute addresses parsed and normalized without a platform URL parser, and their queries |
41
+ | `/text` | Text as editing tools read it: sizes and line counts, line endings, line diffs and the unified diff, and an edit's tiers |
42
+ | `/testing` | `portabilityProblems`, the scan of platform-neutral sources against what Hermes lacks |
43
+
44
+ ## Everything from the caller
45
+
46
+ std reads no clock, timer, randomness or environment of its own: a function that needs one takes it.
47
+
48
+ ```ts
49
+ import { systemClock, withinLimit } from "@octoii-works/octoii-std/async";
50
+ import { uuidv7 } from "@octoii-works/octoii-std/id";
51
+
52
+ const random = (length: number) => crypto.getRandomValues(new Uint8Array(length));
53
+ const id = uuidv7(systemClock.now(), random);
54
+ const answer = await withinLimit(fetch("https://example.com"), 5_000, systemClock);
55
+ if ("timedOut" in answer) console.log("gave up after 5 s");
56
+ ```
57
+
58
+ So a test passes a fake clock and fixed bytes, and gets the same results every run.
59
+
60
+ ## SQLite
61
+
62
+ ```ts
63
+ import { bunSqlite } from "@octoii-works/octoii-std/sqlite/bun";
64
+ import { migrate } from "@octoii-works/octoii-std/sqlite";
65
+
66
+ const db = bunSqlite("app.db", { durability: "full" });
67
+ migrate(db, [
68
+ "CREATE TABLE notes (id TEXT PRIMARY KEY, text TEXT NOT NULL)",
69
+ "ALTER TABLE notes ADD COLUMN at INTEGER",
70
+ ]);
71
+ ```
72
+
73
+ - **One interface, three platforms.** `bunSqlite` on Bun, `nodeSqlite` on Node, `durableObjectSql` in a Durable Object;
74
+ code written against `SqlDriver` runs on all three.
75
+ - **Synchronous.** Every statement returns its rows, so a transaction never waits in the middle.
76
+ - **Migrations count on.** `migrate` runs the statements past the schema's version, each once, in order.
77
+
78
+ ## Constants
79
+
80
+ std's codes and limits are in one module, and each subpath gives those of its own, such as `JSON_RPC_ERRORS` from
81
+ `/json-rpc`, `DIFF_CONTEXT_LINES` from `/text` and `PKCE_VERIFIER_BYTES` from `/hash`.
82
+
83
+ ## Testing
84
+
85
+ Each module's tests run on Bun and on Node. `portabilityProblems` from `/testing` is what every Octoii package's tests
86
+ use to keep platform-neutral code within what Hermes runs.
@@ -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,4 @@
1
+ /** Time, waits, cancellation and backoff, on the clock and signals a caller passes. */
2
+ export { backoffDelay } from "./backoff.js";
3
+ export { 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;
@@ -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>;