@raresportan/marram 0.2.0 → 0.2.2
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/package.json +1 -1
- package/src/core/config.mjs +1 -0
- package/src/core/gate.mjs +21 -4
- package/src/react/keyed.ts +17 -11
package/package.json
CHANGED
package/src/core/config.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// gate: {
|
|
5
5
|
// logs: "where each run's logs go",
|
|
6
6
|
// progress: /lines from a stage worth showing as it runs/, // besides ✓ and ✗ lines
|
|
7
|
+
// sleepGapMs: 30000, // how long a pause counts as the machine sleeping, so a stage that failed runs again
|
|
7
8
|
// tiers: { fast: [stage, …], default: [stage, …], full: [stage, …] }, // stage: { name, run: [command, …args], env? }
|
|
8
9
|
// },
|
|
9
10
|
// review: { classes: "the mistake-classes doc", rungs: ["type", "lint", …] },
|
package/src/core/gate.mjs
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// full The default gate and more (a build, the slow journeys). Before merging UI work, or nightly.
|
|
6
6
|
// What each tier runs is the project's: `gate.tiers` in marram.config.mjs. Stages run in order and the first failure
|
|
7
7
|
// stops the gate. Each stage's output is kept in its own log under `gate.logs`/gate-<tier>-<time>/, with report.json.
|
|
8
|
+
// A stage that fails after the machine slept during it runs again: timeouts ran out and frames stopped while it slept,
|
|
9
|
+
// so that failure says nothing about the code. A pause counts as sleep past `gate.sleepGapMs`, 30 seconds by default.
|
|
8
10
|
// The default and full gates record their verdict for the working tree (gate-state.mjs), which the hooks reuse.
|
|
9
11
|
import { spawn } from "node:child_process";
|
|
10
12
|
import { createWriteStream } from "node:fs";
|
|
11
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
12
14
|
import { join } from "node:path";
|
|
13
15
|
import { treeOf, updateState } from "./gate-state.mjs";
|
|
14
16
|
|
|
@@ -19,7 +21,7 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
19
21
|
console.error(`Unknown gate "${tier}". Usage: marram gate [fast | full]`);
|
|
20
22
|
process.exit(1);
|
|
21
23
|
}
|
|
22
|
-
const { logs, progress, tiers } = config.gate;
|
|
24
|
+
const { logs, progress, tiers, sleepGapMs = 30_000 } = config.gate;
|
|
23
25
|
const dir = join(root, logs, `gate-${tier}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
24
26
|
await mkdir(dir, { recursive: true });
|
|
25
27
|
const report = { tier, startedAt: new Date().toISOString(), stages: [], passed: false };
|
|
@@ -50,16 +52,26 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
50
52
|
child.stdout.on("data", (chunk) => {
|
|
51
53
|
for (const line of chunk.toString().split("\n")) if (/^\s*[✓✗]/.test(line) || progress?.test(line)) console.log(` ${name}: ${line.trim()}`);
|
|
52
54
|
});
|
|
55
|
+
let last = Date.now();
|
|
56
|
+
let slept = 0;
|
|
57
|
+
const noticeSleep = () => {
|
|
58
|
+
const gap = Date.now() - last;
|
|
59
|
+
last += gap;
|
|
60
|
+
if (gap > sleepGapMs) slept += gap;
|
|
61
|
+
};
|
|
62
|
+
const clock = setInterval(noticeSleep, 1000);
|
|
53
63
|
const code = await new Promise((done) => {
|
|
54
64
|
child.on("error", () => done(1));
|
|
55
65
|
child.on("exit", (exit) => done(exit ?? 1));
|
|
56
66
|
});
|
|
67
|
+
clearInterval(clock);
|
|
68
|
+
noticeSleep();
|
|
57
69
|
log.end();
|
|
58
70
|
const seconds = Math.round((Date.now() - started) / 1000);
|
|
59
71
|
report.stages.push({ name, code, seconds });
|
|
60
72
|
await writeFile(join(dir, "report.json"), JSON.stringify(report, null, 2));
|
|
61
73
|
console.log(`${code === 0 ? "✓" : "✗"} ${name} (${seconds}s)${code === 0 ? "" : `: see ${join(dir, `${name}.log`)}`}`);
|
|
62
|
-
return code === 0;
|
|
74
|
+
return { passed: code === 0, slept };
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
// A fast pass doesn't make a change done, so only the default and full gates record a verdict.
|
|
@@ -70,7 +82,12 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
70
82
|
|
|
71
83
|
console.log(`Gate (${tier}). Logs: ${dir}`);
|
|
72
84
|
for (const stage of tiers[tier]) {
|
|
73
|
-
|
|
85
|
+
let { passed, slept } = await runStage(stage);
|
|
86
|
+
while (!passed && slept > 0 && !interrupted) {
|
|
87
|
+
console.log(` The machine slept for ${Math.round(slept / 1000)}s during ${stage.name}, so its failure says nothing about the code. Running it again.`);
|
|
88
|
+
await rename(join(dir, `${stage.name}.log`), join(dir, `${stage.name}-slept.log`));
|
|
89
|
+
({ passed, slept } = await runStage(stage));
|
|
90
|
+
}
|
|
74
91
|
if (interrupted) {
|
|
75
92
|
console.log(`\nGate interrupted at ${stage.name}.`);
|
|
76
93
|
process.exit(1);
|
package/src/react/keyed.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useReducer } from "react";
|
|
2
|
+
|
|
3
|
+
type Value<T> = T | ((previous: T) => T);
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* State that belongs to a key (a watch's parameters, a file, a document): under another key it reads as `initial`, so
|
|
5
7
|
* nothing shows the last key's value while the new key's loads, not even for the one render an effect resetting it
|
|
6
8
|
* would leave. Set it with the key the value is for, or with an update of that key's value; a late answer for a key
|
|
7
|
-
* that's gone
|
|
9
|
+
* that's gone is dropped, so it can't take the place of the current key's value. A key says only what a value is for:
|
|
10
|
+
* two requests under one key can still answer out of order, so a caller that re-reads puts a version or try in the key.
|
|
8
11
|
*/
|
|
9
|
-
export function useKeyed<T>(key: string, initial: T): [T, (forKey: string, value: T
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
export function useKeyed<T>(key: string, initial: T): [T, (forKey: string, value: Value<T>) => void] {
|
|
13
|
+
// Written inline because React runs a reducer in the render an update lands in: this one sees that render's key, which
|
|
14
|
+
// `set`, called from a promise or a listener, can't know.
|
|
15
|
+
const [held, apply] = useReducer(
|
|
16
|
+
(last: { key: string; value: T }, { forKey, value }: { forKey: string; value: Value<T> }) => {
|
|
17
|
+
if (forKey !== key) return last;
|
|
18
|
+
const previous = last.key === key ? last.value : initial;
|
|
19
|
+
return { key, value: typeof value === "function" ? (value as (previous: T) => T)(previous) : value };
|
|
20
|
+
},
|
|
21
|
+
{ key, value: initial },
|
|
22
|
+
);
|
|
23
|
+
const set = useCallback((forKey: string, value: Value<T>) => apply({ forKey, value }), []);
|
|
18
24
|
return [held.key === key ? held.value : initial, set];
|
|
19
25
|
}
|