functionalscript 0.42.0 → 0.43.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/fjs/bnf/data/module.f.d.ts +2 -2
- package/fjs/bnf/ll1/module.f.d.ts +1 -1
- package/fjs/bnf/module.f.d.ts +1 -1
- package/fjs/cas/evo/module.f.d.ts +3 -2
- package/fjs/cas/evo/module.f.js +8 -7
- package/fjs/cas/evo/proof.f.d.ts +4 -0
- package/fjs/cas/evo/proof.f.js +54 -0
- package/fjs/dev/module.f.d.ts +1 -1
- package/fjs/effects/node/module.f.d.ts +2 -2
- package/fjs/fsm/module.f.d.ts +1 -1
- package/fjs/media/html/module.f.d.ts +1 -1
- package/fjs/media/revision/module.f.d.ts +8 -0
- package/fjs/media/revision/module.f.js +13 -1
- package/fjs/media/revision/proof.f.d.ts +11 -0
- package/fjs/media/revision/proof.f.js +51 -1
- package/fjs/types/object/module.f.d.ts +31 -13
- package/fjs/types/object/module.f.js +4 -3
- package/fjs/types/object/proof.f.d.ts +0 -14
- package/fjs/types/object/proof.f.js +0 -4
- package/fjs/types/rtti/common/module.f.d.ts +2 -2
- package/fjs/types/rtti/module.f.d.ts +1 -1
- package/fjs/types/rtti/ts/module.f.d.ts +1 -1
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ export type TerminalRange = number;
|
|
|
11
11
|
*/
|
|
12
12
|
export type Sequence = readonly string[];
|
|
13
13
|
/** A variant of rule names. */
|
|
14
|
-
export type Variant = StringMap<string
|
|
14
|
+
export type Variant = StringMap<string>;
|
|
15
15
|
/**
|
|
16
16
|
* Grammar rule definition.
|
|
17
17
|
*
|
|
@@ -29,7 +29,7 @@ export type RuleSet = Readonly<Record<string, Rule>>;
|
|
|
29
29
|
* variant branch.
|
|
30
30
|
*/
|
|
31
31
|
export type EmptyTag = string | true | undefined;
|
|
32
|
-
type EmptyTagMap = StringMap<
|
|
32
|
+
type EmptyTagMap = StringMap<EmptyTag>;
|
|
33
33
|
/**
|
|
34
34
|
* Computes, for every rule in the set, whether it can match empty input, by
|
|
35
35
|
* the standard nullable-set fixpoint: a sequence is nullable iff all of its
|
|
@@ -26,7 +26,7 @@ type DispatchRuleCollection = {
|
|
|
26
26
|
readonly tag: string | undefined;
|
|
27
27
|
readonly rules: DispatchRuleOrName[];
|
|
28
28
|
};
|
|
29
|
-
type DispatchMap = StringMap<
|
|
29
|
+
type DispatchMap = StringMap<DispatchRule>;
|
|
30
30
|
/**
|
|
31
31
|
* Represents a parsed AST sequence.
|
|
32
32
|
*/
|
package/fjs/bnf/module.f.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export declare const range: (ab: string) => TerminalRange;
|
|
|
89
89
|
/**
|
|
90
90
|
* A set of terminal ranges compatible with the `Variant` rule.
|
|
91
91
|
*/
|
|
92
|
-
export type RangeVariant = StringMap<
|
|
92
|
+
export type RangeVariant = StringMap<TerminalRange>;
|
|
93
93
|
export declare const remove: (range: TerminalRange, v: RangeVariant) => RangeVariant;
|
|
94
94
|
/**
|
|
95
95
|
* Returns the complement set of the provided ranges over {@link fullRange}.
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
import { type Effect, type Operation } from '../../effects/module.f.ts';
|
|
46
46
|
import { type Key, type MemOp } from '../../effects/memory/module.f.ts';
|
|
47
47
|
import { type Cas } from '../module.f.ts';
|
|
48
|
-
import { type Revision } from '../../media/revision/module.f.ts';
|
|
48
|
+
import { type LockMap, type Revision } from '../../media/revision/module.f.ts';
|
|
49
49
|
import { type Result } from '../../types/result/module.f.ts';
|
|
50
50
|
import { type StringMap } from '../../types/object/module.f.ts';
|
|
51
51
|
import type { Vec } from '../../types/bit_vec/module.f.ts';
|
|
@@ -98,6 +98,7 @@ export type RevisionData = {
|
|
|
98
98
|
readonly subject?: Subject | undefined;
|
|
99
99
|
readonly archived?: true | undefined;
|
|
100
100
|
readonly generation?: number | undefined;
|
|
101
|
+
readonly lock?: LockMap | undefined;
|
|
101
102
|
};
|
|
102
103
|
/**
|
|
103
104
|
* Per-subject bookkeeping: every revision hash seen for the subject, every
|
|
@@ -119,7 +120,7 @@ export type SubjectState = {
|
|
|
119
120
|
};
|
|
120
121
|
/** In-memory index: subject → its {@link SubjectState}. */
|
|
121
122
|
export type Cache = {
|
|
122
|
-
readonly bySubject: StringMap<
|
|
123
|
+
readonly bySubject: StringMap<SubjectState>;
|
|
123
124
|
};
|
|
124
125
|
/** A cache with no known subjects yet — the starting point for {@link buildCache}. */
|
|
125
126
|
export declare const emptyCache: Cache;
|
package/fjs/cas/evo/module.f.js
CHANGED
|
@@ -49,9 +49,7 @@ import { collectRead } from '../module.f.js';
|
|
|
49
49
|
import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.js';
|
|
50
50
|
import { fromVec } from '../../text/utf8/module.f.js';
|
|
51
51
|
import { tryUtf8 } from '../../text/module.f.js';
|
|
52
|
-
import { decodeText, dialect, checkReferences, isHash } from '../../media/revision/module.f.js';
|
|
53
|
-
import { stringify } from '../../media/json/module.f.js';
|
|
54
|
-
import { identity } from '../../types/function/module.f.js';
|
|
52
|
+
import { decodeText, encodeText, dialect, checkReferences, isHash } from '../../media/revision/module.f.js';
|
|
55
53
|
import { ok, error } from '../../types/result/module.f.js';
|
|
56
54
|
import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.js';
|
|
57
55
|
import { at, definedEntries } from '../../types/object/module.f.js';
|
|
@@ -59,8 +57,6 @@ import { unwrap } from '../../types/nullable/module.f.js';
|
|
|
59
57
|
import { isNotFound } from '../../effects/node/module.f.js';
|
|
60
58
|
/** A cache with no known subjects yet — the starting point for {@link buildCache}. */
|
|
61
59
|
export const emptyCache = { bySubject: {} };
|
|
62
|
-
/** Canonical JSON encoder for a `Revision` — key order carries no meaning for detection. */
|
|
63
|
-
const toJson = stringify(identity);
|
|
64
60
|
const emptySubjectState = { hashes: [], parents: [], archived: [] };
|
|
65
61
|
/** Adds every item of `items` to `set` that isn't already there, preserving `set`'s existing order. */
|
|
66
62
|
const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc : [...acc, h], set);
|
|
@@ -77,6 +73,8 @@ const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc
|
|
|
77
73
|
* (`fjs/media/revision` `checkReferences`), so decoding here cannot fail.
|
|
78
74
|
*/
|
|
79
75
|
const canonicalHash = (h) => vecToCBase32(unwrap(cBase32ToVec(h)));
|
|
76
|
+
/** Canonicalizes every direct hash in a structurally validated flat lock map. */
|
|
77
|
+
const canonicalLock = (lock) => Object.fromEntries(definedEntries(lock).map(([subject, hash]) => [subject, canonicalHash(hash)]));
|
|
80
78
|
/** A subject's current heads: revision hashes seen that no other revision of the same subject names as a parent. */
|
|
81
79
|
const headsOf = (state) => state.hashes.filter(h => !state.parents.includes(h));
|
|
82
80
|
/**
|
|
@@ -325,6 +323,7 @@ export const addRevision = (cas) => (cacheKey) => (input) => eff(resolveParents(
|
|
|
325
323
|
snapshot: snapshotResult[1],
|
|
326
324
|
generation: computeGeneration(parentsResult[1]),
|
|
327
325
|
archived: input.archived,
|
|
326
|
+
lock: input.lock,
|
|
328
327
|
};
|
|
329
328
|
const referencesResult = checkReferences(revision);
|
|
330
329
|
if (referencesResult[0] === 'error') {
|
|
@@ -341,8 +340,9 @@ export const addRevision = (cas) => (cacheKey) => (input) => eff(resolveParents(
|
|
|
341
340
|
...revision,
|
|
342
341
|
parents: revision.parents.map(canonicalHash),
|
|
343
342
|
snapshot: canonicalHash(revision.snapshot),
|
|
343
|
+
lock: revision.lock === undefined ? undefined : canonicalLock(revision.lock),
|
|
344
344
|
};
|
|
345
|
-
const bytes = tryUtf8(
|
|
345
|
+
const bytes = tryUtf8(encodeText(canonicalRevision));
|
|
346
346
|
if (bytes === null) {
|
|
347
347
|
return pure(error('revision too large to encode'));
|
|
348
348
|
}
|
|
@@ -370,12 +370,13 @@ export const addRevision = (cas) => (cacheKey) => (input) => eff(resolveParents(
|
|
|
370
370
|
* inside `canonicalHash` is safe. Field order follows the stored blob's
|
|
371
371
|
* (minus `dialect`), which is what a JSON encoding of the result shows.
|
|
372
372
|
*/
|
|
373
|
-
const toRevisionData = ({ subject, parents, snapshot, generation, archived }) => ({
|
|
373
|
+
const toRevisionData = ({ subject, parents, snapshot, generation, archived, lock }) => ({
|
|
374
374
|
subject,
|
|
375
375
|
parents: parents.map(canonicalHash),
|
|
376
376
|
snapshot: canonicalHash(snapshot),
|
|
377
377
|
generation,
|
|
378
378
|
archived,
|
|
379
|
+
lock: lock === undefined ? undefined : canonicalLock(lock),
|
|
379
380
|
});
|
|
380
381
|
/**
|
|
381
382
|
* Second stage of {@link readRevision}: interprets an already-performed read
|
package/fjs/cas/evo/proof.f.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export declare const proof: {
|
|
|
35
35
|
revisionNonRevisionBlobIsError: () => void;
|
|
36
36
|
revisionCanonicalizesReferenceSpellings: () => void;
|
|
37
37
|
revisionRoundTripsThroughAdd: () => void;
|
|
38
|
+
revisionLockRoundTripsAndCanonicalizes: () => void;
|
|
39
|
+
revisionAbsentAndEmptyLocksRemainDistinct: () => void;
|
|
40
|
+
revisionInvalidLockValueIsRejected: () => void;
|
|
41
|
+
equivalentLockOrdersReuseOneCasAddress: () => void;
|
|
38
42
|
syncRevisionFoldsValidRevisionIntoCache: () => void;
|
|
39
43
|
syncRevisionIgnoresNonRevisionContent: () => void;
|
|
40
44
|
evoHeadUnknownSubjectIsEmpty: () => void;
|
package/fjs/cas/evo/proof.f.js
CHANGED
|
@@ -596,6 +596,60 @@ export const proof = {
|
|
|
596
596
|
assert(readded[0] === 'ok', ['expected re-add ok', readded]);
|
|
597
597
|
assertEq(readded[1], child[1]);
|
|
598
598
|
},
|
|
599
|
+
revisionLockRoundTripsAndCanonicalizes: () => {
|
|
600
|
+
const c = fileCas(sha256)(home);
|
|
601
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
602
|
+
const e = evo(c)(cacheKey);
|
|
603
|
+
const canonical = vecToCBase32(vec8(0xffn));
|
|
604
|
+
const alias = canonical.toUpperCase();
|
|
605
|
+
const [state1, added] = virtual(state0)(e.add({
|
|
606
|
+
parents: [], subject: 'doc', snapshot: canonical,
|
|
607
|
+
lock: { dependency: alias },
|
|
608
|
+
}));
|
|
609
|
+
assert(added[0] === 'ok', ['expected add ok', added]);
|
|
610
|
+
const [, result] = virtual(state1)(e.revision(added[1]));
|
|
611
|
+
assert(result[0] === 'ok', ['expected revision ok', result]);
|
|
612
|
+
assertEq(result[1].lock?.dependency, canonical);
|
|
613
|
+
},
|
|
614
|
+
revisionAbsentAndEmptyLocksRemainDistinct: () => {
|
|
615
|
+
const c = fileCas(sha256)(home);
|
|
616
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
617
|
+
const e = evo(c)(cacheKey);
|
|
618
|
+
const snapshot = vecToCBase32(vec8(0x42n));
|
|
619
|
+
const [state1, absent] = virtual(state0)(e.add({ parents: [], subject: 'absent', snapshot }));
|
|
620
|
+
assert(absent[0] === 'ok', ['expected add ok', absent]);
|
|
621
|
+
const [state2, empty] = virtual(state1)(e.add({ parents: [], subject: 'empty', snapshot, lock: {} }));
|
|
622
|
+
assert(empty[0] === 'ok', ['expected add ok', empty]);
|
|
623
|
+
const [state3, absentRead] = virtual(state2)(e.revision(absent[1]));
|
|
624
|
+
assert(absentRead[0] === 'ok', ['expected revision ok', absentRead]);
|
|
625
|
+
const [, emptyRead] = virtual(state3)(e.revision(empty[1]));
|
|
626
|
+
assert(emptyRead[0] === 'ok', ['expected revision ok', emptyRead]);
|
|
627
|
+
assertEq(absentRead[1].lock, undefined);
|
|
628
|
+
assertEq(Object.keys(emptyRead[1].lock ?? {}).length, 0);
|
|
629
|
+
},
|
|
630
|
+
revisionInvalidLockValueIsRejected: () => {
|
|
631
|
+
const c = fileCas(sha256)(home);
|
|
632
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
633
|
+
const e = evo(c)(cacheKey);
|
|
634
|
+
const [, result] = virtual(state0)(e.add({
|
|
635
|
+
parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)),
|
|
636
|
+
lock: { dependency: 'not a hash!' },
|
|
637
|
+
}));
|
|
638
|
+
assertEq(result[0], 'error');
|
|
639
|
+
},
|
|
640
|
+
equivalentLockOrdersReuseOneCasAddress: () => {
|
|
641
|
+
const c = fileCas(sha256)(home);
|
|
642
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
643
|
+
const e = evo(c)(cacheKey);
|
|
644
|
+
const a = vecToCBase32(vec8(0x1an));
|
|
645
|
+
const b = vecToCBase32(vec8(0x2bn));
|
|
646
|
+
const snapshot = vecToCBase32(vec8(0x3cn));
|
|
647
|
+
const [state1, first] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot, lock: { '2': b, '10': a } }));
|
|
648
|
+
assert(first[0] === 'ok', ['expected add ok', first]);
|
|
649
|
+
const [, second] = virtual(state1)(e.add({ lock: { '10': a, '2': b }, snapshot, subject: 'doc', parents: [] }));
|
|
650
|
+
assert(second[0] === 'ok', ['expected add ok', second]);
|
|
651
|
+
assertEq(second[1], first[1]);
|
|
652
|
+
},
|
|
599
653
|
// A raw CAS write (e.g. `cas_add`) of valid revision content is folded
|
|
600
654
|
// into the cache exactly as `addRevision` would, without going through
|
|
601
655
|
// `evo.add` — this is what keeps `cas_add` and `evo_add` writes to the
|
package/fjs/dev/module.f.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type Module = {
|
|
|
10
10
|
readonly proof?: unknown;
|
|
11
11
|
readonly [k: string]: unknown;
|
|
12
12
|
};
|
|
13
|
-
export type ModuleMap = StringMap<
|
|
13
|
+
export type ModuleMap = StringMap<Module>;
|
|
14
14
|
/**
|
|
15
15
|
* Returns `true` if the file should be loaded for proof discovery.
|
|
16
16
|
*
|
|
@@ -109,7 +109,7 @@ export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
|
|
|
109
109
|
export declare const stat: Func<Stat>;
|
|
110
110
|
export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
|
|
111
111
|
export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
|
|
112
|
-
export type Headers = StringMap<string
|
|
112
|
+
export type Headers = StringMap<string>;
|
|
113
113
|
export type IncomingMessage = {
|
|
114
114
|
readonly method: string;
|
|
115
115
|
readonly url: string;
|
|
@@ -129,7 +129,7 @@ export declare const listen: Func<Listen>;
|
|
|
129
129
|
export type Http = CreateServer | Listen;
|
|
130
130
|
export type Forever = ['forever', () => never];
|
|
131
131
|
export declare const forever: Func<Forever>;
|
|
132
|
-
export type Module = StringMap<
|
|
132
|
+
export type Module = StringMap<unknown>;
|
|
133
133
|
export type Import = ['import', (path: string) => IoResult<Module>];
|
|
134
134
|
export declare const import_: Func<Import>;
|
|
135
135
|
/** Named output streams accepted by the `Write` effect. */
|
package/fjs/fsm/module.f.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ByteSet } from '../types/byte_set/module.f.ts';
|
|
|
9
9
|
import { type RangeMapArray } from '../types/range_map/module.f.ts';
|
|
10
10
|
type Rule = readonly [string, ByteSet, string];
|
|
11
11
|
export type Grammar = List<Rule>;
|
|
12
|
-
type Dfa = StringMap<
|
|
12
|
+
type Dfa = StringMap<RangeMapArray<string>>;
|
|
13
13
|
export declare const toRange: (s: string) => ByteSet;
|
|
14
14
|
export declare const toUnion: (s: string) => ByteSet;
|
|
15
15
|
export declare const dfa: (grammar: Grammar) => Dfa;
|
|
@@ -17,7 +17,7 @@ type Element2 = readonly [Tag, Attributes, ...Node[]];
|
|
|
17
17
|
* - `[tag, attributes, ...children]` for elements with attributes.
|
|
18
18
|
*/
|
|
19
19
|
export type Element = Element1 | Element2;
|
|
20
|
-
type Attributes = StringMap<string
|
|
20
|
+
type Attributes = StringMap<string>;
|
|
21
21
|
export type Node = Element | string;
|
|
22
22
|
/**
|
|
23
23
|
* Converts a FunctionalScript element into a list of HTML string chunks.
|
|
@@ -20,6 +20,10 @@ export declare const mediaType: "application/vnd.fjs.revision+json";
|
|
|
20
20
|
* not by this schema on its own.
|
|
21
21
|
*/
|
|
22
22
|
export declare const hash: import("../../types/rtti/module.f.ts").String;
|
|
23
|
+
/** Structural schema for a Stage 1 flat lock map. */
|
|
24
|
+
declare const lock: import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>;
|
|
25
|
+
/** A flat set of subject-to-snapshot bindings supplied to dependency resolvers. */
|
|
26
|
+
export type LockMap = Ts<typeof lock>;
|
|
23
27
|
/**
|
|
24
28
|
* rtti schema for a `revision` BLOB. See the README for the full semantics of
|
|
25
29
|
* each field; `dialect` is the type discriminant, matched here as an exact
|
|
@@ -32,9 +36,12 @@ export declare const revisionSchema: {
|
|
|
32
36
|
readonly snapshot: import("../../types/rtti/module.f.ts").String;
|
|
33
37
|
readonly generation: import("../../types/rtti/module.f.ts").Number;
|
|
34
38
|
readonly archived: import("../../types/rtti/module.f.ts").Or<readonly [true, undefined]>;
|
|
39
|
+
readonly lock: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>, undefined]>;
|
|
35
40
|
};
|
|
36
41
|
/** The TypeScript type derived from {@link revisionSchema} — the single source of truth. */
|
|
37
42
|
export type Revision = Ts<typeof revisionSchema>;
|
|
43
|
+
/** Serializes a revision canonically, recursively sorting every object's property names. */
|
|
44
|
+
export declare const encodeText: (revision: Revision) => string;
|
|
38
45
|
/** True when `s` decodes as a cbase32 CAS hash (rejects `https://` and any other non-cbase32 string). */
|
|
39
46
|
export declare const isHash: (s: string) => boolean;
|
|
40
47
|
/** Either a structural validation error or a semantic (hash / generation) error message. */
|
|
@@ -84,3 +91,4 @@ export declare const decodeText: (text: string) => Result<Revision, RevisionErro
|
|
|
84
91
|
* `snapshot` is not a cbase32 hash is not one.
|
|
85
92
|
*/
|
|
86
93
|
export declare const revisionDialect: DialectEntry;
|
|
94
|
+
export {};
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module
|
|
16
16
|
*/
|
|
17
|
-
import { array, number, option, string } from '../../types/rtti/module.f.js';
|
|
17
|
+
import { array, number, option, record, string } from '../../types/rtti/module.f.js';
|
|
18
18
|
import { validate as rttiValidate } from '../../types/rtti/validate/module.f.js';
|
|
19
19
|
import { parse as parseJson } from '../json/module.f.js';
|
|
20
20
|
import { cBase32ToVec } from '../../basen/cbase32/module.f.js';
|
|
21
21
|
import { error, ok } from '../../types/result/module.f.js';
|
|
22
22
|
import { dialectEntry } from '../module.f.js';
|
|
23
|
+
import { definedEntries, sort } from '../../types/object/module.f.js';
|
|
24
|
+
import { stringify } from '../json/module.f.js';
|
|
23
25
|
/**
|
|
24
26
|
* Format tag: names the dialect of this BLOB. The media type it is served
|
|
25
27
|
* with is derived mechanically: `application/` + `dialect` + `+json`.
|
|
@@ -37,6 +39,8 @@ export const mediaType = `application/${dialect}+json`;
|
|
|
37
39
|
* not by this schema on its own.
|
|
38
40
|
*/
|
|
39
41
|
export const hash = string;
|
|
42
|
+
/** Structural schema for a Stage 1 flat lock map. */
|
|
43
|
+
const lock = record(string);
|
|
40
44
|
/**
|
|
41
45
|
* rtti schema for a `revision` BLOB. See the README for the full semantics of
|
|
42
46
|
* each field; `dialect` is the type discriminant, matched here as an exact
|
|
@@ -49,7 +53,10 @@ export const revisionSchema = {
|
|
|
49
53
|
snapshot: hash,
|
|
50
54
|
generation: number,
|
|
51
55
|
archived: option(true),
|
|
56
|
+
lock: option(lock),
|
|
52
57
|
};
|
|
58
|
+
/** Serializes a revision canonically, recursively sorting every object's property names. */
|
|
59
|
+
export const encodeText = stringify(sort);
|
|
53
60
|
/** Structural-only validator: checks the shape, not the hash / generation semantics. */
|
|
54
61
|
const validateShape = rttiValidate(revisionSchema);
|
|
55
62
|
/** True when `s` decodes as a cbase32 CAS hash (rejects `https://` and any other non-cbase32 string). */
|
|
@@ -89,6 +96,11 @@ export const checkReferences = (r) => {
|
|
|
89
96
|
if (!isHash(r.snapshot)) {
|
|
90
97
|
return error(`snapshot is not a valid hash: ${r.snapshot}`);
|
|
91
98
|
}
|
|
99
|
+
for (const [subject, snapshot] of definedEntries(r.lock ?? {})) {
|
|
100
|
+
if (!isHash(snapshot)) {
|
|
101
|
+
return error(`lock value for ${subject} is not a valid hash: ${snapshot}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
92
104
|
if (!Number.isSafeInteger(r.generation) || r.generation < 0) {
|
|
93
105
|
return error(`generation must be a non-negative safe integer: ${r.generation}`);
|
|
94
106
|
}
|
|
@@ -18,6 +18,12 @@ export declare const proof: {
|
|
|
18
18
|
positiveGenerationAccepted: () => void;
|
|
19
19
|
httpsRejectedInParents: () => void;
|
|
20
20
|
archivedAccepted: () => void;
|
|
21
|
+
lockAbsentAccepted: () => void;
|
|
22
|
+
emptyLockAccepted: () => void;
|
|
23
|
+
validLockAccepted: () => void;
|
|
24
|
+
malformedLockRejected: () => void;
|
|
25
|
+
invalidHashLockRejected: () => void;
|
|
26
|
+
aliasHashLockAccepted: () => void;
|
|
21
27
|
wrongDialectRejected: () => void;
|
|
22
28
|
missingSubjectRejected: () => void;
|
|
23
29
|
extraFieldsAccepted: () => void;
|
|
@@ -28,4 +34,9 @@ export declare const proof: {
|
|
|
28
34
|
malformedJsonRejected: () => void;
|
|
29
35
|
ordinaryJsonRejected: () => void;
|
|
30
36
|
};
|
|
37
|
+
encodeText: {
|
|
38
|
+
recursivelySortsObjectsLexicographically: () => void;
|
|
39
|
+
preservesArrayOrder: () => void;
|
|
40
|
+
parsedEquivalentSourcesConverge: () => void;
|
|
41
|
+
};
|
|
31
42
|
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { assert, assertEq } from '../../asserts/module.f.js';
|
|
2
|
-
import { dialect, mediaType, isHash, validate, decodeText } from './module.f.js';
|
|
2
|
+
import { dialect, mediaType, isHash, validate, decodeText, encodeText } from './module.f.js';
|
|
3
3
|
// Valid cbase32 hashes (round-tripped in fjs/basen/cbase32/proof.f.ts): single
|
|
4
4
|
// cbase32 symbols, cheap to write inline here.
|
|
5
5
|
const h1 = '8';
|
|
6
6
|
const h2 = 'r';
|
|
7
|
+
// `I` is accepted as an alias spelling of canonical cBase32 `1`.
|
|
8
|
+
const alias = 'I';
|
|
9
|
+
const _lockMapAllowsMissingSubjects = {};
|
|
7
10
|
// A shape-valid revision: every required field present (`snapshot` and
|
|
8
11
|
// `generation` included), with `extra` overriding or adding fields per test.
|
|
9
12
|
const revisionOf = (extra) => ({
|
|
@@ -95,6 +98,32 @@ export const proof = {
|
|
|
95
98
|
const [t] = validate(revisionOf({ archived: true }));
|
|
96
99
|
assertEq(t, 'ok');
|
|
97
100
|
},
|
|
101
|
+
lockAbsentAccepted: () => {
|
|
102
|
+
const r = validate(revisionOf({}));
|
|
103
|
+
assert(r[0] === 'ok', ['expected ok', r]);
|
|
104
|
+
assertEq(r[1].lock, undefined);
|
|
105
|
+
},
|
|
106
|
+
emptyLockAccepted: () => {
|
|
107
|
+
const r = validate(revisionOf({ lock: {} }));
|
|
108
|
+
assert(r[0] === 'ok', ['expected ok', r]);
|
|
109
|
+
assertEq(Object.keys(r[1].lock ?? {}).length, 0);
|
|
110
|
+
},
|
|
111
|
+
validLockAccepted: () => {
|
|
112
|
+
const [t] = validate(revisionOf({ lock: { dependency: h1 } }));
|
|
113
|
+
assertEq(t, 'ok');
|
|
114
|
+
},
|
|
115
|
+
malformedLockRejected: () => {
|
|
116
|
+
const [t] = validate(revisionOf({ lock: { dependency: 1 } }));
|
|
117
|
+
assertEq(t, 'error');
|
|
118
|
+
},
|
|
119
|
+
invalidHashLockRejected: () => {
|
|
120
|
+
const [t] = validate(revisionOf({ lock: { dependency: 'https://example.com/x' } }));
|
|
121
|
+
assertEq(t, 'error');
|
|
122
|
+
},
|
|
123
|
+
aliasHashLockAccepted: () => {
|
|
124
|
+
const [t] = validate(revisionOf({ lock: { dependency: alias } }));
|
|
125
|
+
assertEq(t, 'ok');
|
|
126
|
+
},
|
|
98
127
|
// Wrong dialect tag: structural validation rejects it outright.
|
|
99
128
|
wrongDialectRejected: () => {
|
|
100
129
|
const [t] = validate({ dialect: 'vnd.fjs.other', subject: h1, parents: [], snapshot: h2, generation: 0 });
|
|
@@ -137,4 +166,25 @@ export const proof = {
|
|
|
137
166
|
assertEq(t, 'error');
|
|
138
167
|
},
|
|
139
168
|
},
|
|
169
|
+
encodeText: {
|
|
170
|
+
recursivelySortsObjectsLexicographically: () => {
|
|
171
|
+
const revision = revisionOf({ lock: { '2': h2, '10': h1 } });
|
|
172
|
+
const decoded = validate(revision);
|
|
173
|
+
assert(decoded[0] === 'ok', ['expected ok', decoded]);
|
|
174
|
+
assertEq(encodeText(decoded[1]), `{"dialect":"${dialect}","generation":0,"lock":{"10":"${h1}","2":"${h2}"},"parents":[],"snapshot":"${h2}","subject":"${h1}"}`);
|
|
175
|
+
},
|
|
176
|
+
preservesArrayOrder: () => {
|
|
177
|
+
const decoded = validate(revisionOf({ parents: [h2, h1] }));
|
|
178
|
+
assert(decoded[0] === 'ok', ['expected ok', decoded]);
|
|
179
|
+
assert(encodeText(decoded[1]).includes(`"parents":["${h2}","${h1}"]`));
|
|
180
|
+
},
|
|
181
|
+
parsedEquivalentSourcesConverge: () => {
|
|
182
|
+
const a = decodeText(` { "subject":"${h1}", "snapshot":"${h2}", "parents":[], "generation":0, "dialect":"${dialect}" } `);
|
|
183
|
+
const b = decodeText(`{"dialect":"${dialect}","generation":0,"parents":[],"snapshot":"${h2}","subject":"${h1}"}`);
|
|
184
|
+
assert(a[0] === 'ok', ['expected ok', a]);
|
|
185
|
+
assert(b[0] === 'ok', ['expected ok', b]);
|
|
186
|
+
assertEq(encodeText(a[1]), encodeText(b[1]));
|
|
187
|
+
},
|
|
188
|
+
},
|
|
140
189
|
};
|
|
190
|
+
void _lockMapAllowsMissingSubjects;
|
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { type List } from '../list/module.f.ts';
|
|
2
2
|
import { type Nullable } from '../nullable/module.f.ts';
|
|
3
3
|
import { type OrderedMap } from '../ordered_map/module.f.ts';
|
|
4
|
-
|
|
4
|
+
/** A record over the keys of `K`, each value possibly missing at runtime. */
|
|
5
|
+
export type OptionalMap<K extends string, T> = {
|
|
6
|
+
readonly [k in K]?: T;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A record over the keys of `K`, each value required.
|
|
10
|
+
*
|
|
11
|
+
* `K` has to be a finite union of string literals: `RequiredMap<string, T>` is
|
|
12
|
+
* `never`, because no object can carry every string as a key. Use `StringMap<T>`
|
|
13
|
+
* for an open key set — its values are optional, which is what such a key set
|
|
14
|
+
* means at runtime.
|
|
15
|
+
*
|
|
16
|
+
* There is no known way to ask TypeScript whether `K` is finite, so the guard
|
|
17
|
+
* approximates it with `string extends K`, which holds exactly when `K` is
|
|
18
|
+
* `string`. Other infinite key sets are not caught: a template literal such as
|
|
19
|
+
* `x-${string}` yields a template index signature instead of `never`, and its
|
|
20
|
+
* reads are typed `T` while the runtime value is `undefined`. Keep `K` a union
|
|
21
|
+
* of string literals.
|
|
22
|
+
*/
|
|
23
|
+
export type RequiredMap<K extends string, T> = string extends K ? never : {
|
|
24
|
+
readonly [k in K]: T;
|
|
25
|
+
};
|
|
26
|
+
/** A record with an open key set. Every value can be missing at runtime. */
|
|
27
|
+
export type StringMap<T> = OptionalMap<string, T>;
|
|
5
28
|
export type Entry<T> = readonly [string, T];
|
|
6
|
-
export declare const at: (name: string) => <T>(object:
|
|
29
|
+
export declare const at: (name: string) => <T>(object: StringMap<T>) => Nullable<Exclude<T, undefined>>;
|
|
7
30
|
export declare const sort: <T>(e: List<Entry<T>>) => List<Entry<T>>;
|
|
8
|
-
export declare const fromEntries: <T>(e: List<Entry<T>>) =>
|
|
9
|
-
export declare const fromMap: <T>(m: OrderedMap<T>) =>
|
|
31
|
+
export declare const fromEntries: <T>(e: List<Entry<T>>) => StringMap<T>;
|
|
32
|
+
export declare const fromMap: <T>(m: OrderedMap<T>) => StringMap<T>;
|
|
10
33
|
/**
|
|
11
34
|
* A set of objects with a single key.
|
|
12
35
|
*
|
|
@@ -14,7 +37,7 @@ export declare const fromMap: <T>(m: OrderedMap<T>) => Map<T>;
|
|
|
14
37
|
* https://stackoverflow.com/questions/57571664/typescript-type-for-an-object-with-only-one-key-no-union-type-allowed-as-a-key
|
|
15
38
|
*/
|
|
16
39
|
export type OneKey<K extends string, V> = {
|
|
17
|
-
[P in K]: (
|
|
40
|
+
[P in K]: (RequiredMap<P, V> & OptionalMap<Exclude<K, P>, never>) extends infer O ? {
|
|
18
41
|
[Q in keyof O]: O[Q];
|
|
19
42
|
} : never;
|
|
20
43
|
}[K];
|
|
@@ -24,13 +47,8 @@ export type OneKey<K extends string, V> = {
|
|
|
24
47
|
export type NotUnion<T, U = T> = T extends unknown ? [
|
|
25
48
|
U
|
|
26
49
|
] extends [T] ? T : never : never;
|
|
27
|
-
export type SingleProperty<T extends StringMap<
|
|
50
|
+
export type SingleProperty<T extends StringMap<never>> = keyof T extends NotUnion<keyof T> ? T : never;
|
|
28
51
|
export declare const isObject: (value: unknown) => value is { readonly [k in string]: unknown; };
|
|
29
52
|
/** Returns only the defined (non-undefined) values of a partial record. */
|
|
30
|
-
export declare const definedValues: <T>(map: StringMap<
|
|
31
|
-
export
|
|
32
|
-
readonly [k in string]?: T;
|
|
33
|
-
} : {
|
|
34
|
-
readonly [k in K]: T;
|
|
35
|
-
};
|
|
36
|
-
export declare const definedEntries: <T>(cmd: StringMap<string, Exclude<T, undefined>>) => readonly (readonly [string, Exclude<T, undefined>])[];
|
|
53
|
+
export declare const definedValues: <T>(map: StringMap<Exclude<T, undefined>>) => readonly Exclude<T, undefined>[];
|
|
54
|
+
export declare const definedEntries: <T>(cmd: StringMap<Exclude<T, undefined>>) => readonly (readonly [string, Exclude<T, undefined>])[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plain-object helpers and types: `
|
|
3
|
-
*
|
|
4
|
-
* `OneKey`/`SingleProperty`/`NotUnion`
|
|
2
|
+
* Plain-object helpers and types: the `OptionalMap`/`RequiredMap`/`StringMap`
|
|
3
|
+
* record shapes and `Entry<T>`, safe property lookup via `at`, conversions
|
|
4
|
+
* between entries and `OrderedMap`, and the `OneKey`/`SingleProperty`/`NotUnion`
|
|
5
|
+
* utility types.
|
|
5
6
|
*
|
|
6
7
|
* @module
|
|
7
8
|
*/
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import type { StringMap } from './module.f.ts';
|
|
2
|
-
type E<A, B> = A extends B ? B extends A ? true : false : false;
|
|
3
|
-
type _InfiniteIsOptional = E<StringMap<string, bigint>, {
|
|
4
|
-
readonly [k in string]?: bigint;
|
|
5
|
-
}>;
|
|
6
|
-
type _FiniteIsRequired = E<StringMap<'a' | 'b', bigint>, {
|
|
7
|
-
readonly a: bigint;
|
|
8
|
-
readonly b: bigint;
|
|
9
|
-
}>;
|
|
10
1
|
export declare const proof: {
|
|
11
|
-
stringMap: {
|
|
12
|
-
infiniteIsOptional: _InfiniteIsOptional;
|
|
13
|
-
finiteIsRequired: _FiniteIsRequired;
|
|
14
|
-
};
|
|
15
2
|
ctor: () => void;
|
|
16
3
|
property: () => void;
|
|
17
4
|
};
|
|
18
|
-
export {};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { at } from './module.f.js';
|
|
2
2
|
import { assertEq } from '../../asserts/module.f.js';
|
|
3
3
|
export const proof = {
|
|
4
|
-
stringMap: {
|
|
5
|
-
infiniteIsOptional: true,
|
|
6
|
-
finiteIsRequired: true,
|
|
7
|
-
},
|
|
8
4
|
ctor: () => {
|
|
9
5
|
const a = {};
|
|
10
6
|
const value = at('constructor')(a);
|
|
@@ -74,11 +74,11 @@ export type Visitor<R> = {
|
|
|
74
74
|
/** Type guard narrowing `Unknown` to a specific container type `C`. */
|
|
75
75
|
export type IsContainer<C extends Unknown> = (value: Unknown) => value is C;
|
|
76
76
|
/** Maps a `Tag1` to its runtime container type. */
|
|
77
|
-
export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : StringMap<
|
|
77
|
+
export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : StringMap<Unknown>;
|
|
78
78
|
/** `IsContainer` guard for arrays, shared by `validate` and `parse`. */
|
|
79
79
|
export declare const isArray: IsContainer<ReadonlyArray<Unknown>>;
|
|
80
80
|
/** `IsContainer` guard for records/structs, shared by `validate` and `parse`. */
|
|
81
|
-
export declare const isObject: IsContainer<StringMap<
|
|
81
|
+
export declare const isObject: IsContainer<StringMap<Unknown>>;
|
|
82
82
|
/**
|
|
83
83
|
* Runs `item` over each `[key, value]` entry, bailing out with the first
|
|
84
84
|
* error, path-prefixed with that entry's key. On success, folds each item's
|
|
@@ -6,7 +6,7 @@ export type Const = null | boolean | number | string | undefined | bigint | {
|
|
|
6
6
|
} | readonly Type[];
|
|
7
7
|
export type ConstObject = Struct | Tuple;
|
|
8
8
|
/** A struct schema: plain object whose values are nested `Type`s. */
|
|
9
|
-
export type Struct = StringMap<
|
|
9
|
+
export type Struct = StringMap<Type>;
|
|
10
10
|
/** A tuple schema: readonly array whose elements are nested `Type`s. */
|
|
11
11
|
export type Tuple = readonly Type[];
|
|
12
12
|
declare const primitive0List: readonly ['bigint', 'boolean', 'number', 'string'];
|
|
@@ -31,7 +31,7 @@ export type Object = {
|
|
|
31
31
|
/** Maps a `Tag0` to its TypeScript type. */
|
|
32
32
|
export type Info0Ts<T extends Tag0> = T extends 'boolean' ? boolean : T extends 'number' ? number : T extends 'string' ? string : T extends 'bigint' ? bigint : T extends 'unknown' ? Unknown : never;
|
|
33
33
|
/** Maps a `Const` schema to its TypeScript type. */
|
|
34
|
-
export type ConstTs<T> = T extends readonly Type[] ? TupleTs<T> : T extends StringMap<
|
|
34
|
+
export type ConstTs<T> = T extends readonly Type[] ? TupleTs<T> : T extends StringMap<Type> ? StructTs<T> : T;
|
|
35
35
|
/** Maps a `Tag1` and inner type to its TypeScript type. */
|
|
36
36
|
export type Info1Ts<K extends Tag1, T extends Type> = K extends 'array' ? ArrayTs<T> : K extends 'record' ? RecordTs<T> : never;
|
|
37
37
|
/** Maps an array schema `T` to `readonly Ts<T>[]`. */
|