functionalscript 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/fs/asserts/module.f.d.ts +1 -1
- package/fs/asserts/module.f.js +4 -1
- package/fs/bnf/data/module.f.d.ts +0 -103
- package/fs/bnf/data/module.f.js +10 -246
- package/fs/bnf/data/proof.f.d.ts +0 -10
- package/fs/bnf/data/proof.f.js +2 -642
- package/fs/bnf/descent/module.f.d.ts +56 -0
- package/fs/bnf/descent/module.f.js +119 -0
- package/fs/bnf/descent/proof.f.d.ts +5 -0
- package/fs/bnf/descent/proof.f.js +296 -0
- package/fs/bnf/ll1/module.f.d.ts +72 -0
- package/fs/bnf/ll1/module.f.js +153 -0
- package/fs/bnf/ll1/proof.f.d.ts +9 -0
- package/fs/bnf/ll1/proof.f.js +358 -0
- package/fs/cas/cli/module.f.d.ts +6 -0
- package/fs/cas/cli/module.f.js +56 -0
- package/fs/cas/cli/proof.f.d.ts +14 -0
- package/fs/cas/cli/proof.f.js +149 -0
- package/fs/cas/mcp/module.f.d.ts +9 -17
- package/fs/cas/mcp/module.f.js +116 -84
- package/fs/cas/mcp/proof.f.d.ts +5 -1
- package/fs/cas/mcp/proof.f.js +74 -32
- package/fs/cas/module.f.d.ts +35 -15
- package/fs/cas/module.f.js +168 -131
- package/fs/cas/proof.f.d.ts +16 -13
- package/fs/cas/proof.f.js +255 -121
- package/fs/ci/config/module.f.d.ts +14 -14
- package/fs/ci/config/module.f.js +14 -14
- package/fs/djs/tokenizer-new/module.f.js +1 -1
- package/fs/djs/tokenizer-new/proof.f.d.ts +2 -0
- package/fs/djs/tokenizer-new/proof.f.js +357 -229
- package/fs/effects/list/module.f.d.ts +31 -0
- package/fs/effects/list/module.f.js +16 -0
- package/fs/effects/module.f.d.ts +11 -5
- package/fs/effects/module.f.js +16 -6
- package/fs/effects/node/module.f.d.ts +29 -3
- package/fs/effects/node/module.f.js +35 -1
- package/fs/effects/node/module.js +32 -18
- package/fs/effects/node/virtual/module.f.js +57 -0
- package/fs/effects/proof.f.d.ts +4 -0
- package/fs/effects/proof.f.js +18 -1
- package/fs/fjs/module.f.js +2 -8
- package/fs/html/module.f.js +6 -8
- package/fs/mcp/module.f.d.ts +1 -1
- package/fs/mime/module.f.d.ts +101 -5
- package/fs/mime/module.f.js +159 -6
- package/fs/mime/proof.f.d.ts +25 -0
- package/fs/mime/proof.f.js +193 -3
- package/fs/text/code_point/module.f.d.ts +39 -2
- package/fs/text/code_point/module.f.js +74 -2
- package/fs/text/code_point/proof.f.d.ts +8 -0
- package/fs/text/code_point/proof.f.js +72 -0
- package/fs/text/utf16/module.f.js +4 -26
- package/fs/text/utf8/module.f.d.ts +27 -0
- package/fs/text/utf8/module.f.js +8 -14
- package/fs/types/function/operator/module.f.d.ts +26 -0
- package/fs/types/list/module.f.d.ts +3 -0
- package/fs/types/result/module.d.ts +2 -4
- package/fs/types/result/module.js +9 -3
- package/package.json +3 -3
- package/issues/demo/data/data.f.js +0 -12
- package/issues/demo/data/shared.f.js +0 -3
- package/issues/demo/fs/app.js +0 -4
- package/issues/demo/fs/math.f.js +0 -4
- package/issues/demo/sample/proof.f.js +0 -13
- /package/{issues → todo}/031-json.f.d.ts +0 -0
- /package/{issues → todo}/031-json.f.js +0 -0
- /package/{issues → todo}/proof.f.d.ts +0 -0
- /package/{issues → todo}/proof.f.js +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Effect, type Operation } from "../module.f.ts";
|
|
2
|
+
export type NonEmpty<O extends Operation, T> = {
|
|
3
|
+
readonly first: T;
|
|
4
|
+
readonly tail: List<O, T>;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The payload of a `List` effect: the next cons cell, or `undefined` at
|
|
8
|
+
* end-of-stream.
|
|
9
|
+
*
|
|
10
|
+
* Since `Pure<T>` is now itself a lazy thunk (`() => T`), the effect alone is the
|
|
11
|
+
* suspension point and the cell needs no extra wrapping thunk. `Effect<O, Next<O, T>>`
|
|
12
|
+
* is used directly in places where `List<O, T>` cannot be written as a return type
|
|
13
|
+
* (see {@link empty}).
|
|
14
|
+
*/
|
|
15
|
+
export type Next<O extends Operation, T> = NonEmpty<O, T> | undefined;
|
|
16
|
+
export type List<O extends Operation, T> = Effect<O, Next<O, T>>;
|
|
17
|
+
/**
|
|
18
|
+
* The empty `List`: a pure end-of-stream marker (`undefined`).
|
|
19
|
+
*
|
|
20
|
+
* The explicit `Effect<O, Next<O, T>>` return type lets the contextual type drive the
|
|
21
|
+
* check, so the recursive payload type-checks without a cast. Construct streams through
|
|
22
|
+
* these two combinators.
|
|
23
|
+
*
|
|
24
|
+
* Note: we use `Effect<O, Next<O, T>>` because TypeScript can't convert `pure(...)` to
|
|
25
|
+
* `List<O, T>`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const empty: <O extends Operation, T>() => Effect<O, Next<O, T>>;
|
|
28
|
+
/**
|
|
29
|
+
* Prepends `head` to a `ListEffect` `tail`, as a pure cons cell. See {@link empty}.
|
|
30
|
+
*/
|
|
31
|
+
export declare const nonEmpty: <O extends Operation, T>(first: T, tail: List<O, T>) => Effect<O, Next<O, T>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { pure } from "../module.f.js";
|
|
2
|
+
/**
|
|
3
|
+
* The empty `List`: a pure end-of-stream marker (`undefined`).
|
|
4
|
+
*
|
|
5
|
+
* The explicit `Effect<O, Next<O, T>>` return type lets the contextual type drive the
|
|
6
|
+
* check, so the recursive payload type-checks without a cast. Construct streams through
|
|
7
|
+
* these two combinators.
|
|
8
|
+
*
|
|
9
|
+
* Note: we use `Effect<O, Next<O, T>>` because TypeScript can't convert `pure(...)` to
|
|
10
|
+
* `List<O, T>`.
|
|
11
|
+
*/
|
|
12
|
+
export const empty = () => pure(undefined);
|
|
13
|
+
/**
|
|
14
|
+
* Prepends `head` to a `ListEffect` `tail`, as a pure cons cell. See {@link empty}.
|
|
15
|
+
*/
|
|
16
|
+
export const nonEmpty = (first, tail) => pure({ first, tail });
|
package/fs/effects/module.f.d.ts
CHANGED
|
@@ -10,12 +10,19 @@ export type Effect<O extends Operation, T> = {
|
|
|
10
10
|
step: <Q extends Operation, R>(f: (p: T) => Effect<Q, R>) => Effect<O | Q, R>;
|
|
11
11
|
};
|
|
12
12
|
export type Value<O extends Operation, T> = Pure<T> | Do<O, T>;
|
|
13
|
-
export type Pure<T> =
|
|
13
|
+
export type Pure<T> = () => T;
|
|
14
14
|
export type DoKPR<O extends Operation, T, K extends string, PR extends readonly [unknown, unknown]> = readonly [K, PR[0], (_: PR[1]) => Effect<O, T>];
|
|
15
15
|
export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (...args: infer P) => infer R] ? readonly [P, R] : never;
|
|
16
16
|
export type DoK<O extends Operation, T, K extends O[0]> = DoKPR<O, T, K, Pr<O, K>>;
|
|
17
17
|
export type Do<O extends Operation, T> = DoK<O, T, O[0]>;
|
|
18
18
|
export declare const pure: <T>(v: T) => Effect<never, T>;
|
|
19
|
+
/**
|
|
20
|
+
* A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
|
|
21
|
+
* demand instead of holding an already-computed value. Use this to produce the
|
|
22
|
+
* next item of a list without instantiating the rest — the thunk runs only when
|
|
23
|
+
* the effect is decoded (or stepped into).
|
|
24
|
+
*/
|
|
25
|
+
export declare const lazy: <T>(t: () => T) => Effect<never, T>;
|
|
19
26
|
export declare const doFull: <O extends Operation, T, K extends O[0]>(cmd: K, param: Pr<O, K>[0], cont: (input: Pr<O, K>[1]) => Effect<O, T>) => Effect<O, T>;
|
|
20
27
|
export type Param<O extends Operation> = F<O>[0];
|
|
21
28
|
export type Return<O extends Operation> = F<O>[1];
|
|
@@ -53,10 +60,10 @@ export type Decoded<O extends Operation, T> = {
|
|
|
53
60
|
/**
|
|
54
61
|
* Decodes an effect's next step: a pure result, or a command to perform.
|
|
55
62
|
*
|
|
56
|
-
* This is the only function that knows how `Value` is laid out (a
|
|
57
|
-
*
|
|
63
|
+
* This is the only function that knows how `Value` is laid out (a thunk
|
|
64
|
+
* `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
|
|
58
65
|
* Interpreters and proofs must go through `decode` (or `match`) instead of
|
|
59
|
-
* inspecting the
|
|
66
|
+
* inspecting the value, so the representation can change without touching
|
|
60
67
|
* them.
|
|
61
68
|
*/
|
|
62
69
|
export declare const decode: <O extends Operation, T>({ value }: Effect<O, T>) => Decoded<O, T>;
|
|
@@ -82,4 +89,3 @@ export type ToAsyncOperationMap<O extends Operation> = {
|
|
|
82
89
|
};
|
|
83
90
|
export type F<O extends Operation> = Pr<O, O[0]>;
|
|
84
91
|
export type Func<O extends Operation> = (..._: Param<O>) => Effect<O, Return<O>>;
|
|
85
|
-
export type ListEffect<O extends Operation, T> = Effect<O, readonly [T, ListEffect<O, T>] | undefined>;
|
package/fs/effects/module.f.js
CHANGED
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { fold } from "../types/list/module.f.js";
|
|
7
7
|
export const pure = (v) => ({
|
|
8
|
-
value:
|
|
8
|
+
value: () => v,
|
|
9
9
|
step: f => f(v)
|
|
10
10
|
});
|
|
11
|
+
/**
|
|
12
|
+
* A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
|
|
13
|
+
* demand instead of holding an already-computed value. Use this to produce the
|
|
14
|
+
* next item of a list without instantiating the rest — the thunk runs only when
|
|
15
|
+
* the effect is decoded (or stepped into).
|
|
16
|
+
*/
|
|
17
|
+
export const lazy = (t) => ({
|
|
18
|
+
value: t,
|
|
19
|
+
step: f => f(t())
|
|
20
|
+
});
|
|
11
21
|
export const doFull = (cmd, param, cont) => ({
|
|
12
22
|
value: [cmd, param, cont],
|
|
13
23
|
step: (f) => doFull(cmd, param, x => cont(x).step(f)),
|
|
@@ -32,14 +42,14 @@ export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(u
|
|
|
32
42
|
/**
|
|
33
43
|
* Decodes an effect's next step: a pure result, or a command to perform.
|
|
34
44
|
*
|
|
35
|
-
* This is the only function that knows how `Value` is laid out (a
|
|
36
|
-
*
|
|
45
|
+
* This is the only function that knows how `Value` is laid out (a thunk
|
|
46
|
+
* `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
|
|
37
47
|
* Interpreters and proofs must go through `decode` (or `match`) instead of
|
|
38
|
-
* inspecting the
|
|
48
|
+
* inspecting the value, so the representation can change without touching
|
|
39
49
|
* them.
|
|
40
50
|
*/
|
|
41
|
-
export const decode = ({ value }) => value
|
|
42
|
-
? { done: true, result: value
|
|
51
|
+
export const decode = ({ value }) => typeof value === 'function'
|
|
52
|
+
? { done: true, result: value() }
|
|
43
53
|
: { done: false, command: value[0], payload: value[1], continuation: value[2] };
|
|
44
54
|
/**
|
|
45
55
|
* Decodes an effect's next step and dispatches its command to `map`,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Vec } from '../../types/bit_vec/module.f.ts';
|
|
2
2
|
import type { MemOp } from '../memory/module.f.ts';
|
|
3
3
|
import type { Nominal } from '../../types/nominal/module.f.ts';
|
|
4
4
|
import { type Result } from '../../types/result/module.f.ts';
|
|
5
5
|
import type { StringMap } from '../../types/object/module.f.ts';
|
|
6
6
|
import { type Effect, type Func, type Operation, type ToAsyncOperationMap } from '../module.f.ts';
|
|
7
|
+
import type { List } from '../list/module.f.ts';
|
|
7
8
|
export type IoResult<T> = Result<T, unknown>;
|
|
8
9
|
/**
|
|
9
10
|
* True if `e` is a "file or directory does not exist" (`ENOENT`) error.
|
|
@@ -81,7 +82,32 @@ export type Exec = readonly ['exec', (command: string, stdin?: string) => IoResu
|
|
|
81
82
|
export declare const exec: Func<Exec>;
|
|
82
83
|
export type Access = readonly ['access', (path: string) => IoResult<void>];
|
|
83
84
|
export declare const access: Func<Access>;
|
|
84
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Creates `path` as an empty file with `O_CREAT|O_EXCL` — fails if it already
|
|
87
|
+
* exists. This is the exclusive create that claims a staging name in the
|
|
88
|
+
* lock-free upload ([staging-lease.md](../../../issues/cas/staging-lease.md));
|
|
89
|
+
* with 256 random bits in the name `EEXIST` never happens in practice, so it
|
|
90
|
+
* is just a sanity guard.
|
|
91
|
+
*/
|
|
92
|
+
export type CreateExclusive = readonly ['createExclusive', (path: string) => IoResult<void>];
|
|
93
|
+
export declare const createExclusive: Func<CreateExclusive>;
|
|
94
|
+
/**
|
|
95
|
+
* Writes the **entire** `data` vector to an **existing** `path` at byte `offset`
|
|
96
|
+
* (positional write). The mirror of {@link readBytes}: it never creates the file
|
|
97
|
+
* (a missing path is `ENOENT`), and it writes every byte or returns an error —
|
|
98
|
+
* the runner loops over short writes — so a later size check can never pass over
|
|
99
|
+
* a hole. Bounded to ≤128 KiB per call, like `readBytes`.
|
|
100
|
+
*/
|
|
101
|
+
export type WriteBytes = readonly ['writeBytes', (path: string, offset: number, data: Vec) => IoResult<void>];
|
|
102
|
+
export declare const writeBytes: Func<WriteBytes>;
|
|
103
|
+
export declare const writeFromStream: <O extends Operation>(path: string, e: List<O, IoResult<Vec>>) => Effect<O | WriteBytes | CreateExclusive, IoResult<void>>;
|
|
104
|
+
/** File metadata returned by {@link stat}. Only `size` (in bytes) for now. */
|
|
105
|
+
export type FileStat = {
|
|
106
|
+
readonly size: number;
|
|
107
|
+
};
|
|
108
|
+
export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
|
|
109
|
+
export declare const stat: Func<Stat>;
|
|
110
|
+
export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
|
|
85
111
|
export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
|
|
86
112
|
export type Headers = StringMap<string, string>;
|
|
87
113
|
export type IncomingMessage = {
|
|
@@ -216,7 +242,7 @@ export type TestContext = {
|
|
|
216
242
|
/** Effect operation that registers a named test with the active `TestContext`. */
|
|
217
243
|
export type Test = readonly ['test', (ctx: TestContext, name: string, expectFailure: boolean, test: (t: TestContext) => Effect<Test | All | Await, void>) => void];
|
|
218
244
|
export declare const test: Func<Test>;
|
|
219
|
-
export type NodeOp = All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
|
|
245
|
+
export type NodeOp = Access | All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
|
|
220
246
|
export type NodeEffect<T> = Effect<NodeOp, T>;
|
|
221
247
|
/**
|
|
222
248
|
* Writes an error line to `stderr` and yields exit code `1`. The canonical
|
|
@@ -12,7 +12,8 @@ import { utf8, utf8ToString } from "../../text/module.f.js";
|
|
|
12
12
|
import { toCodePointList } from "../../text/utf8/module.f.js";
|
|
13
13
|
import { codePointListToString } from "../../text/utf16/module.f.js";
|
|
14
14
|
import { reverse } from "../../types/list/module.f.js";
|
|
15
|
-
import {
|
|
15
|
+
import { length } from "../../types/bit_vec/module.f.js";
|
|
16
|
+
import { ok, error as resultError } from "../../types/result/module.f.js";
|
|
16
17
|
import { do_, pure } from "../module.f.js";
|
|
17
18
|
/**
|
|
18
19
|
* True if `e` is a "file or directory does not exist" (`ENOENT`) error.
|
|
@@ -53,6 +54,39 @@ export const readBytes = do_('readBytes');
|
|
|
53
54
|
export const randomInt = do_('randomInt');
|
|
54
55
|
export const exec = do_('exec');
|
|
55
56
|
export const access = do_('access');
|
|
57
|
+
export const createExclusive = do_('createExclusive');
|
|
58
|
+
export const writeBytes = do_('writeBytes');
|
|
59
|
+
const writeLoop = (path) => {
|
|
60
|
+
const f = (offset, e) => e.step(r => {
|
|
61
|
+
if (r === undefined) {
|
|
62
|
+
return pure(ok(undefined));
|
|
63
|
+
}
|
|
64
|
+
const { first: [t, v], tail } = r;
|
|
65
|
+
if (t === 'error') {
|
|
66
|
+
return pure(resultError(v));
|
|
67
|
+
}
|
|
68
|
+
const lenV = length(v);
|
|
69
|
+
if ((lenV & 7n) !== 0n) {
|
|
70
|
+
return pure(resultError('invalid buffer size'));
|
|
71
|
+
}
|
|
72
|
+
return writeBytes(path, offset, v)
|
|
73
|
+
.step((r) => {
|
|
74
|
+
if (r[0] === 'error') {
|
|
75
|
+
return pure(r);
|
|
76
|
+
}
|
|
77
|
+
return f(offset + Number(lenV >> 3n), tail);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
return f;
|
|
81
|
+
};
|
|
82
|
+
export const writeFromStream = (path, e) => createExclusive(path)
|
|
83
|
+
.step(([r, v]) => {
|
|
84
|
+
if (r === 'error') {
|
|
85
|
+
return pure(resultError(v));
|
|
86
|
+
}
|
|
87
|
+
return writeLoop(path)(0, e);
|
|
88
|
+
});
|
|
89
|
+
export const stat = do_('stat');
|
|
56
90
|
export const createServer = do_('createServer');
|
|
57
91
|
export const listen = do_('listen');
|
|
58
92
|
export const forever = do_('forever');
|
|
@@ -35,6 +35,7 @@ import { memoryOperationMap } from "./memory/module.js";
|
|
|
35
35
|
import {} from "./module.f.js";
|
|
36
36
|
import { asBase, asNominal } from "../../types/nominal/module.f.js";
|
|
37
37
|
import { error, ok } from "../../types/result/module.f.js";
|
|
38
|
+
import { asyncTryCatch } from "../../types/result/module.js";
|
|
38
39
|
import { fromVec, listToVec, toVec } from "../../types/uint8array/module.f.js";
|
|
39
40
|
import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
|
|
40
41
|
/**
|
|
@@ -44,14 +45,6 @@ import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
|
|
|
44
45
|
* requires them present; this local view keeps the narrowing in one place.
|
|
45
46
|
*/
|
|
46
47
|
const createServer = http.createServer;
|
|
47
|
-
const tc = async (f) => {
|
|
48
|
-
try {
|
|
49
|
-
return ok(await f());
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
return error(e);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
48
|
const collect = async (v) => {
|
|
56
49
|
let result = [];
|
|
57
50
|
for await (const a of v) {
|
|
@@ -156,15 +149,15 @@ const readStdinByte = async () => {
|
|
|
156
149
|
const runNodeEffect = asyncRun({
|
|
157
150
|
...memoryOperationMap(),
|
|
158
151
|
all: async (...effects) => await Promise.all(effects.map(runNodeEffect)),
|
|
159
|
-
fetch: async (url) =>
|
|
152
|
+
fetch: async (url) => asyncTryCatch(async () => {
|
|
160
153
|
const response = await fetch(url);
|
|
161
154
|
if (!response.ok) {
|
|
162
155
|
throw new Error(`Fetch error: ${response.status} ${response.statusText}`);
|
|
163
156
|
}
|
|
164
157
|
return toVec(new Uint8Array(await response.arrayBuffer()));
|
|
165
158
|
}),
|
|
166
|
-
mkdir: (...p) =>
|
|
167
|
-
readFile: path =>
|
|
159
|
+
mkdir: (...p) => asyncTryCatch(async () => { await mkdir(...p); }),
|
|
160
|
+
readFile: path => asyncTryCatch(async () => {
|
|
168
161
|
const fileStats = await stat(path);
|
|
169
162
|
// if the file is too big, toVec should fail anyway but in this case we don't want to load the file.
|
|
170
163
|
if (fileStats.size > maxFileSizeBytes) {
|
|
@@ -172,16 +165,16 @@ const runNodeEffect = asyncRun({
|
|
|
172
165
|
}
|
|
173
166
|
return toVec(await readFile(path));
|
|
174
167
|
}),
|
|
175
|
-
readdir: (path, r) =>
|
|
168
|
+
readdir: (path, r) => asyncTryCatch(async () => (await readdir(path, { ...r, withFileTypes: true }))
|
|
176
169
|
.map(v => ({
|
|
177
170
|
name: v.name,
|
|
178
171
|
parentPath: normalize(v.parentPath),
|
|
179
172
|
isFile: v.isFile()
|
|
180
173
|
}))),
|
|
181
|
-
writeFile: (path, data) =>
|
|
182
|
-
rm: path =>
|
|
183
|
-
rename: (src, dst) =>
|
|
184
|
-
readBytes: (path, offset, size) =>
|
|
174
|
+
writeFile: (path, data) => asyncTryCatch(() => writeFile(path, fromVec(data))),
|
|
175
|
+
rm: path => asyncTryCatch(() => rm(path)),
|
|
176
|
+
rename: (src, dst) => asyncTryCatch(() => rename(src, dst)),
|
|
177
|
+
readBytes: (path, offset, size) => asyncTryCatch(async () => {
|
|
185
178
|
if (offset < 0) {
|
|
186
179
|
throw new Error(`Offset ${offset} is negative`);
|
|
187
180
|
}
|
|
@@ -199,8 +192,29 @@ const runNodeEffect = asyncRun({
|
|
|
199
192
|
}
|
|
200
193
|
}),
|
|
201
194
|
randomInt: async () => crypto.randomInt(2 ** 32),
|
|
202
|
-
access: path =>
|
|
203
|
-
|
|
195
|
+
access: path => asyncTryCatch(() => access(path)),
|
|
196
|
+
createExclusive: path => asyncTryCatch(async () => {
|
|
197
|
+
const fh = await open(path, 'wx');
|
|
198
|
+
await fh.close();
|
|
199
|
+
}),
|
|
200
|
+
writeBytes: (path, offset, data) => asyncTryCatch(async () => {
|
|
201
|
+
const fh = await open(path, 'r+');
|
|
202
|
+
try {
|
|
203
|
+
const buffer = fromVec(data);
|
|
204
|
+
// Loop over short writes so the whole Vec lands — a partial pwrite would
|
|
205
|
+
// leave a hole the publish-time size check could pass over.
|
|
206
|
+
let written = 0;
|
|
207
|
+
while (written < buffer.length) {
|
|
208
|
+
const { bytesWritten } = await fh.write(buffer, written, buffer.length - written, offset + written);
|
|
209
|
+
written += bytesWritten;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
await fh.close();
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
stat: path => asyncTryCatch(async () => ({ size: (await stat(path)).size })),
|
|
217
|
+
import: path => asyncTryCatch(() => asyncImport(path)),
|
|
204
218
|
exec: (command, stdin) => new Promise(resolve => {
|
|
205
219
|
const child = exec(command, (e, stdout, stderr) => resolve(e !== null ? ['error', e] : ok({ stdout, stderr })));
|
|
206
220
|
child.stdin?.end(stdin);
|
|
@@ -290,6 +290,60 @@ const readBytesOp = (path, offset, size) => readOperation((dir, p) => {
|
|
|
290
290
|
}
|
|
291
291
|
return ok(result);
|
|
292
292
|
})(path);
|
|
293
|
+
/** Total byte size of a chunk-list file (each chunk is byte-aligned). */
|
|
294
|
+
const fileSizeBytes = (chunks) => chunks.reduce((acc, c) => acc + Number(length(c) / 8n), 0);
|
|
295
|
+
/** Absent-path error for an already-existing exclusive create, mirroring `EEXIST`. */
|
|
296
|
+
const eexist = error({ code: 'EEXIST' });
|
|
297
|
+
const createExclusive = operation((dir, path) => {
|
|
298
|
+
if (path.length !== 1) {
|
|
299
|
+
return [dir, invalidPath];
|
|
300
|
+
}
|
|
301
|
+
const [name] = path;
|
|
302
|
+
// O_EXCL: fail if the name is already taken; otherwise create an empty file.
|
|
303
|
+
if (dir[name] !== undefined) {
|
|
304
|
+
return [dir, eexist];
|
|
305
|
+
}
|
|
306
|
+
return [{ ...dir, [name]: [] }, okVoid];
|
|
307
|
+
});
|
|
308
|
+
// The lock-free upload only ever writes sequentially at the current end of the
|
|
309
|
+
// staging file (`offset === size`), so the virtual model implements that append
|
|
310
|
+
// case exactly: it never creates (a missing file is `ENOENT`), never overwrites
|
|
311
|
+
// existing bytes, and never leaves a hole — matching the effect's contract for
|
|
312
|
+
// the one access pattern its callers use.
|
|
313
|
+
const writeBytesOp = (path, offset, data) => operation((dir, p) => {
|
|
314
|
+
if (p.length !== 1) {
|
|
315
|
+
return [dir, enoent];
|
|
316
|
+
}
|
|
317
|
+
const [name] = p;
|
|
318
|
+
const file = dir[name];
|
|
319
|
+
if (file === undefined) {
|
|
320
|
+
return [dir, enoent];
|
|
321
|
+
} // writeBytes never creates
|
|
322
|
+
if (!Array.isArray(file)) {
|
|
323
|
+
return [dir, error(`'${name}' is not a file`)];
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
326
|
+
return [dir, error(`Offset ${offset} is invalid`)];
|
|
327
|
+
}
|
|
328
|
+
const chunks = file;
|
|
329
|
+
if (offset !== fileSizeBytes(chunks)) {
|
|
330
|
+
return [dir, error(`writeBytes offset ${offset} must equal the file size (append-only)`)];
|
|
331
|
+
}
|
|
332
|
+
return [{ ...dir, [name]: [...chunks, data] }, okVoid];
|
|
333
|
+
})(path);
|
|
334
|
+
const statOp = readOperation((dir, path) => {
|
|
335
|
+
if (path.length !== 1) {
|
|
336
|
+
return enoent;
|
|
337
|
+
}
|
|
338
|
+
const file = dir[path[0]];
|
|
339
|
+
if (file === undefined) {
|
|
340
|
+
return enoent;
|
|
341
|
+
}
|
|
342
|
+
if (!Array.isArray(file)) {
|
|
343
|
+
return error(`'${path[0]}' is not a file`);
|
|
344
|
+
}
|
|
345
|
+
return ok({ size: fileSizeBytes(file) });
|
|
346
|
+
});
|
|
293
347
|
const map = {
|
|
294
348
|
all: (...a) => state => {
|
|
295
349
|
let e = [];
|
|
@@ -330,6 +384,9 @@ const map = {
|
|
|
330
384
|
rm,
|
|
331
385
|
rename,
|
|
332
386
|
readBytes: readBytesOp,
|
|
387
|
+
createExclusive,
|
|
388
|
+
writeBytes: writeBytesOp,
|
|
389
|
+
stat: statOp,
|
|
333
390
|
randomInt: () => state => [{ ...state, randomNext: state.randomNext + 1 }, state.randomNext],
|
|
334
391
|
exec: todo,
|
|
335
392
|
createServer: todo,
|
package/fs/effects/proof.f.d.ts
CHANGED
package/fs/effects/proof.f.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decode, do_, foldStep, forEachStep, match, pure } from "./module.f.js";
|
|
1
|
+
import { decode, do_, foldStep, forEachStep, lazy, match, pure } from "./module.f.js";
|
|
2
2
|
const assertPure = (e, expected) => {
|
|
3
3
|
const d = decode(e);
|
|
4
4
|
if (!d.done) {
|
|
@@ -10,6 +10,23 @@ const assertPure = (e, expected) => {
|
|
|
10
10
|
};
|
|
11
11
|
const next = match({ add: (a, b) => a + b });
|
|
12
12
|
export const proof = {
|
|
13
|
+
lazy: {
|
|
14
|
+
value: () => {
|
|
15
|
+
assertPure(lazy(() => 42), 42);
|
|
16
|
+
},
|
|
17
|
+
deferred: () => {
|
|
18
|
+
// The thunk runs only when the effect is decoded, not when `lazy` is called.
|
|
19
|
+
let evaluated = false;
|
|
20
|
+
const e = lazy(() => { evaluated = true; return 7; });
|
|
21
|
+
if (evaluated) {
|
|
22
|
+
throw 'lazy must not evaluate eagerly';
|
|
23
|
+
}
|
|
24
|
+
assertPure(e, 7);
|
|
25
|
+
if (!evaluated) {
|
|
26
|
+
throw 'decode must force the thunk';
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
},
|
|
13
30
|
foldStep: {
|
|
14
31
|
empty: () => {
|
|
15
32
|
const e = foldStep((x) => (s) => pure(s + x))(10)([]);
|
package/fs/fjs/module.f.js
CHANGED
|
@@ -5,14 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { compile } from "../djs/module.f.js";
|
|
7
7
|
import { main as testMain } from "../emergent_testing/module.f.js";
|
|
8
|
-
import { commands as casCommands
|
|
8
|
+
import { commands as casCommands } from "../cas/cli/module.f.js";
|
|
9
9
|
import { main as ciMain } from "../ci/module.f.js";
|
|
10
10
|
import { import_ } from "../effects/node/module.f.js";
|
|
11
11
|
import { dispatch } from "../cli/module.f.js";
|
|
12
12
|
import { casMcpServer } from "../cas/mcp/module.f.js";
|
|
13
|
-
import { toPath } from "../cas/module.f.js";
|
|
14
|
-
import { sha256 } from "../crypto/sha2/module.f.js";
|
|
15
|
-
import { join } from "../path/module.f.js";
|
|
16
13
|
import { pure } from "../effects/module.f.js";
|
|
17
14
|
const commands = [
|
|
18
15
|
{
|
|
@@ -33,10 +30,7 @@ const commands = [
|
|
|
33
30
|
{
|
|
34
31
|
names: ['mcp', 'm'],
|
|
35
32
|
description: 'Run an MCP server over stdio exposing the CAS as tools',
|
|
36
|
-
handler: ({ home }) =>
|
|
37
|
-
const c = fileCas(sha256)(home);
|
|
38
|
-
return casMcpServer(c, home, hash => join(home, toPath(hash))).step(() => pure(0));
|
|
39
|
-
},
|
|
33
|
+
handler: ({ home }) => casMcpServer(home).step(() => pure(0)),
|
|
40
34
|
},
|
|
41
35
|
{
|
|
42
36
|
names: ['ci', 'i'],
|
package/fs/html/module.f.js
CHANGED
|
@@ -46,15 +46,13 @@ const rawText = [
|
|
|
46
46
|
/**
|
|
47
47
|
* https://stackoverflow.com/questions/7381974/which-characters-need-to-be-escaped-in-html
|
|
48
48
|
*/
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
case greaterThanSign: return '>';
|
|
55
|
-
default: return fromCharCode(code);
|
|
56
|
-
}
|
|
49
|
+
const escapeTable = {
|
|
50
|
+
[quotationMark]: '"',
|
|
51
|
+
[ampersand]: '&',
|
|
52
|
+
[lessThanSign]: '<',
|
|
53
|
+
[greaterThanSign]: '>',
|
|
57
54
|
};
|
|
55
|
+
const escapeCharCode = (code) => escapeTable[code] ?? fromCharCode(code);
|
|
58
56
|
const escape = compose(stringToList)(map(escapeCharCode));
|
|
59
57
|
const node = (n) => typeof n === 'string' ? escape(n) : element(n);
|
|
60
58
|
const nodes = flatMap(node);
|
package/fs/mcp/module.f.d.ts
CHANGED
|
@@ -229,4 +229,4 @@ export type McpConfig = {
|
|
|
229
229
|
* - `tools/list` params (an optional pagination `cursor`) are validated and passed
|
|
230
230
|
* to the handler; invalid params → -32602.
|
|
231
231
|
*/
|
|
232
|
-
export declare const mcpStep:
|
|
232
|
+
export declare const mcpStep: ({ protocolVersion, capabilities, serverInfo, }: McpConfig) => <O extends Operation>(handlers: McpHandlers<O>) => (stateKey: Key<McpSessionState>) => (value: Unknown) => Effect<MemOp | O, Response | null>;
|
package/fs/mime/module.f.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Magic-byte MIME type detection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* for
|
|
7
|
-
*
|
|
4
|
+
* `detect` is a pure table lookup over the leading bytes of a `Vec`: it returns a
|
|
5
|
+
* MIME type string for the container formats whose signatures it knows, or `null`
|
|
6
|
+
* for anything else — text, unknown binary, or a `Vec` too short to match.
|
|
7
|
+
*
|
|
8
|
+
* Beside it, `detectStream` is the **streaming counterpart**: a byte-accepting
|
|
9
|
+
* state machine (length × magic-byte eliminator × UTF-8 validity DFA) that derives
|
|
10
|
+
* `{ length, mime_type, type }` by folding a CAS read stream in O(1) space, without
|
|
11
|
+
* ever buffering the blob into a single `maxLength`-bounded `Vec`. See the README
|
|
12
|
+
* for the factored design.
|
|
8
13
|
*
|
|
9
14
|
* The CAS store is type-agnostic and keeps raw bytes only, so type is never
|
|
10
15
|
* stored; it is recovered on read by sniffing the content. Callers decide what
|
|
@@ -29,11 +34,102 @@
|
|
|
29
34
|
*/
|
|
30
35
|
import { type Vec } from '../types/bit_vec/module.f.ts';
|
|
31
36
|
import type { Nullable } from '../types/nullable/module.f.ts';
|
|
37
|
+
import { type Effect, type Operation } from '../effects/module.f.ts';
|
|
38
|
+
import type { List } from '../effects/list/module.f.ts';
|
|
39
|
+
import type { IoResult } from '../effects/node/module.f.ts';
|
|
40
|
+
import { type Utf8State } from '../text/utf8/module.f.ts';
|
|
32
41
|
/**
|
|
33
42
|
* Detects the MIME type of `bytes` from its leading magic-byte signature.
|
|
34
43
|
*
|
|
35
|
-
* @returns the MIME type string for a
|
|
44
|
+
* @returns the MIME type string for a recognized format, or `null` when the
|
|
36
45
|
* leading bytes match no known signature (including any `Vec` shorter than
|
|
37
46
|
* the signature it might otherwise match).
|
|
38
47
|
*/
|
|
39
48
|
export declare const detect: (bytes: Vec) => Nullable<string>;
|
|
49
|
+
/**
|
|
50
|
+
* A magic-byte signature as a byte pattern. `null` entries are wildcards (the
|
|
51
|
+
* four little-endian size bytes of WebP, between its `RIFF` and `WEBP` markers).
|
|
52
|
+
*/
|
|
53
|
+
type Signature = {
|
|
54
|
+
readonly pattern: readonly Nullable<number>[];
|
|
55
|
+
readonly mime: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* `A_magic`: signature elimination. `scan` holds the byte offset and the still-viable
|
|
59
|
+
* signatures; a fully matched signature absorbs into `matched`, an empty viable set
|
|
60
|
+
* into `dead`. Settles within 12 bytes — `matched`/`dead` are absorbing.
|
|
61
|
+
*/
|
|
62
|
+
type MagicState = {
|
|
63
|
+
readonly tag: 'scan';
|
|
64
|
+
readonly pos: number;
|
|
65
|
+
readonly viable: readonly Signature[];
|
|
66
|
+
} | {
|
|
67
|
+
readonly tag: 'matched';
|
|
68
|
+
readonly mime: string;
|
|
69
|
+
} | {
|
|
70
|
+
readonly tag: 'dead';
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* `A_utf8`: a streaming UTF-8 validity-and-text check riding the shared
|
|
74
|
+
* `utf8ByteToCodePointOp` decoder. `st` is the decoder's mid-sequence state;
|
|
75
|
+
* `valid` is `false` once an illegal byte, surrogate, or out-of-range code point
|
|
76
|
+
* is seen — `valid: false` is absorbing. A non-null `st` at EOF (a truncated
|
|
77
|
+
* multi-byte sequence) is invalid. `text` is the orthogonal text-ness verdict: it
|
|
78
|
+
* is `false` once a non-text (control) code point is decoded, even though that
|
|
79
|
+
* code point is perfectly well-formed UTF-8 — `text: false` is absorbing too.
|
|
80
|
+
* Keeping the two distinct lets a valid-but-control blob (e.g. NUL) decode
|
|
81
|
+
* cleanly yet still classify as binary.
|
|
82
|
+
*/
|
|
83
|
+
type Utf8Detect = {
|
|
84
|
+
readonly st: Utf8State;
|
|
85
|
+
readonly valid: boolean;
|
|
86
|
+
readonly text: boolean;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The product state: running bit length × magic eliminator × UTF-8 validator.
|
|
90
|
+
* The factors never read each other; they meet only in {@link finish}.
|
|
91
|
+
*/
|
|
92
|
+
export type DetectState = {
|
|
93
|
+
readonly length: bigint;
|
|
94
|
+
readonly magic: MagicState;
|
|
95
|
+
readonly utf8: Utf8Detect;
|
|
96
|
+
};
|
|
97
|
+
/** The initial detector state `q₀`. */
|
|
98
|
+
export declare const detectInit: DetectState;
|
|
99
|
+
/**
|
|
100
|
+
* Folds one `Vec` chunk into the detector state (`δ` over a whole chunk). Length
|
|
101
|
+
* always advances by the chunk's bit length; per-byte iteration stops as soon as
|
|
102
|
+
* the verdict is fixed (see {@link isSettled}), so large blobs — including large
|
|
103
|
+
* magic-matched ones — cost ≈ length counting.
|
|
104
|
+
*/
|
|
105
|
+
export declare const push: (s: DetectState) => (chunk: Vec) => DetectState;
|
|
106
|
+
/** The metadata read off the detector at end-of-stream. */
|
|
107
|
+
export type DetectMeta = {
|
|
108
|
+
readonly length: bigint;
|
|
109
|
+
readonly mime_type: string;
|
|
110
|
+
readonly type: 'text' | 'base64';
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Reads the answer off the final state (`λ`). Reproduces the three-way result of
|
|
114
|
+
* the pure path: magic hit → `base64` + detected mime; else whole-blob-valid UTF-8
|
|
115
|
+
* that is also all-text (byte-aligned, no invalidity, no control bytes) → `text` +
|
|
116
|
+
* `text/plain`; else → `base64` + `application/octet-stream`. A valid-but-control
|
|
117
|
+
* blob (NUL, other controls) is well-formed UTF-8 yet falls through to the binary
|
|
118
|
+
* branch.
|
|
119
|
+
*/
|
|
120
|
+
export declare const finish: (s: DetectState) => DetectMeta;
|
|
121
|
+
/**
|
|
122
|
+
* Classifies a whole `Vec` with the same state machine as {@link detectStream}.
|
|
123
|
+
* The single-buffer counterpart for callers that already hold the bytes (the
|
|
124
|
+
* `cas_get` `content: true` path materializes the blob anyway): both paths read
|
|
125
|
+
* the three-way `{ length, mime_type, type }` verdict from one machine instead of
|
|
126
|
+
* re-deriving it from `detect` + a separate UTF-8 check.
|
|
127
|
+
*/
|
|
128
|
+
export declare const detectVec: (bytes: Vec) => DetectMeta;
|
|
129
|
+
/**
|
|
130
|
+
* Folds a CAS read stream through {@link push} and reads {@link finish} at EOF,
|
|
131
|
+
* deriving `cas_get` metadata without ever materializing the blob. A read `error`
|
|
132
|
+
* item short-circuits into the `IoResult` error.
|
|
133
|
+
*/
|
|
134
|
+
export declare const detectStream: <O extends Operation>(stream: List<O, IoResult<Vec>>) => Effect<O, IoResult<DetectMeta>>;
|
|
135
|
+
export {};
|