@braidlabs/skew 0.1.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.
@@ -0,0 +1,70 @@
1
+ import { VersionedEnvelope } from './versioned.js';
2
+ /**
3
+ * HTTP-native version carriage.
4
+ *
5
+ * The `{ v, payload }` body envelope is right for storage, where there is
6
+ * nothing but the bytes. HTTP already has places to carry a version — the
7
+ * URL (`/v2/funds`), a header, the media type
8
+ * (`application/vnd.acme.fund.v2+json`) — and requiring servers to reshape
9
+ * every response body just to adopt Skew is an adoption tax with no payoff.
10
+ *
11
+ * These helpers extract the version from wherever the response carries it and
12
+ * hand back a normal envelope, so the rest of the pipeline (`schema.read`)
13
+ * never knows the difference:
14
+ *
15
+ * ```ts
16
+ * const res = await fetch(`${API_BASE}/v1/funds`);
17
+ * const body = await res.json();
18
+ * const result = FundListSchema.read(envelopeFromResponse(res, body));
19
+ * ```
20
+ */
21
+ /** The subset of `Response` these helpers need — also satisfied by mocks. */
22
+ export interface ResponseLike {
23
+ readonly url?: string;
24
+ readonly headers?: {
25
+ get(name: string): string | null;
26
+ };
27
+ }
28
+ export interface VersionCarriage {
29
+ /**
30
+ * Header carrying the version. The value may be a bare integer (`2`), a
31
+ * `v=` parameter (`portfolio-fund; v=2`), or a Skew contract link
32
+ * (`</.well-known/skew/contracts/portfolio-fund>; v=2`).
33
+ */
34
+ readonly header?: string;
35
+ /** Pattern run against `content-type`; first capture group is the version. */
36
+ readonly mediaType?: RegExp;
37
+ /** Pattern run against the response URL; first capture group is the version. */
38
+ readonly urlPattern?: RegExp;
39
+ }
40
+ /** The response header a Skew-aware server uses to name its contract. */
41
+ export declare const SKEW_CONTRACT_HEADER = "skew-contract";
42
+ export interface SkewContractRef {
43
+ /** Contract document URL, when the header carried one. */
44
+ readonly url?: string;
45
+ /** Contract name, when the header carried a bare name instead of a URL. */
46
+ readonly name?: string;
47
+ readonly v?: number;
48
+ }
49
+ /**
50
+ * Parses a `Skew-Contract` header value.
51
+ *
52
+ * Accepted forms: `2` · `portfolio-fund; v=2` ·
53
+ * `</.well-known/skew/contracts/portfolio-fund>; v=2`
54
+ */
55
+ export declare function parseSkewContractHeader(value: string): SkewContractRef;
56
+ /** Formats the header the parser above accepts. */
57
+ export declare function formatSkewContractHeader(ref: SkewContractRef): string;
58
+ /**
59
+ * Extracts the contract version a response carries, or `null` when it
60
+ * carries none. Checks, in order: the header, the media type, the URL.
61
+ */
62
+ export declare function versionFromResponse(res: ResponseLike, carriage?: VersionCarriage): number | null;
63
+ /**
64
+ * Normalizes a response + parsed body into something `schema.read` accepts.
65
+ *
66
+ * - Body already enveloped → returned as-is (the writer's statement wins).
67
+ * - Version found in the response → the body wrapped at that version.
68
+ * - Neither → the bare body, unchanged, which `read` treats as legacy data.
69
+ */
70
+ export declare function envelopeFromResponse(res: ResponseLike, body: unknown, carriage?: VersionCarriage): VersionedEnvelope | unknown;
@@ -0,0 +1,76 @@
1
+ /** The response header a Skew-aware server uses to name its contract. */
2
+ export const SKEW_CONTRACT_HEADER = 'skew-contract';
3
+ const DEFAULT_CARRIAGE = {
4
+ header: SKEW_CONTRACT_HEADER,
5
+ mediaType: /vnd\.[^+;]*\.v(\d+)\+json/i,
6
+ urlPattern: /\/v(\d+)(?:\/|\?|$)/,
7
+ };
8
+ /**
9
+ * Parses a `Skew-Contract` header value.
10
+ *
11
+ * Accepted forms: `2` · `portfolio-fund; v=2` ·
12
+ * `</.well-known/skew/contracts/portfolio-fund>; v=2`
13
+ */
14
+ export function parseSkewContractHeader(value) {
15
+ var _a, _b, _c;
16
+ const trimmed = value.trim();
17
+ const ref = {};
18
+ const linked = trimmed.match(/^<([^>]+)>/);
19
+ const bare = (_b = (_a = trimmed.split(';')[0]) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
20
+ if (linked === null || linked === void 0 ? void 0 : linked[1]) {
21
+ ref.url = linked[1];
22
+ }
23
+ else if (bare && !/^\d+$/.test(bare)) {
24
+ ref.name = bare;
25
+ }
26
+ const explicit = trimmed.match(/(?:^|;)\s*v\s*=\s*(\d+)/);
27
+ const implicit = /^\d+$/.test(bare) ? bare : undefined;
28
+ const version = (_c = explicit === null || explicit === void 0 ? void 0 : explicit[1]) !== null && _c !== void 0 ? _c : implicit;
29
+ if (version !== undefined)
30
+ ref.v = Number(version);
31
+ return ref;
32
+ }
33
+ /** Formats the header the parser above accepts. */
34
+ export function formatSkewContractHeader(ref) {
35
+ var _a;
36
+ const subject = ref.url !== undefined ? `<${ref.url}>` : ((_a = ref.name) !== null && _a !== void 0 ? _a : '');
37
+ const version = ref.v !== undefined ? `v=${ref.v}` : '';
38
+ return [subject, version].filter((part) => part.length > 0).join('; ');
39
+ }
40
+ /**
41
+ * Extracts the contract version a response carries, or `null` when it
42
+ * carries none. Checks, in order: the header, the media type, the URL.
43
+ */
44
+ export function versionFromResponse(res, carriage = {}) {
45
+ var _a, _b, _c, _d, _e, _f;
46
+ const headerName = (_a = carriage.header) !== null && _a !== void 0 ? _a : DEFAULT_CARRIAGE.header;
47
+ const headerValue = (_b = res.headers) === null || _b === void 0 ? void 0 : _b.get(headerName);
48
+ if (headerValue) {
49
+ const parsed = parseSkewContractHeader(headerValue);
50
+ if (parsed.v !== undefined)
51
+ return parsed.v;
52
+ }
53
+ const contentType = (_c = res.headers) === null || _c === void 0 ? void 0 : _c.get('content-type');
54
+ const mediaMatch = contentType === null || contentType === void 0 ? void 0 : contentType.match((_d = carriage.mediaType) !== null && _d !== void 0 ? _d : DEFAULT_CARRIAGE.mediaType);
55
+ if (mediaMatch === null || mediaMatch === void 0 ? void 0 : mediaMatch[1])
56
+ return Number(mediaMatch[1]);
57
+ const urlMatch = (_e = res.url) === null || _e === void 0 ? void 0 : _e.match((_f = carriage.urlPattern) !== null && _f !== void 0 ? _f : DEFAULT_CARRIAGE.urlPattern);
58
+ if (urlMatch === null || urlMatch === void 0 ? void 0 : urlMatch[1])
59
+ return Number(urlMatch[1]);
60
+ return null;
61
+ }
62
+ /**
63
+ * Normalizes a response + parsed body into something `schema.read` accepts.
64
+ *
65
+ * - Body already enveloped → returned as-is (the writer's statement wins).
66
+ * - Version found in the response → the body wrapped at that version.
67
+ * - Neither → the bare body, unchanged, which `read` treats as legacy data.
68
+ */
69
+ export function envelopeFromResponse(res, body, carriage = {}) {
70
+ if (typeof body === 'object' && body !== null && typeof body.v === 'number' && 'payload' in body) {
71
+ return body;
72
+ }
73
+ const version = versionFromResponse(res, carriage);
74
+ return version === null ? body : { v: version, payload: body };
75
+ }
76
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../../../libs/skew/src/lib/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Build identity and skew negotiation.
3
+ *
4
+ * The premise: a deployed artifact needs a stable name so two independently
5
+ * deployed parties can discover that they disagree. Everything else here —
6
+ * stale chunks, contract mismatches, hydration failures — is a consequence of
7
+ * that one missing primitive.
8
+ */
9
+ /** Identity of the running client build, stamped at build time. */
10
+ export interface BuildIdentity {
11
+ readonly buildId: string;
12
+ /** ISO timestamp. Optional, but without it two builds cannot be *ordered*. */
13
+ readonly builtAt?: string;
14
+ }
15
+ /**
16
+ * The artifact an origin serves so clients can discover the current build.
17
+ * Serve it with `Cache-Control: no-store`.
18
+ */
19
+ export interface SkewManifest {
20
+ readonly buildId: string;
21
+ readonly builtAt?: string;
22
+ /**
23
+ * Optional logical-module → output-chunk map. Present, it lets a client
24
+ * distinguish "this route moved" from "this route was deleted"; absent,
25
+ * classification falls back to build identity alone.
26
+ */
27
+ readonly modules?: Readonly<Record<string, {
28
+ readonly file: string;
29
+ readonly hash?: string;
30
+ }>>;
31
+ }
32
+ /**
33
+ * Outcome of comparing the running client against the origin.
34
+ *
35
+ * `staleOrigin` is the case naïve implementations miss, and the reason a plain
36
+ * reload-on-error can brick a tab: if the origin is handing out an *older*
37
+ * build than the one already running (a cached entry document, a lagging
38
+ * region), reloading fetches the same stale bundle and fails again, forever.
39
+ */
40
+ export type SkewStatus = {
41
+ readonly kind: 'current';
42
+ readonly buildId: string;
43
+ } | {
44
+ /** A newer deployment exists. Reloading is safe and will resolve it. */
45
+ readonly kind: 'staleClient';
46
+ readonly local: string;
47
+ readonly remote: string;
48
+ readonly remoteBuiltAt?: string;
49
+ } | {
50
+ /** Origin is behind us — reloading would loop. Do not reload. */
51
+ readonly kind: 'staleOrigin';
52
+ readonly local: string;
53
+ readonly remote: string;
54
+ } | {
55
+ /** Builds differ but cannot be ordered (no timestamps to compare). */
56
+ readonly kind: 'differs';
57
+ readonly local: string;
58
+ readonly remote: string;
59
+ } | {
60
+ /** The origin could not be reached: offline, blocked, or timed out. */
61
+ readonly kind: 'unreachable';
62
+ readonly error: unknown;
63
+ };
64
+ export interface VersionProbeOptions {
65
+ /** Identity of this build. */
66
+ readonly identity: BuildIdentity;
67
+ /** URL of the manifest. Should be served `no-store`. */
68
+ readonly manifestUrl: string;
69
+ /** Injected for testing and for non-browser runtimes. */
70
+ readonly fetch?: typeof globalThis.fetch;
71
+ /** Abort the probe after this long. Default 5000ms. */
72
+ readonly timeoutMs?: number;
73
+ /**
74
+ * Minimum gap between network probes; repeat calls inside the window reuse
75
+ * the last answer. Default 10_000ms. Prevents a burst of chunk failures from
76
+ * turning into a burst of manifest requests.
77
+ */
78
+ readonly minIntervalMs?: number;
79
+ }
80
+ export interface VersionProbe {
81
+ /** Compares this build against the origin, subject to the interval cache. */
82
+ check(): Promise<SkewStatus>;
83
+ /** Last status observed, without touching the network. */
84
+ last(): SkewStatus | null;
85
+ /**
86
+ * The manifest behind the last successful probe.
87
+ *
88
+ * Exposed because classification often needs the payload, not just the
89
+ * verdict — `moduleWasRemoved()` is the motivating case, where "the route was
90
+ * deleted" and "the chunk moved" demand different recovery.
91
+ */
92
+ lastManifest(): SkewManifest | null;
93
+ /** Discards the cached answer so the next `check()` hits the network. */
94
+ invalidate(): void;
95
+ }
96
+ /**
97
+ * Creates a probe that answers "is this client still current?".
98
+ *
99
+ * ```ts
100
+ * const probe = createVersionProbe({
101
+ * identity: { buildId: BUILD_ID, builtAt: BUILT_AT },
102
+ * manifestUrl: '/skew-manifest.json',
103
+ * });
104
+ *
105
+ * const status = await probe.check();
106
+ * if (status.kind === 'staleClient') offerReload();
107
+ * if (status.kind === 'staleOrigin') doNotReload(); // would loop
108
+ * ```
109
+ */
110
+ export declare function createVersionProbe(options: VersionProbeOptions): VersionProbe;
111
+ /**
112
+ * Pure comparison of a client identity against a manifest.
113
+ *
114
+ * Ordering requires timestamps on both sides. Without them the only honest
115
+ * answer is `differs` — which callers should treat conservatively, because
116
+ * they cannot rule out the reload-loop case.
117
+ */
118
+ export declare function compareBuilds(identity: BuildIdentity, manifest: SkewManifest): SkewStatus;
119
+ /** True when a module is absent from the manifest, i.e. deleted in the new build. */
120
+ export declare function moduleWasRemoved(manifest: SkewManifest, moduleId: string): boolean;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Build identity and skew negotiation.
3
+ *
4
+ * The premise: a deployed artifact needs a stable name so two independently
5
+ * deployed parties can discover that they disagree. Everything else here —
6
+ * stale chunks, contract mismatches, hydration failures — is a consequence of
7
+ * that one missing primitive.
8
+ */
9
+ import { __awaiter } from "tslib";
10
+ /**
11
+ * Creates a probe that answers "is this client still current?".
12
+ *
13
+ * ```ts
14
+ * const probe = createVersionProbe({
15
+ * identity: { buildId: BUILD_ID, builtAt: BUILT_AT },
16
+ * manifestUrl: '/skew-manifest.json',
17
+ * });
18
+ *
19
+ * const status = await probe.check();
20
+ * if (status.kind === 'staleClient') offerReload();
21
+ * if (status.kind === 'staleOrigin') doNotReload(); // would loop
22
+ * ```
23
+ */
24
+ export function createVersionProbe(options) {
25
+ var _a;
26
+ const { identity, manifestUrl, fetch: fetchImpl = (_a = globalThis.fetch) === null || _a === void 0 ? void 0 : _a.bind(globalThis), timeoutMs = 5000, minIntervalMs = 10000, } = options;
27
+ let cached = null;
28
+ let cachedManifest = null;
29
+ let cachedAt = 0;
30
+ let inFlight = null;
31
+ function probe() {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ if (!fetchImpl) {
34
+ return { kind: 'unreachable', error: new Error('no fetch implementation available') };
35
+ }
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
38
+ try {
39
+ const response = yield fetchImpl(manifestUrl, {
40
+ cache: 'no-store',
41
+ signal: controller.signal,
42
+ });
43
+ if (!response.ok) {
44
+ return { kind: 'unreachable', error: new Error(`manifest responded ${response.status}`) };
45
+ }
46
+ const manifest = (yield response.json());
47
+ cachedManifest = manifest;
48
+ return compareBuilds(identity, manifest);
49
+ }
50
+ catch (error) {
51
+ return { kind: 'unreachable', error };
52
+ }
53
+ finally {
54
+ clearTimeout(timer);
55
+ }
56
+ });
57
+ }
58
+ return {
59
+ check() {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ const now = Date.now();
62
+ if (cached && now - cachedAt < minIntervalMs)
63
+ return cached;
64
+ // Collapse concurrent callers onto one request — a page that fails to
65
+ // load three chunks at once should still make a single probe.
66
+ if (inFlight)
67
+ return inFlight;
68
+ inFlight = probe().then((status) => {
69
+ cached = status;
70
+ cachedAt = Date.now();
71
+ inFlight = null;
72
+ return status;
73
+ });
74
+ return inFlight;
75
+ });
76
+ },
77
+ last: () => cached,
78
+ lastManifest: () => cachedManifest,
79
+ invalidate: () => {
80
+ cached = null;
81
+ cachedManifest = null;
82
+ cachedAt = 0;
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * Pure comparison of a client identity against a manifest.
88
+ *
89
+ * Ordering requires timestamps on both sides. Without them the only honest
90
+ * answer is `differs` — which callers should treat conservatively, because
91
+ * they cannot rule out the reload-loop case.
92
+ */
93
+ export function compareBuilds(identity, manifest) {
94
+ if (identity.buildId === manifest.buildId) {
95
+ return { kind: 'current', buildId: manifest.buildId };
96
+ }
97
+ const localTime = parseTime(identity.builtAt);
98
+ const remoteTime = parseTime(manifest.builtAt);
99
+ if (localTime !== null && remoteTime !== null) {
100
+ if (remoteTime > localTime) {
101
+ return manifest.builtAt === undefined
102
+ ? { kind: 'staleClient', local: identity.buildId, remote: manifest.buildId }
103
+ : {
104
+ kind: 'staleClient',
105
+ local: identity.buildId,
106
+ remote: manifest.buildId,
107
+ remoteBuiltAt: manifest.builtAt,
108
+ };
109
+ }
110
+ if (remoteTime < localTime) {
111
+ return { kind: 'staleOrigin', local: identity.buildId, remote: manifest.buildId };
112
+ }
113
+ }
114
+ return { kind: 'differs', local: identity.buildId, remote: manifest.buildId };
115
+ }
116
+ function parseTime(iso) {
117
+ if (!iso)
118
+ return null;
119
+ const ms = Date.parse(iso);
120
+ return Number.isNaN(ms) ? null : ms;
121
+ }
122
+ /** True when a module is absent from the manifest, i.e. deleted in the new build. */
123
+ export function moduleWasRemoved(manifest, moduleId) {
124
+ return manifest.modules !== undefined && manifest.modules[moduleId] === undefined;
125
+ }
126
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../../../../libs/skew/src/lib/identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ import type { MigrationContext } from './context.js';
2
+ /**
3
+ * Declarative migration ops — the data-driven alternative to closure
4
+ * migrations.
5
+ *
6
+ * A closure can express any migration but can only travel inside the bundle
7
+ * that compiled it. An ops list is data: it can be published by an API,
8
+ * fetched by a build that has never seen the newer shape, audited by a human,
9
+ * and — because every op is structural — **mechanically inverted**, so
10
+ * declaring the up-migration buys the down-migration for free.
11
+ *
12
+ * The op set is deliberately closed and non-Turing-complete. Nothing fetched
13
+ * over the network is ever executed as code; it is interpreted, op by op,
14
+ * against a whitelist. Semantic transforms ("split this name into first and
15
+ * last") do not belong here — they stay as named code steps, and a reader
16
+ * that lacks the named function degrades loudly rather than guessing.
17
+ *
18
+ * Directionality: ops describe the **up** migration (older shape → newer
19
+ * shape). The inverse of each op, where one exists, is derived:
20
+ *
21
+ * | op | up (old → new) | down (new → old) |
22
+ * | --------- | --------------------------------------- | ------------------------------------ |
23
+ * | `rename` | move value `from` → `to` | move value `to` → `from` |
24
+ * | `move` | alias of `rename` | reversed |
25
+ * | `wrap` | scalar → `{ [key]: scalar, ...also }` | object → its `[key]`; `also` dropped |
26
+ * | `hoist` | object → its `[key]` member | `[key]` re-wrapped (siblings lost) |
27
+ * | `map` | sub-ops over each array element | inverted sub-ops, reversed |
28
+ * | `default` | set `path` when absent | delete `path` (lossy) |
29
+ * | `drop` | delete `path` | set `restore` if given, else none |
30
+ * | `convert` | coerce / map the value at `path` | opposite coercion / inverted map |
31
+ * | `const` | set `path` unconditionally | delete `path` (lossy) |
32
+ */
33
+ /**
34
+ * A value computed while applying an op:
35
+ * - `{ $now: true }` — `ctx.now().toISOString()`. Derived by definition.
36
+ * - `{ $from: 'path' }` — copied from another field. A leading `/` resolves
37
+ * against the document root (useful inside `map`, where the scope is the
38
+ * array element); otherwise against the current scope. Real data, not a
39
+ * guess.
40
+ * - `{ $value: x }` — the literal `x`, for literals that would otherwise be
41
+ * mistaken for one of the specs above. Derived by definition.
42
+ * - anything else — itself, as a literal. Derived by definition.
43
+ */
44
+ export type LensValueSpec = {
45
+ readonly $now: true;
46
+ } | {
47
+ readonly $from: string;
48
+ } | {
49
+ readonly $value: unknown;
50
+ } | unknown;
51
+ export type LensOp = {
52
+ readonly rename: {
53
+ readonly from: string;
54
+ readonly to: string;
55
+ };
56
+ } | {
57
+ readonly move: {
58
+ readonly from: string;
59
+ readonly to: string;
60
+ };
61
+ } | {
62
+ readonly wrap: {
63
+ readonly path: string;
64
+ readonly key: string;
65
+ readonly also?: Readonly<Record<string, LensValueSpec>>;
66
+ };
67
+ } | {
68
+ readonly hoist: {
69
+ readonly path: string;
70
+ readonly key: string;
71
+ };
72
+ } | {
73
+ readonly map: {
74
+ readonly path: string;
75
+ readonly ops: readonly LensOp[];
76
+ };
77
+ } | {
78
+ readonly default: {
79
+ readonly path: string;
80
+ readonly value: unknown;
81
+ /** Defaults to `true` — a filled-in default is a guess by definition. */
82
+ readonly derived?: boolean;
83
+ };
84
+ } | {
85
+ readonly drop: {
86
+ readonly path: string;
87
+ /**
88
+ * Value the down-migration reinstates for the field the newer shape
89
+ * removed. Without it the step cannot travel downward at all.
90
+ */
91
+ readonly restore?: unknown;
92
+ };
93
+ } | {
94
+ readonly convert: {
95
+ readonly path: string;
96
+ readonly to: 'string' | 'number';
97
+ /**
98
+ * Explicit value mapping (keys are `String(oldValue)`). Invertible
99
+ * only when bijective. Without `via`, `'string'` and `'number'` are
100
+ * treated as inverses of each other.
101
+ */
102
+ readonly via?: Readonly<Record<string, unknown>>;
103
+ };
104
+ } | {
105
+ readonly const: {
106
+ readonly path: string;
107
+ readonly value: unknown;
108
+ /** Defaults to `false` — use `true` when the constant is a placeholder. */
109
+ readonly derived?: boolean;
110
+ };
111
+ };
112
+ export interface CompiledLens {
113
+ /** Applies the ops in order. Clones the input; never mutates it. */
114
+ readonly up: (data: unknown, ctx: MigrationContext) => unknown;
115
+ /**
116
+ * Applies the inverted ops in reverse order, or `null` when any op is not
117
+ * invertible (see {@link CompiledLens.invertible}).
118
+ */
119
+ readonly down: ((data: unknown, ctx: MigrationContext) => unknown) | null;
120
+ readonly invertible: boolean;
121
+ /** Paths the up direction fills with guesses (statically known). */
122
+ readonly derivedUp: readonly string[];
123
+ /** Paths the down direction fills with guesses (`drop.restore`). */
124
+ readonly derivedDown: readonly string[];
125
+ /** Paths the down direction discards — the cost of the projection. */
126
+ readonly lossyDown: readonly string[];
127
+ }
128
+ /**
129
+ * Compiles an ops list into executable up and (when possible) down
130
+ * migrations. Throws `TypeError` on a malformed ops list — a bad contract is
131
+ * a programming error and should fail at declaration, not at read time.
132
+ */
133
+ export declare function compileLens(ops: readonly LensOp[]): CompiledLens;