functionalscript 0.38.0 → 0.39.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/fjs/bnf/token_symbol/module.f.d.ts +38 -0
- package/fjs/bnf/token_symbol/module.f.js +48 -0
- package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
- package/fjs/bnf/token_symbol/proof.f.js +37 -0
- package/fjs/cas/cli/module.f.js +6 -9
- package/fjs/cas/evo/mcp/module.f.d.ts +9 -1
- package/fjs/cas/evo/mcp/module.f.js +52 -14
- package/fjs/cas/evo/mcp/proof.f.d.ts +2 -0
- package/fjs/cas/evo/mcp/proof.f.js +33 -2
- package/fjs/cas/evo/module.f.d.ts +76 -12
- package/fjs/cas/evo/module.f.js +129 -17
- package/fjs/cas/evo/proof.f.d.ts +7 -0
- package/fjs/cas/evo/proof.f.js +119 -0
- package/fjs/cas/mcp/module.f.js +22 -20
- package/fjs/cas/mcp/proof.f.d.ts +1 -1
- package/fjs/cas/mcp/proof.f.js +9 -9
- package/fjs/cas/module.f.js +64 -37
- package/fjs/cas/proof.f.d.ts +3 -9
- package/fjs/cas/proof.f.js +94 -74
- package/fjs/ci/bun/module.f.js +3 -3
- package/fjs/ci/common/module.f.d.ts +1 -2
- package/fjs/ci/common/module.f.js +1 -5
- package/fjs/ci/config/module.f.d.ts +5 -5
- package/fjs/ci/config/module.f.js +5 -5
- package/fjs/ci/deno/module.f.js +3 -3
- package/fjs/ci/module.f.js +3 -4
- package/fjs/ci/node/module.f.js +11 -9
- package/fjs/ci/proof.f.js +2 -0
- package/fjs/cli/module.f.js +2 -2
- package/fjs/common/monoid/module.f.d.ts +46 -2
- package/fjs/common/monoid/module.f.js +44 -0
- package/fjs/common/monoid/proof.f.d.ts +5 -0
- package/fjs/common/monoid/proof.f.js +27 -1
- package/fjs/dev/module.f.js +25 -25
- package/fjs/djs/module.f.js +5 -8
- package/fjs/djs/parser/module.f.d.ts +3 -0
- package/fjs/djs/parser/module.f.js +17 -0
- package/fjs/djs/transpiler/module.f.js +6 -6
- package/fjs/effects/eff/module.f.d.ts +72 -0
- package/fjs/effects/eff/module.f.js +46 -0
- package/fjs/effects/eff/proof.f.d.ts +11 -0
- package/fjs/effects/eff/proof.f.js +58 -0
- package/fjs/effects/list/module.f.d.ts +11 -5
- package/fjs/effects/list/module.f.js +2 -1
- package/fjs/effects/memory/proof.f.js +7 -8
- package/fjs/effects/module.f.d.ts +334 -52
- package/fjs/effects/module.f.js +253 -45
- package/fjs/effects/node/memory/proof.js +3 -2
- package/fjs/effects/node/module.f.d.ts +1 -1
- package/fjs/effects/node/module.f.js +9 -11
- package/fjs/effects/node/proof.f.js +18 -10
- package/fjs/effects/proof.f.d.ts +32 -6
- package/fjs/effects/proof.f.js +131 -39
- package/fjs/emergent_testing/module.f.js +55 -39
- package/fjs/emergent_testing/proof.f.d.ts +4 -0
- package/fjs/emergent_testing/proof.f.js +53 -20
- package/fjs/mcp/module.f.js +8 -8
- package/fjs/mcp/proof.f.js +5 -4
- package/fjs/mcp/stdio/module.f.d.ts +2 -2
- package/fjs/mcp/stdio/module.f.js +12 -12
- package/fjs/media/type/module.f.js +2 -2
- package/fjs/media/type/proof.f.js +7 -10
- package/fjs/module.f.js +4 -8
- package/fjs/types/bigint/module.f.js +4 -3
- package/fjs/types/bit_vec/module.f.js +5 -15
- package/fjs/types/btree/remove/module.f.d.ts +5 -0
- package/fjs/types/btree/remove/module.f.js +12 -0
- package/fjs/types/list/module.f.d.ts +21 -0
- package/fjs/types/list/module.f.js +17 -0
- package/fjs/types/list/proof.f.d.ts +1 -0
- package/fjs/types/list/proof.f.js +24 -1
- package/fjs/types/number/module.f.js +2 -1
- package/fjs/types/string/module.f.js +10 -2
- package/fjs/website/module.f.js +2 -3
- package/package.json +3 -3
package/fjs/cas/module.f.js
CHANGED
|
@@ -7,7 +7,8 @@ import { sha256 } from '../crypto/sha2/module.f.js';
|
|
|
7
7
|
import { join, normalize, parse } from '../path/module.f.js';
|
|
8
8
|
import { empty, length, maxLength, maxLengthBytes, msb, vec } from '../types/bit_vec/module.f.js';
|
|
9
9
|
import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.js';
|
|
10
|
-
import { foldStep, forEachStep, okStep, pure } from '../effects/module.f.js';
|
|
10
|
+
import { foldStep, forEachStep, history, historyStep, okStep, pure, step } from '../effects/module.f.js';
|
|
11
|
+
import { eff } from '../effects/eff/module.f.js';
|
|
11
12
|
import { access, createExclusive, isNotFound, mkdir, now, randomInt, readBytes, readdir, rename, rm, stat, writeBytes, } from '../effects/node/module.f.js';
|
|
12
13
|
import { toOption } from '../types/nullable/module.f.js';
|
|
13
14
|
import { error, ok, unwrap } from '../types/result/module.f.js';
|
|
@@ -32,7 +33,8 @@ export const toPath = (key) => {
|
|
|
32
33
|
* process.
|
|
33
34
|
*/
|
|
34
35
|
export const collectRead = (stream) => {
|
|
35
|
-
const loop = (acc) => (s) => s
|
|
36
|
+
const loop = (acc) => (s) => eff(s)
|
|
37
|
+
.step((node) => {
|
|
36
38
|
if (node === undefined) {
|
|
37
39
|
return pure(ok(acc));
|
|
38
40
|
}
|
|
@@ -45,7 +47,8 @@ export const collectRead = (stream) => {
|
|
|
45
47
|
return pure(error(`cas blob exceeds maximum vector length of ${maxLength} bits`));
|
|
46
48
|
}
|
|
47
49
|
return loop(msb.concat(acc)(v))(tail);
|
|
48
|
-
})
|
|
50
|
+
})
|
|
51
|
+
.value;
|
|
49
52
|
return loop(empty)(stream);
|
|
50
53
|
};
|
|
51
54
|
/** Maximum chunk size for streaming reads: the largest `Vec` the runtime allows. */
|
|
@@ -75,13 +78,18 @@ const deadlineOf = (name) => Number(name.slice(0, name.indexOf('-')));
|
|
|
75
78
|
* still-live lease is left alone; the fencing rename keeps even a misjudged
|
|
76
79
|
* reclaim fail-safe (worst case: that upload restarts).
|
|
77
80
|
*/
|
|
78
|
-
const gcStage = (stageDir) => now()
|
|
81
|
+
const gcStage = (stageDir) => eff(now())
|
|
82
|
+
.step(() => readdir(stageDir, {}))
|
|
83
|
+
.step(([k, v], t) => {
|
|
79
84
|
if (k === 'error') {
|
|
80
85
|
return pure(undefined);
|
|
81
86
|
}
|
|
82
87
|
const expired = v.flatMap(d => d.isFile && deadlineOf(d.name) < t ? [d.name] : []);
|
|
83
|
-
return forEachStep((
|
|
84
|
-
|
|
88
|
+
return forEachStep(pure(expired), name => eff(rm(join(stageDir, name)))
|
|
89
|
+
.step(() => pure(undefined))
|
|
90
|
+
.value);
|
|
91
|
+
})
|
|
92
|
+
.value;
|
|
85
93
|
/**
|
|
86
94
|
* Builds a content-addressable storage facade from a SHA-2 implementation.
|
|
87
95
|
*/
|
|
@@ -92,7 +100,7 @@ export const fileCas = (sha2) => (path) => {
|
|
|
92
100
|
return {
|
|
93
101
|
read: (hash) => {
|
|
94
102
|
const p = join(path, toPath(hash));
|
|
95
|
-
const loop = (offset) => readBytes(p, offset, chunkBytes)
|
|
103
|
+
const loop = (offset) => eff(readBytes(p, offset, chunkBytes))
|
|
96
104
|
.step((result) => {
|
|
97
105
|
const [t, v] = result;
|
|
98
106
|
// A missing shard or read error is an explicit error item, never EOF.
|
|
@@ -104,7 +112,8 @@ export const fileCas = (sha2) => (path) => {
|
|
|
104
112
|
return length(v) === 0n
|
|
105
113
|
? elEmpty()
|
|
106
114
|
: nonEmpty(ok(v), loop(offset + chunkBytes));
|
|
107
|
-
})
|
|
115
|
+
})
|
|
116
|
+
.value;
|
|
108
117
|
return loop(0);
|
|
109
118
|
},
|
|
110
119
|
// Lock-free staging upload (issues/cas/staging-lease.md): stream each chunk straight
|
|
@@ -125,17 +134,20 @@ export const fileCas = (sha2) => (path) => {
|
|
|
125
134
|
const rel = toPath(hash);
|
|
126
135
|
const dst = join(path, rel);
|
|
127
136
|
const dstDir = join(path, ...parse(rel).slice(0, -1));
|
|
128
|
-
return mkdir(dstDir, { recursive: true })
|
|
137
|
+
return eff(mkdir(dstDir, { recursive: true }))
|
|
129
138
|
.step(() => rename(curPath, dst))
|
|
130
139
|
.step(() => rm(curPath))
|
|
131
140
|
.step(() => stat(dst))
|
|
132
|
-
.step(st => pure(st[0] === 'ok' && st[1].size === offset ? ok(hash) : error('publish size mismatch')))
|
|
141
|
+
.step(st => pure(st[0] === 'ok' && st[1].size === offset ? ok(hash) : error('publish size mismatch')))
|
|
142
|
+
.value;
|
|
133
143
|
};
|
|
134
144
|
// Any streaming error fails closed: delete the partial file, return the error.
|
|
135
|
-
const fail = (curPath, e) => rm(curPath)
|
|
136
|
-
|
|
145
|
+
const fail = (curPath, e) => eff(rm(curPath))
|
|
146
|
+
.step(() => pure(error(e)))
|
|
147
|
+
.value;
|
|
148
|
+
return eff(gcStage(stageDir)).step(() => eff(random256).step(rnd => {
|
|
137
149
|
const rndStr = vecToCBase32(rnd);
|
|
138
|
-
const loop = (state, offset, curPath) => (stream) => stream
|
|
150
|
+
const loop = (state, offset, curPath) => (stream) => eff(stream)
|
|
139
151
|
.step((node) => {
|
|
140
152
|
if (node === undefined) {
|
|
141
153
|
return publish(state, offset, curPath);
|
|
@@ -145,7 +157,7 @@ export const fileCas = (sha2) => (path) => {
|
|
|
145
157
|
return fail(curPath, first[1]);
|
|
146
158
|
}
|
|
147
159
|
const chunk = first[1];
|
|
148
|
-
return writeBytes(curPath, offset, chunk)
|
|
160
|
+
return eff(writeBytes(curPath, offset, chunk))
|
|
149
161
|
.step(wb => {
|
|
150
162
|
if (wb[0] === 'error') {
|
|
151
163
|
return fail(curPath, wb[1]);
|
|
@@ -153,49 +165,59 @@ export const fileCas = (sha2) => (path) => {
|
|
|
153
165
|
const newState = sha2.append(chunk)(state);
|
|
154
166
|
const newOffset = offset + Number(length(chunk) / 8n);
|
|
155
167
|
// Renew the lease: rename to a fresh deadline (keeps `delta` constant).
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
.
|
|
168
|
+
// The new path is still needed after the rename, to recurse with,
|
|
169
|
+
// so the rename captures it rather than closing over it.
|
|
170
|
+
const nextPath = step(now(), t => pure(join(stageDir, stageName(t + leaseDelta, rndStr))));
|
|
171
|
+
const renamed = historyStep(history(nextPath), next => rename(curPath, next));
|
|
172
|
+
return step(renamed, ([[rt, v], next]) => rt === 'error'
|
|
173
|
+
? fail(curPath, v)
|
|
174
|
+
: loop(newState, newOffset, next)(tail));
|
|
175
|
+
})
|
|
176
|
+
.value;
|
|
177
|
+
})
|
|
178
|
+
.value;
|
|
179
|
+
return eff(mkdir(stageDir, { recursive: true }))
|
|
180
|
+
.step(() => now())
|
|
167
181
|
.step(t0 => {
|
|
168
182
|
const path0 = join(stageDir, stageName(t0 + leaseDelta, rndStr));
|
|
169
|
-
return createExclusive(path0)
|
|
170
|
-
.step(okStep(() => loop(sha2.init, 0, path0)(payload)))
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
return eff(createExclusive(path0))
|
|
184
|
+
.step(okStep(() => loop(sha2.init, 0, path0)(payload)))
|
|
185
|
+
.value;
|
|
186
|
+
})
|
|
187
|
+
.value;
|
|
188
|
+
}).value).value;
|
|
173
189
|
},
|
|
174
190
|
list: () =>
|
|
175
191
|
// A fresh store has no `.cas` directory yet. Treat *only* that case as an
|
|
176
192
|
// empty store, mirroring how `read` maps a missing shard to an error item.
|
|
177
193
|
// A `.cas` that exists but cannot be read (permissions, corruption) is a
|
|
178
194
|
// genuine storage error and is surfaced, not masked as "no hashes".
|
|
179
|
-
access(storePrefix)
|
|
195
|
+
eff(access(storePrefix))
|
|
196
|
+
.step(a => {
|
|
180
197
|
if (a[0] === 'error') {
|
|
181
198
|
if (isNotFound(a[1])) {
|
|
182
199
|
return pure([]);
|
|
183
200
|
}
|
|
184
201
|
throw a[1];
|
|
185
202
|
}
|
|
186
|
-
return readdir(storePrefix, { recursive: true })
|
|
203
|
+
return eff(readdir(storePrefix, { recursive: true }))
|
|
187
204
|
.step(r => pure(unwrap(r).flatMap(({ name, parentPath, isFile }) => toOption(isFile
|
|
188
205
|
? cBase32ToVec(normalize(parentPath).substring(normalizedStorePrefix.length).replaceAll('/', '') + name)
|
|
189
|
-
: null))))
|
|
190
|
-
|
|
206
|
+
: null))))
|
|
207
|
+
.value;
|
|
208
|
+
})
|
|
209
|
+
.value,
|
|
191
210
|
url: (hash) => join(path, toPath(hash))
|
|
192
211
|
};
|
|
193
212
|
};
|
|
194
213
|
/** 256-bit random `Vec` built from 8 sequential `randomInt` (32-bit) calls. */
|
|
195
|
-
const random256 = foldStep(
|
|
214
|
+
const random256 = foldStep(pure([0, 1, 2, 3, 4, 5, 6, 7]), empty, () => (acc) => eff(randomInt())
|
|
215
|
+
.step(r => pure(msb.concat(acc)(vec(32n)(BigInt(r)))))
|
|
216
|
+
.value);
|
|
196
217
|
/** Streams any file at `filePath` in `<=128 KiB` chunks as a `ListEffect` of `ok` items. */
|
|
197
218
|
const streamFile = (filePath) => {
|
|
198
|
-
const loop = (offset) => readBytes(filePath, offset, chunkBytes)
|
|
219
|
+
const loop = (offset) => eff(readBytes(filePath, offset, chunkBytes))
|
|
220
|
+
.step((result) => {
|
|
199
221
|
if (result[0] === 'error') {
|
|
200
222
|
return nonEmpty(result, elEmpty());
|
|
201
223
|
}
|
|
@@ -203,7 +225,8 @@ const streamFile = (filePath) => {
|
|
|
203
225
|
return length(chunk) === 0n
|
|
204
226
|
? elEmpty()
|
|
205
227
|
: nonEmpty(ok(chunk), loop(offset + chunkBytes));
|
|
206
|
-
})
|
|
228
|
+
})
|
|
229
|
+
.value;
|
|
207
230
|
return loop(0);
|
|
208
231
|
};
|
|
209
232
|
/**
|
|
@@ -224,5 +247,9 @@ cas.write(streamFile(path));
|
|
|
224
247
|
export const casUpload = (home) => (fileName) => {
|
|
225
248
|
const src = join(home, 'cas_upload', fileName);
|
|
226
249
|
const c = fileCas(sha256)(home);
|
|
227
|
-
return casAddFile(c)(src)
|
|
250
|
+
return eff(casAddFile(c)(src))
|
|
251
|
+
.step(okStep(v => eff(rm(src))
|
|
252
|
+
.step(() => pure(ok(v)))
|
|
253
|
+
.value))
|
|
254
|
+
.value;
|
|
228
255
|
};
|
package/fjs/cas/proof.f.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { type FileCasOperation } from './module.f.ts';
|
|
2
|
-
import { type Effect } from '../effects/module.f.ts';
|
|
3
|
-
import { type ReadFile, type WriteFile, type Rm, type Mkdir } from '../effects/node/module.f.ts';
|
|
4
|
-
type TestOp = FileCasOperation | WriteFile | ReadFile | Rm | Mkdir;
|
|
5
|
-
declare const testAddBigFile: () => Effect<TestOp, void>;
|
|
6
|
-
declare const testAddAndGetBigFile: () => Effect<TestOp, void>;
|
|
7
1
|
export declare const proof: {
|
|
8
|
-
addBigFile:
|
|
9
|
-
addAndGetBigFile:
|
|
2
|
+
addBigFile: () => void;
|
|
3
|
+
addAndGetBigFile: () => void;
|
|
10
4
|
casWriteRead: () => void;
|
|
11
5
|
casReadMissingShard: () => void;
|
|
12
6
|
casWriteMultiChunk: () => void;
|
|
@@ -14,6 +8,7 @@ export declare const proof: {
|
|
|
14
8
|
casWriteErrorItemAborts: () => void;
|
|
15
9
|
casWriteReadExceedsMaxLength: () => void;
|
|
16
10
|
casWriteGcReclaimsExpired: () => void;
|
|
11
|
+
casWriteGcSkipsLiveLease: () => void;
|
|
17
12
|
casUploadSuccess: () => void;
|
|
18
13
|
casUploadFailureKeepsSource: () => void;
|
|
19
14
|
collectReadDrainsChunks: () => void;
|
|
@@ -21,4 +16,3 @@ export declare const proof: {
|
|
|
21
16
|
collectReadOverflow: () => void;
|
|
22
17
|
casListPropagatesNonNotFoundAccessError: () => void;
|
|
23
18
|
};
|
|
24
|
-
export {};
|
package/fjs/cas/proof.f.js
CHANGED
|
@@ -2,7 +2,7 @@ import { length, maxLength, msb, vec, vec8 } from '../types/bit_vec/module.f.js'
|
|
|
2
2
|
import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.js';
|
|
3
3
|
import { computeSync, sha256 } from '../crypto/sha2/module.f.js';
|
|
4
4
|
import { fileCas, casAddFile, collectRead, casUpload } from './module.f.js';
|
|
5
|
-
import {
|
|
5
|
+
import { match, pure, runPure, step } from '../effects/module.f.js';
|
|
6
6
|
import { mkdir, writeFile, rm, readFile, access } from '../effects/node/module.f.js';
|
|
7
7
|
import { error, ok } from '../types/result/module.f.js';
|
|
8
8
|
import { emptyState, virtual } from '../effects/node/virtual/module.f.js';
|
|
@@ -10,6 +10,23 @@ import { join } from '../path/module.f.js';
|
|
|
10
10
|
import { nonEmpty, empty } from '../effects/list/module.f.js';
|
|
11
11
|
import { assert, assertEq, assertNotNullish } from '../asserts/module.f.js';
|
|
12
12
|
const testDir = './test-cas-cli';
|
|
13
|
+
// Names the command a `FileCasOperation` effect stops at, so a proof can assert
|
|
14
|
+
// on it and resume the continuation without reading the `Do` layout. The map
|
|
15
|
+
// has to list every operation the CAS can perform — that is what makes it total,
|
|
16
|
+
// and what makes a new operation a compile error here rather than a silent gap.
|
|
17
|
+
const casCommand = match({
|
|
18
|
+
access: () => 'access',
|
|
19
|
+
createExclusive: () => 'createExclusive',
|
|
20
|
+
mkdir: () => 'mkdir',
|
|
21
|
+
now: () => 'now',
|
|
22
|
+
randomInt: () => 'randomInt',
|
|
23
|
+
readBytes: () => 'readBytes',
|
|
24
|
+
readdir: () => 'readdir',
|
|
25
|
+
rename: () => 'rename',
|
|
26
|
+
rm: () => 'rm',
|
|
27
|
+
stat: () => 'stat',
|
|
28
|
+
writeBytes: () => 'writeBytes',
|
|
29
|
+
});
|
|
13
30
|
// Create a 128 KiB big file content (at the max Vec size limit)
|
|
14
31
|
// This tests the boundary where files are at the chunk size limit
|
|
15
32
|
const createBigFileContent = () => {
|
|
@@ -18,68 +35,54 @@ const createBigFileContent = () => {
|
|
|
18
35
|
return vec(byteCount * 8n)(0x42424242n);
|
|
19
36
|
};
|
|
20
37
|
// Test adding a big file and verifying the hash
|
|
21
|
-
const testAddBigFile = () =>
|
|
22
|
-
const bigContent = createBigFileContent();
|
|
38
|
+
const testAddBigFile = () => {
|
|
23
39
|
const bigFilePath = `${testDir}/big-file.bin`;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return casAddFile(cas)(bigFilePath).step(addRes => {
|
|
30
|
-
if (addRes[0] === 'error') {
|
|
31
|
-
throw new Error(`Failed to add file to CAS: ${addRes[1]}`);
|
|
32
|
-
}
|
|
33
|
-
const hash = addRes[1];
|
|
34
|
-
// Verify hash is 256 bits (SHA-256)
|
|
35
|
-
if (length(hash) !== 256n) {
|
|
36
|
-
throw new Error(`Expected hash length 256 bits, got ${length(hash)}`);
|
|
37
|
-
}
|
|
38
|
-
// Verify hash can be encoded/decoded
|
|
39
|
-
const hashCBase32 = vecToCBase32(hash);
|
|
40
|
-
assertNotNullish(cBase32ToVec(hashCBase32), new Error('Failed to decode hash from base32'));
|
|
41
|
-
return rm(testDir).step(() => pure(undefined));
|
|
42
|
-
});
|
|
40
|
+
const cas = fileCas(sha256)(testDir);
|
|
41
|
+
const x0 = step(mkdir(testDir, { recursive: true }), () => writeFile(bigFilePath, createBigFileContent()));
|
|
42
|
+
const x1 = step(x0, writeRes => {
|
|
43
|
+
assert(writeRes[0] === 'ok', ['failed to write test file', writeRes]);
|
|
44
|
+
return casAddFile(cas)(bigFilePath);
|
|
43
45
|
});
|
|
44
|
-
|
|
46
|
+
const x2 = step(x1, addRes => {
|
|
47
|
+
assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes]);
|
|
48
|
+
const hash = addRes[1];
|
|
49
|
+
// Verify hash is 256 bits (SHA-256)
|
|
50
|
+
assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)]);
|
|
51
|
+
// Verify hash can be encoded/decoded
|
|
52
|
+
assertNotNullish(cBase32ToVec(vecToCBase32(hash)), 'failed to decode hash from cBase32');
|
|
53
|
+
return rm(testDir);
|
|
54
|
+
});
|
|
55
|
+
return step(x2, () => pure(undefined));
|
|
56
|
+
};
|
|
45
57
|
// Test adding and retrieving a big file
|
|
46
|
-
const testAddAndGetBigFile = () =>
|
|
47
|
-
.step(() => {
|
|
58
|
+
const testAddAndGetBigFile = () => {
|
|
48
59
|
const bigContent = createBigFileContent();
|
|
49
60
|
const bigFilePath = `${testDir}/big-file.bin`;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const cas = fileCas(sha256)(testDir);
|
|
56
|
-
return casAddFile(cas)(bigFilePath)
|
|
57
|
-
.step(addRes => {
|
|
58
|
-
if (addRes[0] === 'error') {
|
|
59
|
-
throw new Error(`Failed to add file to CAS: ${addRes[1]}`);
|
|
60
|
-
}
|
|
61
|
-
const hash = addRes[1];
|
|
62
|
-
const storedPath = cas.url(hash);
|
|
63
|
-
// Verify file is stored at the expected location
|
|
64
|
-
return readFile(storedPath).step(readRes => {
|
|
65
|
-
if (readRes[0] === 'error') {
|
|
66
|
-
throw new Error(`Failed to read stored file: ${readRes[1]}`);
|
|
67
|
-
}
|
|
68
|
-
const storedContent = readRes[1];
|
|
69
|
-
// Verify content is the same size as original
|
|
70
|
-
const storedLen = length(storedContent);
|
|
71
|
-
const originalLen = length(bigContent);
|
|
72
|
-
if (storedLen !== originalLen) {
|
|
73
|
-
throw new Error(`Content size mismatch: stored ${storedLen} bits, expected ${originalLen} bits`);
|
|
74
|
-
}
|
|
75
|
-
return rm(testDir).step(() => pure(undefined));
|
|
76
|
-
});
|
|
77
|
-
});
|
|
61
|
+
const cas = fileCas(sha256)(testDir);
|
|
62
|
+
const x0 = step(mkdir(testDir, { recursive: true }), () => writeFile(bigFilePath, bigContent));
|
|
63
|
+
const x1 = step(x0, writeRes => {
|
|
64
|
+
assert(writeRes[0] === 'ok', ['failed to write test file', writeRes]);
|
|
65
|
+
return casAddFile(cas)(bigFilePath);
|
|
78
66
|
});
|
|
79
|
-
|
|
67
|
+
// Verify the file is stored at the expected location
|
|
68
|
+
const x2 = step(x1, addRes => {
|
|
69
|
+
assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes]);
|
|
70
|
+
return readFile(cas.url(addRes[1]));
|
|
71
|
+
});
|
|
72
|
+
const x3 = step(x2, readRes => {
|
|
73
|
+
assert(readRes[0] === 'ok', ['failed to read stored file', readRes]);
|
|
74
|
+
// Verify content is the same size as original
|
|
75
|
+
assertEq(length(readRes[1]), length(bigContent), 'stored content size mismatch');
|
|
76
|
+
return rm(testDir);
|
|
77
|
+
});
|
|
78
|
+
return step(x3, () => pure(undefined));
|
|
79
|
+
};
|
|
80
80
|
export const proof = {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// Both effects must be interpreted, not merely built: an `Effect` is inert
|
|
82
|
+
// data, so returning one from a proof runs none of its continuations and
|
|
83
|
+
// asserts nothing.
|
|
84
|
+
addBigFile: () => { virtual(emptyState)(testAddBigFile()); },
|
|
85
|
+
addAndGetBigFile: () => { virtual(emptyState)(testAddAndGetBigFile()); },
|
|
83
86
|
//
|
|
84
87
|
casWriteRead: () => {
|
|
85
88
|
// Round-trip a single-chunk payload through the real streaming CAS: `write` returns
|
|
@@ -92,7 +95,7 @@ export const proof = {
|
|
|
92
95
|
const hash = writeResult[1];
|
|
93
96
|
assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)]);
|
|
94
97
|
assertEq(msb.cmp(hash)(computeSync(sha256)([content])), 0, 'write hash mismatch');
|
|
95
|
-
const drain = (acc) => (stream) =>
|
|
98
|
+
const drain = (acc) => (stream) => step(stream, (node) => {
|
|
96
99
|
if (node === undefined) {
|
|
97
100
|
return pure(ok(acc));
|
|
98
101
|
}
|
|
@@ -125,7 +128,7 @@ export const proof = {
|
|
|
125
128
|
assert(writeResult[0] === 'ok', ['expected write ok', writeResult]);
|
|
126
129
|
const hash = writeResult[1];
|
|
127
130
|
assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'multi-chunk write hash mismatch');
|
|
128
|
-
const drain = (acc) => (stream) =>
|
|
131
|
+
const drain = (acc) => (stream) => step(stream, (node) => {
|
|
129
132
|
if (node === undefined) {
|
|
130
133
|
return pure(ok(acc));
|
|
131
134
|
}
|
|
@@ -181,7 +184,7 @@ export const proof = {
|
|
|
181
184
|
const hash = w[1];
|
|
182
185
|
assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'oversized write hash mismatch');
|
|
183
186
|
// Fold the read stream straight into a fresh SHA-2 state — never one `Vec`.
|
|
184
|
-
const rehash = (state) => (stream) =>
|
|
187
|
+
const rehash = (state) => (stream) => step(stream, (node) => {
|
|
185
188
|
if (node === undefined) {
|
|
186
189
|
return pure(ok(sha256.end(state)));
|
|
187
190
|
}
|
|
@@ -212,6 +215,23 @@ export const proof = {
|
|
|
212
215
|
const [, present] = virtual(state1)(access(stalePath));
|
|
213
216
|
assert(present[0] === 'error', 'expected GC to reclaim the expired staging file');
|
|
214
217
|
},
|
|
218
|
+
casWriteGcSkipsLiveLease: () => {
|
|
219
|
+
// A staging file whose deadline is still in the future is left alone by the GC
|
|
220
|
+
// that `write` runs before staging its own file.
|
|
221
|
+
const livePath = join('.', '.cas', '_stage', '0000000000002000000-live');
|
|
222
|
+
const state0 = {
|
|
223
|
+
...emptyState,
|
|
224
|
+
epochNs: 1_000_000,
|
|
225
|
+
root: { '.cas': { '_stage': { '0000000000002000000-live': [vec8(0x99n)] } } },
|
|
226
|
+
};
|
|
227
|
+
const content = vec8(0x2an);
|
|
228
|
+
const c = fileCas(sha256)('.');
|
|
229
|
+
const x = c.write(nonEmpty(ok(content), empty()));
|
|
230
|
+
const [state1, w] = virtual(state0)(x);
|
|
231
|
+
assert(w[0] === 'ok', ['expected write ok', w]);
|
|
232
|
+
const [, present] = virtual(state1)(access(livePath));
|
|
233
|
+
assert(present[0] === 'ok', 'expected GC to leave the live staging file alone');
|
|
234
|
+
},
|
|
215
235
|
casUploadSuccess: () => {
|
|
216
236
|
// A successful upload returns the hash and deletes the source file from cas_upload/.
|
|
217
237
|
const content = vec8(0x2an);
|
|
@@ -239,18 +259,19 @@ export const proof = {
|
|
|
239
259
|
// The common path: every chunk is `ok`, so collectRead concatenates them all
|
|
240
260
|
// and returns the whole blob as one `Vec`.
|
|
241
261
|
const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(ok(vec8(0x22n)), empty()));
|
|
242
|
-
const
|
|
243
|
-
assert(
|
|
244
|
-
assertEq(
|
|
262
|
+
const o = runPure(collectRead(stream));
|
|
263
|
+
assert(o.length === 1, 'expected collectRead to finish without issuing a command');
|
|
264
|
+
assertEq(o[0][0], 'ok');
|
|
245
265
|
},
|
|
246
266
|
collectReadPropagatesErrorItem: () => {
|
|
247
267
|
// An error item mid-stream short-circuits collectRead with that same error.
|
|
248
268
|
const boom = error('boom');
|
|
249
269
|
const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(boom, empty()));
|
|
250
|
-
const
|
|
251
|
-
assert(
|
|
252
|
-
|
|
253
|
-
assertEq(
|
|
270
|
+
const o = runPure(collectRead(stream));
|
|
271
|
+
assert(o.length === 1, 'expected collectRead to finish without issuing a command');
|
|
272
|
+
const [r] = o;
|
|
273
|
+
assertEq(r[0], 'error');
|
|
274
|
+
assertEq(r[1], 'boom');
|
|
254
275
|
},
|
|
255
276
|
// A single `Vec` cannot exceed `maxLength` bits — feed a pure stream whose second
|
|
256
277
|
// chunk pushes the running total just over the limit so the overflow guard fires
|
|
@@ -260,22 +281,21 @@ export const proof = {
|
|
|
260
281
|
const v1 = vec(half)(0n);
|
|
261
282
|
const v2 = vec(half + 1n)(0n);
|
|
262
283
|
const stream = nonEmpty(ok(v1), nonEmpty(ok(v2), empty()));
|
|
263
|
-
const
|
|
264
|
-
assert(
|
|
265
|
-
assertEq(
|
|
284
|
+
const o = runPure(collectRead(stream));
|
|
285
|
+
assert(o.length === 1, 'expected collectRead to finish without issuing a command');
|
|
286
|
+
assertEq(o[0][0], 'error');
|
|
266
287
|
},
|
|
267
288
|
casListPropagatesNonNotFoundAccessError: () => {
|
|
268
289
|
// A non-ENOENT `access` failure (permissions, corruption) is a genuine storage
|
|
269
290
|
// error and must propagate out of `list`, not be swallowed as an empty store.
|
|
270
291
|
const c = fileCas(sha256)('.');
|
|
271
292
|
const boom = { code: 'EACCES' };
|
|
272
|
-
const
|
|
273
|
-
assert(
|
|
274
|
-
assertEq(
|
|
275
|
-
const continuation = d.continuation;
|
|
293
|
+
const r = casCommand(c.list());
|
|
294
|
+
assert(r[0] === 'cont', 'expected list() to issue an access command first');
|
|
295
|
+
assertEq(r[1], 'access');
|
|
276
296
|
let threw;
|
|
277
297
|
try {
|
|
278
|
-
|
|
298
|
+
r[2](error(boom));
|
|
279
299
|
}
|
|
280
300
|
catch (e) {
|
|
281
301
|
threw = e;
|
package/fjs/ci/bun/module.f.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { bun } from '../config/module.f.js';
|
|
8
|
-
import {
|
|
9
|
-
export const bunSteps = (version) =>
|
|
8
|
+
import { install, test, uses } from '../common/module.f.js';
|
|
9
|
+
export const bunSteps = (version) => [
|
|
10
10
|
install(uses('oven-sh/setup-bun', { 'bun-version': bun })),
|
|
11
11
|
install({ run: `bun install -g functionalscript@${version}` }),
|
|
12
12
|
test({ run: 'bun install --frozen-lockfile' }),
|
|
13
13
|
test({ run: `bunx functionalscript@${version} t` }),
|
|
14
14
|
test({ run: 'bun test --coverage' }),
|
|
15
|
-
]
|
|
15
|
+
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared CI types and helpers: GitHub Actions step/job RTTI schemas, the
|
|
3
3
|
* `MetaStep` representation used by tool-specific modules, and assemblers like
|
|
4
|
-
* `install`, `test`, `
|
|
4
|
+
* `install`, `test`, `ubuntu`, and `toSteps`.
|
|
5
5
|
*
|
|
6
6
|
* @module
|
|
7
7
|
*/
|
|
@@ -83,7 +83,6 @@ export type MetaStep = {
|
|
|
83
83
|
export declare const uses: (name: keyof typeof actions, w?: Record<string, string>) => Step;
|
|
84
84
|
export declare const install: (step: Step) => MetaStep;
|
|
85
85
|
export declare const test: (step: Step) => MetaStep;
|
|
86
|
-
export declare const clean: (steps: readonly MetaStep[]) => readonly MetaStep[];
|
|
87
86
|
export declare const toSteps: (m: readonly MetaStep[]) => readonly Step[];
|
|
88
87
|
export declare const ubuntu: (ms: readonly MetaStep[]) => Job;
|
|
89
88
|
export declare const ubuntuArm: (ms: readonly MetaStep[]) => Job;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared CI types and helpers: GitHub Actions step/job RTTI schemas, the
|
|
3
3
|
* `MetaStep` representation used by tool-specific modules, and assemblers like
|
|
4
|
-
* `install`, `test`, `
|
|
4
|
+
* `install`, `test`, `ubuntu`, and `toSteps`.
|
|
5
5
|
*
|
|
6
6
|
* @module
|
|
7
7
|
*/
|
|
@@ -37,10 +37,6 @@ export const uses = (name, w) => ({
|
|
|
37
37
|
});
|
|
38
38
|
export const install = (step) => ({ type: 'install', step });
|
|
39
39
|
export const test = (step) => ({ type: 'test', step });
|
|
40
|
-
export const clean = (steps) => [
|
|
41
|
-
...steps,
|
|
42
|
-
test({ run: 'git reset --hard HEAD && git clean -fdx' })
|
|
43
|
-
];
|
|
44
40
|
export const toSteps = (m) => {
|
|
45
41
|
const filter = (st) => m.flatMap((mt) => mt.type === st ? [mt.step] : []);
|
|
46
42
|
const aptGet = m.flatMap(v => v.type === 'apt-get' ? [v.package] : []).join(' ');
|
|
@@ -19,19 +19,19 @@ export declare const images: {
|
|
|
19
19
|
readonly arm: 'windows-11-arm';
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
-
export declare const functionalscript: '0.
|
|
22
|
+
export declare const functionalscript: '0.38.0';
|
|
23
23
|
export declare const bun = "1.3.14";
|
|
24
|
-
export declare const deno = "2.9.
|
|
25
|
-
export declare const playwright = "1.
|
|
24
|
+
export declare const deno = "2.9.4";
|
|
25
|
+
export declare const playwright = "1.62.0";
|
|
26
26
|
export declare const node: {
|
|
27
27
|
readonly default: '26.5.0';
|
|
28
28
|
readonly node22: '22.23.1';
|
|
29
29
|
readonly node24: '24.18.0';
|
|
30
30
|
};
|
|
31
31
|
export declare const wasmtime = "47.0.2";
|
|
32
|
-
export declare const wasmer = "7.2.
|
|
32
|
+
export declare const wasmer = "7.2.1";
|
|
33
33
|
export declare const actions: {
|
|
34
|
-
readonly 'actions/checkout': 'v7';
|
|
34
|
+
readonly 'actions/checkout': 'v7.0.1';
|
|
35
35
|
readonly 'actions/setup-node': 'v7.0.0';
|
|
36
36
|
readonly 'actions/cache': 'v6.1.0';
|
|
37
37
|
readonly 'denoland/setup-deno': 'v2.0.5';
|
|
@@ -24,13 +24,13 @@ export const images = {
|
|
|
24
24
|
// published FunctionalScript release; do not tie it to package.json's current
|
|
25
25
|
// in-repo version.
|
|
26
26
|
// https://www.npmjs.com/package/functionalscript
|
|
27
|
-
export const functionalscript = '0.
|
|
27
|
+
export const functionalscript = '0.38.0';
|
|
28
28
|
// https://bun.sh/
|
|
29
29
|
export const bun = '1.3.14';
|
|
30
30
|
// https://deno.com/
|
|
31
|
-
export const deno = '2.9.
|
|
31
|
+
export const deno = '2.9.4';
|
|
32
32
|
// https://www.npmjs.com/package/playwright
|
|
33
|
-
export const playwright = '1.
|
|
33
|
+
export const playwright = '1.62.0';
|
|
34
34
|
// https://nodejs.org/en/download
|
|
35
35
|
export const node = {
|
|
36
36
|
default: '26.5.0',
|
|
@@ -40,14 +40,14 @@ export const node = {
|
|
|
40
40
|
// https://github.com/bytecodealliance/wasmtime/releases
|
|
41
41
|
export const wasmtime = '47.0.2';
|
|
42
42
|
// https://github.com/wasmerio/wasmer/releases
|
|
43
|
-
export const wasmer = '7.2.
|
|
43
|
+
export const wasmer = '7.2.1';
|
|
44
44
|
// GitHub Action versions used by CI step builders. The key is the action
|
|
45
45
|
// `owner/name`; call sites compose the full ref as
|
|
46
46
|
// `` `${name}@${actions[name]}` ``.
|
|
47
47
|
// Note: dtolnay/rust-toolchain value is a Rust version, not an action version.
|
|
48
48
|
export const actions = {
|
|
49
49
|
// https://github.com/marketplace/actions/checkout
|
|
50
|
-
'actions/checkout': 'v7',
|
|
50
|
+
'actions/checkout': 'v7.0.1',
|
|
51
51
|
// https://github.com/marketplace/actions/setup-node-js-environment
|
|
52
52
|
'actions/setup-node': 'v7.0.0',
|
|
53
53
|
// https://github.com/marketplace/actions/cache
|
package/fjs/ci/deno/module.f.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { deno } from '../config/module.f.js';
|
|
8
|
-
import {
|
|
8
|
+
import { install, test, uses } from '../common/module.f.js';
|
|
9
9
|
const denoTest = 'deno test --allow-read --allow-env --allow-sys';
|
|
10
|
-
export const denoSteps = (version) =>
|
|
10
|
+
export const denoSteps = (version) => [
|
|
11
11
|
install(uses('denoland/setup-deno', { 'deno-version': deno })),
|
|
12
12
|
// We need --minimum-dependency-age=0 for functionalscript because we would like to use
|
|
13
13
|
// the latest version of the package even if it is not yet 24 hours old,
|
|
@@ -17,4 +17,4 @@ export const denoSteps = (version) => clean([
|
|
|
17
17
|
test({ run: `deno run -A --minimum-dependency-age=0 npm:functionalscript@${version} t` }),
|
|
18
18
|
test({ run: 'deno install --frozen' }),
|
|
19
19
|
test({ run: `${denoTest} --coverage && deno coverage --include='.*module\\.f\\.ts'` }),
|
|
20
|
-
]
|
|
20
|
+
];
|
package/fjs/ci/module.f.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import { pure } from '../effects/module.f.js';
|
|
6
|
+
import { pure, step } from '../effects/module.f.js';
|
|
7
7
|
import { access, writeUtf8File } from '../effects/node/module.f.js';
|
|
8
8
|
import { functionalscript, images } from './config/module.f.js';
|
|
9
9
|
import { architecture, os, toSteps, ubuntuArm } from './common/module.f.js';
|
|
@@ -29,7 +29,7 @@ const canonicalJobs = (rust) => ({
|
|
|
29
29
|
...nodeVersionJobs(functionalscript),
|
|
30
30
|
playwright: playwrightJob,
|
|
31
31
|
});
|
|
32
|
-
export const ci = ({ nodeExtra }) => access('Cargo.toml')
|
|
32
|
+
export const ci = ({ nodeExtra }) => step(access('Cargo.toml'), result => {
|
|
33
33
|
const rust = result[0] === 'ok';
|
|
34
34
|
const jobs = {
|
|
35
35
|
...Object.fromEntries(os.flatMap(o => architecture.map(job(rust, nodeExtra(o))(o)))),
|
|
@@ -46,7 +46,6 @@ export const ci = ({ nodeExtra }) => access('Cargo.toml').step(result => {
|
|
|
46
46
|
},
|
|
47
47
|
jobs,
|
|
48
48
|
};
|
|
49
|
-
return writeUtf8File('.github/workflows/ci.yml', JSON.stringify(gha, null, ' '))
|
|
50
|
-
.step(() => pure(0));
|
|
49
|
+
return step(writeUtf8File('.github/workflows/ci.yml', JSON.stringify(gha, null, ' ')), () => pure(0));
|
|
51
50
|
});
|
|
52
51
|
export const main = () => ci({ nodeExtra: () => [] });
|