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.
Files changed (75) hide show
  1. package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
  2. package/fjs/bnf/token_symbol/module.f.js +48 -0
  3. package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
  4. package/fjs/bnf/token_symbol/proof.f.js +37 -0
  5. package/fjs/cas/cli/module.f.js +6 -9
  6. package/fjs/cas/evo/mcp/module.f.d.ts +9 -1
  7. package/fjs/cas/evo/mcp/module.f.js +52 -14
  8. package/fjs/cas/evo/mcp/proof.f.d.ts +2 -0
  9. package/fjs/cas/evo/mcp/proof.f.js +33 -2
  10. package/fjs/cas/evo/module.f.d.ts +76 -12
  11. package/fjs/cas/evo/module.f.js +129 -17
  12. package/fjs/cas/evo/proof.f.d.ts +7 -0
  13. package/fjs/cas/evo/proof.f.js +119 -0
  14. package/fjs/cas/mcp/module.f.js +22 -20
  15. package/fjs/cas/mcp/proof.f.d.ts +1 -1
  16. package/fjs/cas/mcp/proof.f.js +9 -9
  17. package/fjs/cas/module.f.js +64 -37
  18. package/fjs/cas/proof.f.d.ts +3 -9
  19. package/fjs/cas/proof.f.js +94 -74
  20. package/fjs/ci/bun/module.f.js +3 -3
  21. package/fjs/ci/common/module.f.d.ts +1 -2
  22. package/fjs/ci/common/module.f.js +1 -5
  23. package/fjs/ci/config/module.f.d.ts +5 -5
  24. package/fjs/ci/config/module.f.js +5 -5
  25. package/fjs/ci/deno/module.f.js +3 -3
  26. package/fjs/ci/module.f.js +3 -4
  27. package/fjs/ci/node/module.f.js +11 -9
  28. package/fjs/ci/proof.f.js +2 -0
  29. package/fjs/cli/module.f.js +2 -2
  30. package/fjs/common/monoid/module.f.d.ts +46 -2
  31. package/fjs/common/monoid/module.f.js +44 -0
  32. package/fjs/common/monoid/proof.f.d.ts +5 -0
  33. package/fjs/common/monoid/proof.f.js +27 -1
  34. package/fjs/dev/module.f.js +25 -25
  35. package/fjs/djs/module.f.js +5 -8
  36. package/fjs/djs/parser/module.f.d.ts +3 -0
  37. package/fjs/djs/parser/module.f.js +17 -0
  38. package/fjs/djs/transpiler/module.f.js +6 -6
  39. package/fjs/effects/eff/module.f.d.ts +72 -0
  40. package/fjs/effects/eff/module.f.js +46 -0
  41. package/fjs/effects/eff/proof.f.d.ts +11 -0
  42. package/fjs/effects/eff/proof.f.js +58 -0
  43. package/fjs/effects/list/module.f.d.ts +11 -5
  44. package/fjs/effects/list/module.f.js +2 -1
  45. package/fjs/effects/memory/proof.f.js +7 -8
  46. package/fjs/effects/module.f.d.ts +334 -52
  47. package/fjs/effects/module.f.js +253 -45
  48. package/fjs/effects/node/memory/proof.js +3 -2
  49. package/fjs/effects/node/module.f.d.ts +1 -1
  50. package/fjs/effects/node/module.f.js +9 -11
  51. package/fjs/effects/node/proof.f.js +18 -10
  52. package/fjs/effects/proof.f.d.ts +32 -6
  53. package/fjs/effects/proof.f.js +131 -39
  54. package/fjs/emergent_testing/module.f.js +55 -39
  55. package/fjs/emergent_testing/proof.f.d.ts +4 -0
  56. package/fjs/emergent_testing/proof.f.js +53 -20
  57. package/fjs/mcp/module.f.js +8 -8
  58. package/fjs/mcp/proof.f.js +5 -4
  59. package/fjs/mcp/stdio/module.f.d.ts +2 -2
  60. package/fjs/mcp/stdio/module.f.js +12 -12
  61. package/fjs/media/type/module.f.js +2 -2
  62. package/fjs/media/type/proof.f.js +7 -10
  63. package/fjs/module.f.js +4 -8
  64. package/fjs/types/bigint/module.f.js +4 -3
  65. package/fjs/types/bit_vec/module.f.js +5 -15
  66. package/fjs/types/btree/remove/module.f.d.ts +5 -0
  67. package/fjs/types/btree/remove/module.f.js +12 -0
  68. package/fjs/types/list/module.f.d.ts +21 -0
  69. package/fjs/types/list/module.f.js +17 -0
  70. package/fjs/types/list/proof.f.d.ts +1 -0
  71. package/fjs/types/list/proof.f.js +24 -1
  72. package/fjs/types/number/module.f.js +2 -1
  73. package/fjs/types/string/module.f.js +10 -2
  74. package/fjs/website/module.f.js +2 -3
  75. package/package.json +3 -3
@@ -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.step((node) => {
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().step(t => readdir(stageDir, {}).step(([k, v]) => {
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((name) => rm(join(stageDir, name)).step(() => pure(undefined)))(expired);
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).step(() => pure(error(e)));
136
- return gcStage(stageDir).step(() => random256.step(rnd => {
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
- return now()
157
- .step(t => {
158
- const next = join(stageDir, stageName(t + leaseDelta, rndStr));
159
- return rename(curPath, next).step(([t, v]) => t === 'error'
160
- ? fail(curPath, v)
161
- : loop(newState, newOffset, next)(tail));
162
- });
163
- });
164
- });
165
- return mkdir(stageDir, { recursive: true })
166
- .step(() => now()
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).step(a => {
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((_) => (acc) => randomInt().step(r => pure(msb.concat(acc)(vec(32n)(BigInt(r))))))(empty)([0, 1, 2, 3, 4, 5, 6, 7]);
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).step((result) => {
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).step(okStep(v => rm(src).step(() => pure(ok(v)))));
250
+ return eff(casAddFile(c)(src))
251
+ .step(okStep(v => eff(rm(src))
252
+ .step(() => pure(ok(v)))
253
+ .value))
254
+ .value;
228
255
  };
@@ -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: typeof testAddBigFile;
9
- addAndGetBigFile: typeof testAddAndGetBigFile;
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 {};
@@ -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 { decode, pure } from '../effects/module.f.js';
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 = () => mkdir(testDir, { recursive: true }).step(() => {
22
- const bigContent = createBigFileContent();
38
+ const testAddBigFile = () => {
23
39
  const bigFilePath = `${testDir}/big-file.bin`;
24
- return writeFile(bigFilePath, bigContent).step(writeRes => {
25
- if (writeRes[0] === 'error') {
26
- throw new Error(`Failed to write test file: ${writeRes[1]}`);
27
- }
28
- const cas = fileCas(sha256)(testDir);
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 = () => mkdir(testDir, { recursive: true })
47
- .step(() => {
58
+ const testAddAndGetBigFile = () => {
48
59
  const bigContent = createBigFileContent();
49
60
  const bigFilePath = `${testDir}/big-file.bin`;
50
- return writeFile(bigFilePath, bigContent)
51
- .step(writeRes => {
52
- if (writeRes[0] === 'error') {
53
- throw new Error(`Failed to write test file: ${writeRes[1]}`);
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
- addBigFile: testAddBigFile,
82
- addAndGetBigFile: testAddAndGetBigFile,
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) => stream.step((node) => {
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) => stream.step((node) => {
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) => stream.step((node) => {
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 d = decode(collectRead(stream));
243
- assert(d.done, 'expected collectRead to finish without issuing a command');
244
- assertEq(d.result[0], 'ok');
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 d = decode(collectRead(stream));
251
- assert(d.done, 'expected collectRead to finish without issuing a command');
252
- assertEq(d.result[0], 'error');
253
- assertEq(d.result[1], 'boom');
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 d = decode(collectRead(stream));
264
- assert(d.done, 'expected collectRead to finish without issuing a command');
265
- assertEq(d.result[0], 'error');
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 d = decode(c.list());
273
- assert(!d.done, 'expected list() to issue an access command first');
274
- assertEq(d.command, 'access');
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
- continuation(error(boom));
298
+ r[2](error(boom));
279
299
  }
280
300
  catch (e) {
281
301
  threw = e;
@@ -5,11 +5,11 @@
5
5
  * @module
6
6
  */
7
7
  import { bun } from '../config/module.f.js';
8
- import { clean, install, test, uses } from '../common/module.f.js';
9
- export const bunSteps = (version) => clean([
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`, `clean`, `ubuntu`, and `toSteps`.
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`, `clean`, `ubuntu`, and `toSteps`.
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.37.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.3";
25
- export declare const playwright = "1.61.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.0";
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.37.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.3';
31
+ export const deno = '2.9.4';
32
32
  // https://www.npmjs.com/package/playwright
33
- export const playwright = '1.61.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.0';
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
@@ -5,9 +5,9 @@
5
5
  * @module
6
6
  */
7
7
  import { deno } from '../config/module.f.js';
8
- import { clean, install, test, uses } from '../common/module.f.js';
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) => clean([
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
+ ];
@@ -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').step(result => {
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: () => [] });