@michaelrwalker/stagehand 0.3.0 → 0.5.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 +280 -2
- package/dist/cache.d.ts +65 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +251 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -2
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/script.d.ts +191 -7
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +245 -16
- package/dist/script.js.map +1 -1
- package/dist/state.d.ts +6 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +18 -1
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +160 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/frame.d.ts.map +1 -1
- package/dist/ui/frame.js +28 -7
- package/dist/ui/frame.js.map +1 -1
- package/dist/ui/renderer.d.ts.map +1 -1
- package/dist/ui/renderer.js +9 -6
- package/dist/ui/renderer.js.map +1 -1
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +2 -0
- package/dist/ui/theme.js.map +1 -1
- package/package.json +6 -9
package/README.md
CHANGED
|
@@ -303,6 +303,139 @@ per placement.
|
|
|
303
303
|
`when` returning false marks the step **skipped** — its rollback never runs.
|
|
304
304
|
Phases accept `when` too, which skips all their steps.
|
|
305
305
|
|
|
306
|
+
## Caching
|
|
307
|
+
|
|
308
|
+
A phase or a step can reuse what it produced on an earlier run. Point it at a
|
|
309
|
+
store and it stops repeating work:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import { Script, fileStore } from "@michaelrwalker/stagehand";
|
|
313
|
+
|
|
314
|
+
const cache = fileStore("./.stagehand-cache.json");
|
|
315
|
+
|
|
316
|
+
new Script<Input>({ name: "deploy" })
|
|
317
|
+
.addPhase("Build", { cache })
|
|
318
|
+
.addStep({ name: "install", handler: … })
|
|
319
|
+
.addStep({ name: "compile", handler: … })
|
|
320
|
+
|
|
321
|
+
.addPhase("Release")
|
|
322
|
+
.addStep({ name: "warm CDN", cache, handler: … })
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
On a hit the work is skipped and the stored value is merged into the context —
|
|
326
|
+
downstream steps see the same keys, at the same types, either way.
|
|
327
|
+
|
|
328
|
+
**What gets stored.** A phase stores its **delta**: the keys its steps
|
|
329
|
+
contributed, minus anything they `clean`ed. Keys from earlier phases are never
|
|
330
|
+
part of it, so a hit can't overwrite a value this run just computed. A step
|
|
331
|
+
stores exactly what its handler returned.
|
|
332
|
+
|
|
333
|
+
**There is no key.** The store is the identity. Say when an entry stops being
|
|
334
|
+
good with `stale`, or delete the file:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
.addPhase("Build", {
|
|
338
|
+
cache: {
|
|
339
|
+
store: cache,
|
|
340
|
+
stale: ({ value, ctx, ageMs }) => ageMs > 3_600_000 || value.sha !== ctx.sha,
|
|
341
|
+
schema: BuildSchema, // optional; a value that no longer fits is a miss
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`stale` and `schema` both see the context as it stands when the phase is
|
|
347
|
+
reached, so `ctx` is typed with no annotation. `value` arrives as `unknown` —
|
|
348
|
+
annotate the parameter, or hand over a `schema` and let it narrow. A stored
|
|
349
|
+
value that fails its schema is treated as a **miss**, not an error, which is
|
|
350
|
+
what stops a cache written by an older version of the script from feeding the
|
|
351
|
+
wrong shape into the context.
|
|
352
|
+
|
|
353
|
+
**Rollback.** Cached work never ran, so its `rollback` can't fire during a
|
|
354
|
+
later unwind — the side effect belongs to the run that wrote the entry and was
|
|
355
|
+
never undone. For the same reason a failure downstream leaves entries alone.
|
|
356
|
+
|
|
357
|
+
**Per run**, without touching the script:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
await deploy.run(input, { cache: argv.includes("--no-cache") ? "off" : "on" });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`"on"` (default) reads and writes · `"off"` ignores caching entirely ·
|
|
364
|
+
`"refresh"` runs everything and overwrites · `"read-only"` uses hits but never
|
|
365
|
+
writes.
|
|
366
|
+
|
|
367
|
+
### Reaching the cache from a step
|
|
368
|
+
|
|
369
|
+
Every handler and rollback gets a `cache` handle, typed to the cached phases
|
|
370
|
+
declared **before** it:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
.addPhase("Fetch", { cache: { store } })
|
|
374
|
+
.addStep({ name: "pull orders", handler: … }) // → { orders }
|
|
375
|
+
|
|
376
|
+
.addPhase("Adjust")
|
|
377
|
+
.addStep({
|
|
378
|
+
name: "restock",
|
|
379
|
+
handler: async ({ ctx, cache }) => {
|
|
380
|
+
await cache.clear("Fetch"); // next run refetches
|
|
381
|
+
await cache.write("Fetch", { orders: ctx.orders }, { keepAge: true });
|
|
382
|
+
const age = await cache.ageOf("Fetch");
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Slot names are checked, not stringly-typed. An uncached phase isn't a slot at
|
|
388
|
+
all, and `as` renames a mounted routine's slots along with its phases — so the
|
|
389
|
+
moment you write `.use(pullChannel, { as: "Amazon" })`, every
|
|
390
|
+
`cache.clear("Fetch")` in the script goes red and offers `"Amazon / Fetch"`
|
|
391
|
+
instead. That's the failure a hand-written string can't catch: it would keep
|
|
392
|
+
compiling and silently address nothing.
|
|
393
|
+
|
|
394
|
+
Invalidation flows backwards only — a later step can drop an earlier phase's
|
|
395
|
+
entry, not the reverse.
|
|
396
|
+
|
|
397
|
+
`write` restamps the entry as written *now* unless you pass `keepAge`. Reach
|
|
398
|
+
for it when you are correcting a value rather than refreshing it: restamping
|
|
399
|
+
silently buys another full TTL for data that is exactly as old as it was a
|
|
400
|
+
moment ago, which is the opposite of what you want when the cache is there to
|
|
401
|
+
keep you inside a rate limit.
|
|
402
|
+
|
|
403
|
+
**Values are typed too.** A slot with a `schema` reads back as the schema's
|
|
404
|
+
output, because that is the one thing actually *checked* against the stored
|
|
405
|
+
JSON. Without a schema the phase's own delta stands in, which is convenient and
|
|
406
|
+
true right up until an entry outlives the code that wrote it. For that case
|
|
407
|
+
`read` returns the value behind a guard:
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
CacheShapeError: Read "orders.0.date_fixed" from cache slot "Fetch", but the
|
|
411
|
+
stored value has no such property. The entry was probably written before this
|
|
412
|
+
script's shape changed — drop it with cache.clear("Fetch"), or declare a schema
|
|
413
|
+
on the slot to have mismatches rejected on read.
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Only reads are trapped; assigning a new property is how you'd patch an entry.
|
|
417
|
+
Missing array indices, `JSON.stringify`, `await` and iteration all behave
|
|
418
|
+
normally. Pass `{ raw: true }` to opt out — do that before putting the value
|
|
419
|
+
into the context, or the guard travels with it into later steps.
|
|
420
|
+
|
|
421
|
+
**Stores** are three methods, so Redis or S3 is a dozen lines:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
interface CacheStore {
|
|
425
|
+
read(slot: string): Awaitable<CachedEntry | undefined>;
|
|
426
|
+
write(slot: string, entry: CachedEntry): Awaitable<void>;
|
|
427
|
+
clear(slot: string): Awaitable<void>;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`slot` is derived from the phase or step name so several phases can share one
|
|
432
|
+
store; scripts never write it. `fileStore(path)` keeps every slot in one JSON
|
|
433
|
+
file — a missing, unreadable or corrupt file all read as no cache at all, so
|
|
434
|
+
deleting it is always a valid reset. `memoryStore()` lasts for the process.
|
|
435
|
+
|
|
436
|
+
A store that throws — unreadable file, full disk, a `stale` predicate with a bug
|
|
437
|
+
— logs a warning and does the work. Caching never decides whether a run passes.
|
|
438
|
+
|
|
306
439
|
## Script options
|
|
307
440
|
|
|
308
441
|
```ts
|
|
@@ -350,9 +483,119 @@ script has actually produced `Ctx` by that point. Dropping a step that needs
|
|
|
350
483
|
`{ user: User }` into a script that has not loaded a user yet is a compile
|
|
351
484
|
error, not a runtime `undefined`.
|
|
352
485
|
|
|
486
|
+
## Reusable scripts
|
|
487
|
+
|
|
488
|
+
`routineFor` does for whole phases what `stepFor` does for one step. Declare a
|
|
489
|
+
fragment once, mount it wherever it fits:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// routines/channel.ts
|
|
493
|
+
export const pullChannel = routineFor<{ channel: string; since: string }>()(
|
|
494
|
+
"pull channel",
|
|
495
|
+
(script) =>
|
|
496
|
+
script
|
|
497
|
+
.addPhase("Fetch", { cache: { store: cache, stale: … } })
|
|
498
|
+
.addStep({ name: "authenticate", handler: … })
|
|
499
|
+
.addStep({ name: "pull orders", handler: … })
|
|
500
|
+
.addPhase("Normalize")
|
|
501
|
+
.addStep({ name: "dedupe and total", handler: … }),
|
|
502
|
+
);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
new Script<Input>({ name: "channel sync" })
|
|
507
|
+
.use(pullChannel)
|
|
508
|
+
.addPhase("Validation")
|
|
509
|
+
.addStep({ name: "check totals", handler: ({ ctx }) => … }) // ctx.orderCount, ctx.gross
|
|
510
|
+
|
|
511
|
+
// months later, a different job entirely
|
|
512
|
+
new Script<ReportInput>({ name: "monthly report" })
|
|
513
|
+
.use(pullChannel)
|
|
514
|
+
.addPhase("Report")
|
|
515
|
+
.addStep({ name: "build summary", handler: ({ ctx }) => … })
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The two parameters on `routineFor<In, Ctx>()` are the **minimum** the fragment
|
|
519
|
+
needs — the input it reads, and the context it expects to already exist. Both
|
|
520
|
+
are checked at the `use()` call:
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
Property 'hash' is missing in type '{}' but required in type '{ hash: string; }'.
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
A plain `Script` mounts too, which is what keeps a pipeline runnable on its
|
|
527
|
+
own — `pipeline.run(input)` today, `.use(pipeline)` tomorrow. Its own
|
|
528
|
+
`ScriptOptions` (rollback mode, `logPlacement`, `silent`) are ignored in favour
|
|
529
|
+
of the host's; only its phases and steps come across.
|
|
530
|
+
|
|
531
|
+
**Everything travels with the fragment.** Phase-level `when` and `cache`,
|
|
532
|
+
step-level `retry`, `clean`, `rollback` — a spliced rollback compensates during
|
|
533
|
+
the host's unwind, and keys it reserved through `rollbackKeys` stay reserved in
|
|
534
|
+
the host, so a later `clean` there still can't delete them.
|
|
535
|
+
|
|
536
|
+
That combines well with caching: mount a routine whose phase caches, and the
|
|
537
|
+
second script to mount it gets the data the first one pulled.
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
› Fetch
|
|
541
|
+
⊙ authenticate cached
|
|
542
|
+
⊙ pull orders cached
|
|
543
|
+
› Report
|
|
544
|
+
✔ build summary (avg 24.50) 263ms
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Giving a mount its own input
|
|
548
|
+
|
|
549
|
+
A mount can feed its fragment something other than the host's input — which is
|
|
550
|
+
what lets one routine serve two storefronts, each with its own credentials:
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
new Script<{ since: string; amazonKey: string; shopifyKey: string }>({ name: "all channels" })
|
|
554
|
+
.use(pullChannel, {
|
|
555
|
+
as: "Amazon",
|
|
556
|
+
input: ({ input }) => ({ channel: "amazon", since: input.since, apiKey: input.amazonKey }),
|
|
557
|
+
})
|
|
558
|
+
.use(pullChannel, {
|
|
559
|
+
as: "Shopify",
|
|
560
|
+
input: ({ input }) => ({ channel: "shopify", since: input.since, apiKey: input.shopifyKey }),
|
|
561
|
+
})
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
`input` takes a fixed value or a function of `{ input, ctx }`, and may be
|
|
565
|
+
async. It is resolved **once per mount**, when the mount is reached, and every
|
|
566
|
+
phase and step of that fragment sees the result — including its `when`, its
|
|
567
|
+
`cache`, and its rollbacks during an unwind.
|
|
568
|
+
|
|
569
|
+
With a mapper the host no longer has to match the routine's input shape at all;
|
|
570
|
+
it only has to produce it. TypeScript checks the return value:
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
Property 'apiKey' is missing in type '{ channel: string; }' but required in type 'ChannelInput'.
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Without one, the host's own input must satisfy the routine, as `stepFor` steps
|
|
577
|
+
require.
|
|
578
|
+
|
|
579
|
+
**Mounting twice** needs `as`, because phase names have to stay unique:
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
.use(pullChannel, { as: "Amazon" }) // → "Amazon / Fetch", "Amazon / Normalize"
|
|
583
|
+
.use(pullChannel, { as: "Shopify" })
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Without it you get a `DuplicateNameError` at build time. That rule is not
|
|
587
|
+
routine-specific — two phases anywhere in a script, or two steps within one
|
|
588
|
+
phase, are refused. Names label the frame, are what `outline()` reports, and
|
|
589
|
+
identify a cache entry; two units sharing a name share a slot, which surfaces
|
|
590
|
+
as wrong data rather than as a failure.
|
|
591
|
+
|
|
592
|
+
A routine owns whole phases, so `addStep` directly after `use()` is refused
|
|
593
|
+
too — open a phase of your own first, rather than quietly appending to a
|
|
594
|
+
fragment someone else's script also mounts.
|
|
595
|
+
|
|
353
596
|
## Examples
|
|
354
597
|
|
|
355
|
-
|
|
598
|
+
Ten runnable scripts, each aimed at a different part of the API. Most take a
|
|
356
599
|
flag to switch between the happy path and the interesting one.
|
|
357
600
|
|
|
358
601
|
| | |
|
|
@@ -363,6 +606,9 @@ flag to switch between the happy path and the interesting one.
|
|
|
363
606
|
| [`intake.ts`](examples/intake.ts) | `defineInput` with a hand-rolled Standard Schema, plus the full handler UI surface. `-- --bad` to watch validation reject the run, `-- --dirty` for a failing checklist item |
|
|
364
607
|
| [`modular.ts`](examples/modular.ts) | `stepFor` steps living in [`steps/tenancy.ts`](examples/steps/tenancy.ts), shared by two unrelated scripts — the imported rollback compensates in both. `-- --fail` |
|
|
365
608
|
| [`log-placement.ts`](examples/log-placement.ts) | The same script run three times, once per `logPlacement` value, so the difference is visible directly |
|
|
609
|
+
| [`cache.ts`](examples/cache.ts) | A cached `Build` phase and a cached step. Run it twice to watch the second run skip both. `-- --fresh` to rebuild and overwrite |
|
|
610
|
+
| [`invalidate.ts`](examples/invalidate.ts) | `context.cache` from inside a step: correct an entry without refetching, drop one that is beyond saving, and watch the shape guard catch an entry written before the code changed. `-- --patch`, `-- --drop`, `-- --drift` |
|
|
611
|
+
| [`routine.ts`](examples/routine.ts) | One reusable fragment in [`routines/channel.ts`](examples/routines/channel.ts), mounted by a sync script and, months later, a report that maps its own input onto the routine's — and gets the pull from cache. `-- --multi` mounts it twice, one storefront and API key per mount |
|
|
366
612
|
| [`validate.ts`](examples/validate.ts) | The smallest thing that runs: two steps, `rollback: "none"` |
|
|
367
613
|
|
|
368
614
|
```
|
|
@@ -370,6 +616,8 @@ npm run example:checkout -- --ok
|
|
|
370
616
|
npm run example:resilience -- --cancel
|
|
371
617
|
npm run example:intake -- --bad
|
|
372
618
|
npm run example:modular -- --fail
|
|
619
|
+
npm run example:invalidate -- --drift
|
|
620
|
+
npm run example:routine -- --multi
|
|
373
621
|
npm run example:log-placement
|
|
374
622
|
```
|
|
375
623
|
|
|
@@ -378,12 +626,42 @@ npm run example:log-placement
|
|
|
378
626
|
```
|
|
379
627
|
npm run build # emit dist/
|
|
380
628
|
npm test # node:test suite
|
|
381
|
-
npm run typecheck # library + examples + tests
|
|
629
|
+
npm run typecheck # library + examples + tests + scripts
|
|
630
|
+
npm run preflight # the release gate (add -- --allow-dirty while working)
|
|
382
631
|
npm run example # the happy path, live
|
|
383
632
|
npm run example:fail
|
|
384
633
|
npm run release # release-it: version, tag, publish
|
|
385
634
|
```
|
|
386
635
|
|
|
636
|
+
## Releasing
|
|
637
|
+
|
|
638
|
+
[`scripts/preflight.ts`](scripts/preflight.ts) is the gate, and it is written
|
|
639
|
+
with this library — so every release exercises phases, tasks, `note`, `clean`,
|
|
640
|
+
`when` and `retry` on real work:
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
› Quality
|
|
644
|
+
✔ typecheck 1.2s
|
|
645
|
+
✔ tests (120 passing in 9 files) 5.1s
|
|
646
|
+
› Package
|
|
647
|
+
✔ build 404ms
|
|
648
|
+
✔ entry point resolves 0ms
|
|
649
|
+
✔ tarball contents (47 files, 66kB) 180ms
|
|
650
|
+
› Registry
|
|
651
|
+
✔ version is unpublished 212ms
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
It checks the working tree is clean, typechecks all four projects separately so
|
|
655
|
+
a failure names the one that broke, runs the suite, then imports `dist/index.js`
|
|
656
|
+
the way a consumer would and asserts the public surface is actually there.
|
|
657
|
+
`npm pack --dry-run` has to contain the entry point, its declarations, LICENSE
|
|
658
|
+
and README — and must not leak `src/`, `test/`, `examples/` or `scripts/`.
|
|
659
|
+
Finally it asks npm whether the version is already published.
|
|
660
|
+
|
|
661
|
+
release-it runs it from `after:bump`, which is the first point at which
|
|
662
|
+
package.json holds the version being released. A failure aborts before anything
|
|
663
|
+
is committed, tagged or published.
|
|
664
|
+
|
|
387
665
|
## TypeScript layout
|
|
388
666
|
|
|
389
667
|
One config per directory, because editors resolve a file's config by finding
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CacheMode, CacheOptions, CacheSource, CacheStore } from "./types.js";
|
|
2
|
+
/** Lives for the lifetime of the process. Useful in tests, and for re-runs. */
|
|
3
|
+
export declare function memoryStore(): CacheStore;
|
|
4
|
+
/**
|
|
5
|
+
* One JSON file, holding every slot that points at it. Several phases may
|
|
6
|
+
* share a path safely, and concurrent writes to one path are serialised.
|
|
7
|
+
*
|
|
8
|
+
* A missing, unreadable or malformed file all mean the same thing — no cache
|
|
9
|
+
* yet — so deleting the file is always a valid way to start over. Values must
|
|
10
|
+
* survive `JSON.stringify`; anything that does not (a `Date`, a class
|
|
11
|
+
* instance) comes back as its JSON shape.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fileStore(filePath: string): CacheStore;
|
|
14
|
+
/** `cache: someStore` is shorthand for `cache: { store: someStore }`. */
|
|
15
|
+
export declare function normalizeCache(source: CacheSource | undefined): CacheOptions | undefined;
|
|
16
|
+
export type CacheLookup = {
|
|
17
|
+
hit: true;
|
|
18
|
+
value: unknown;
|
|
19
|
+
ageMs: number;
|
|
20
|
+
} | {
|
|
21
|
+
hit: false;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Read an entry and put it through every gate. A failure at any gate is a
|
|
25
|
+
* miss — the work runs and the entry is replaced — never an error.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readCache(cache: CacheOptions, slot: string, input: unknown, ctx: unknown, mode: CacheMode): Promise<CacheLookup>;
|
|
28
|
+
/** Only ever called after the phase or step has fully succeeded. */
|
|
29
|
+
export declare function writeCache(cache: CacheOptions, slot: string, value: unknown, mode: CacheMode): Promise<void>;
|
|
30
|
+
/** Identifies a phase's entry. Steps hang off their phase to avoid collisions. */
|
|
31
|
+
export declare const phaseSlot: (phase: string) => string;
|
|
32
|
+
export declare const stepSlot: (phase: string, step: string) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a value read back from a store so that touching a field it does not
|
|
35
|
+
* have throws {@link CacheShapeError} instead of yielding `undefined`.
|
|
36
|
+
*
|
|
37
|
+
* Only reads are trapped. Assigning a new property is how you'd legitimately
|
|
38
|
+
* patch an entry before writing it back, so it is left alone — the lie is
|
|
39
|
+
* always on the read side, where the type promised something the JSON lacks.
|
|
40
|
+
*
|
|
41
|
+
* Absent array indices pass through too: `orders[9]` on a short list is
|
|
42
|
+
* ordinary JavaScript, not a shape mismatch.
|
|
43
|
+
*/
|
|
44
|
+
export declare function guardShape<T>(value: T, slot: string, path?: string): T;
|
|
45
|
+
/** Which store backs each slot, and whether that slot validates on read. */
|
|
46
|
+
export interface SlotBinding {
|
|
47
|
+
store: CacheStore;
|
|
48
|
+
schema: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the `cache` handle a handler sees. Slots it does not know about are
|
|
52
|
+
* already unreachable through the types; at runtime they throw rather than
|
|
53
|
+
* silently doing nothing, which is what a plain `store.clear("typo")` does.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createCacheHandle(slots: Map<string, SlotBinding>): {
|
|
56
|
+
read(slot: string, options?: {
|
|
57
|
+
raw?: boolean;
|
|
58
|
+
}): Promise<unknown>;
|
|
59
|
+
write(slot: string, value: unknown, options?: {
|
|
60
|
+
keepAge?: boolean;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
clear(slot: string): Promise<void>;
|
|
63
|
+
ageOf(slot: string): Promise<number | undefined>;
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,EAEX,MAAM,YAAY,CAAC;AAMpB,+EAA+E;AAC/E,wBAAgB,WAAW,IAAI,UAAU,CAWxC;AA2BD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAoEtD;AAgBD,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAMxF;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,GAAG,EAAE,KAAK,CAAA;CAAE,CAAC;AAInB;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,SAAS,GACd,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED,oEAAoE;AACpE,wBAAsB,UAAU,CAC9B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,SAAS,GACd,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,kFAAkF;AAClF,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,KAAG,MAAe,CAAC;AAC1D,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,MAA6B,CAAC;AAqBrF;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,CAAC,CAelE;AAMD,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG;IAClE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAClD,CAoCA"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { CacheShapeError, StepDefinitionError } from "./errors.js";
|
|
4
|
+
/* -------------------------------------------------------------------------- */
|
|
5
|
+
/* Stores */
|
|
6
|
+
/* -------------------------------------------------------------------------- */
|
|
7
|
+
/** Lives for the lifetime of the process. Useful in tests, and for re-runs. */
|
|
8
|
+
export function memoryStore() {
|
|
9
|
+
const slots = new Map();
|
|
10
|
+
return {
|
|
11
|
+
read: (slot) => slots.get(slot),
|
|
12
|
+
write: (slot, entry) => {
|
|
13
|
+
slots.set(slot, entry);
|
|
14
|
+
},
|
|
15
|
+
clear: (slot) => {
|
|
16
|
+
slots.delete(slot);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* In-process write queues, keyed by resolved path so two `fileStore` calls on
|
|
22
|
+
* the same file share one.
|
|
23
|
+
*
|
|
24
|
+
* Every write is a read-modify-write of the whole file, which is only correct
|
|
25
|
+
* if writes to that file never overlap. They otherwise lose each other's slots
|
|
26
|
+
* and race on the temp file. Cross-process safety is out of scope: two `node`
|
|
27
|
+
* processes writing one cache file can still interleave.
|
|
28
|
+
*/
|
|
29
|
+
const writeQueues = new Map();
|
|
30
|
+
let tempCounter = 0;
|
|
31
|
+
function serialize(key, task) {
|
|
32
|
+
const next = (writeQueues.get(key) ?? Promise.resolve()).then(task, task);
|
|
33
|
+
// A failed write must not poison the writes queued behind it.
|
|
34
|
+
writeQueues.set(key, next.then(() => undefined, () => undefined));
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One JSON file, holding every slot that points at it. Several phases may
|
|
39
|
+
* share a path safely, and concurrent writes to one path are serialised.
|
|
40
|
+
*
|
|
41
|
+
* A missing, unreadable or malformed file all mean the same thing — no cache
|
|
42
|
+
* yet — so deleting the file is always a valid way to start over. Values must
|
|
43
|
+
* survive `JSON.stringify`; anything that does not (a `Date`, a class
|
|
44
|
+
* instance) comes back as its JSON shape.
|
|
45
|
+
*/
|
|
46
|
+
export function fileStore(filePath) {
|
|
47
|
+
const target = resolve(filePath);
|
|
48
|
+
const readAll = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(await readFile(target, "utf8"));
|
|
51
|
+
if (parsed === null || typeof parsed !== "object")
|
|
52
|
+
return {};
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// Written through a temp file so an interrupted run cannot leave a
|
|
60
|
+
// half-serialised cache behind for the next one to choke on. The temp name
|
|
61
|
+
// carries a counter as well as the pid — the pid alone is shared by every
|
|
62
|
+
// write in the process, which is exactly how concurrent writes used to
|
|
63
|
+
// rename each other's files out from under themselves.
|
|
64
|
+
const writeAll = async (data) => {
|
|
65
|
+
await mkdir(dirname(target), { recursive: true });
|
|
66
|
+
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
67
|
+
const temp = `${target}.${process.pid}.${(tempCounter += 1)}.tmp`;
|
|
68
|
+
try {
|
|
69
|
+
await writeFile(temp, payload, "utf8");
|
|
70
|
+
try {
|
|
71
|
+
await rename(temp, target);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Windows, antivirus, editors and file watchers can all hold the
|
|
75
|
+
// destination open and refuse the atomic swap. Writing in place gives
|
|
76
|
+
// up atomicity rather than the write.
|
|
77
|
+
const code = error.code;
|
|
78
|
+
if (code !== "EPERM" && code !== "EACCES" && code !== "EBUSY" && code !== "EEXIST") {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
await writeFile(target, payload, "utf8");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await rm(temp, { force: true });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const update = (mutate) => serialize(target, async () => {
|
|
89
|
+
const data = await readAll();
|
|
90
|
+
if (!mutate(data))
|
|
91
|
+
return;
|
|
92
|
+
await writeAll(data);
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
async read(slot) {
|
|
96
|
+
const entry = (await readAll())[slot];
|
|
97
|
+
return isEntry(entry) ? entry : undefined;
|
|
98
|
+
},
|
|
99
|
+
write(slot, entry) {
|
|
100
|
+
return update((data) => {
|
|
101
|
+
data[slot] = entry;
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
clear(slot) {
|
|
106
|
+
return update((data) => {
|
|
107
|
+
if (!Object.hasOwn(data, slot))
|
|
108
|
+
return false;
|
|
109
|
+
delete data[slot];
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** A hand-edited or older-format file should read as a miss, not as a crash. */
|
|
116
|
+
function isEntry(value) {
|
|
117
|
+
return (typeof value === "object" &&
|
|
118
|
+
value !== null &&
|
|
119
|
+
"value" in value &&
|
|
120
|
+
typeof value.savedAt === "number");
|
|
121
|
+
}
|
|
122
|
+
/* -------------------------------------------------------------------------- */
|
|
123
|
+
/* Runtime */
|
|
124
|
+
/* -------------------------------------------------------------------------- */
|
|
125
|
+
/** `cache: someStore` is shorthand for `cache: { store: someStore }`. */
|
|
126
|
+
export function normalizeCache(source) {
|
|
127
|
+
if (!source)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (typeof source.read === "function") {
|
|
130
|
+
return { store: source };
|
|
131
|
+
}
|
|
132
|
+
return source;
|
|
133
|
+
}
|
|
134
|
+
const MISS = { hit: false };
|
|
135
|
+
/**
|
|
136
|
+
* Read an entry and put it through every gate. A failure at any gate is a
|
|
137
|
+
* miss — the work runs and the entry is replaced — never an error.
|
|
138
|
+
*/
|
|
139
|
+
export async function readCache(cache, slot, input, ctx, mode) {
|
|
140
|
+
if (mode === "off" || mode === "refresh")
|
|
141
|
+
return MISS;
|
|
142
|
+
const entry = await cache.store.read(slot);
|
|
143
|
+
if (!entry)
|
|
144
|
+
return MISS;
|
|
145
|
+
let value = entry.value;
|
|
146
|
+
if (cache.schema) {
|
|
147
|
+
const result = await cache.schema["~standard"].validate(value);
|
|
148
|
+
if (result.issues)
|
|
149
|
+
return MISS;
|
|
150
|
+
value = result.value;
|
|
151
|
+
}
|
|
152
|
+
const ageMs = Math.max(0, Date.now() - entry.savedAt);
|
|
153
|
+
if (cache.stale && (await cache.stale({ value, input, ctx, savedAt: entry.savedAt, ageMs }))) {
|
|
154
|
+
return MISS;
|
|
155
|
+
}
|
|
156
|
+
return { hit: true, value, ageMs };
|
|
157
|
+
}
|
|
158
|
+
/** Only ever called after the phase or step has fully succeeded. */
|
|
159
|
+
export async function writeCache(cache, slot, value, mode) {
|
|
160
|
+
if (mode === "off" || mode === "read-only")
|
|
161
|
+
return;
|
|
162
|
+
await cache.store.write(slot, { value, savedAt: Date.now() });
|
|
163
|
+
}
|
|
164
|
+
/** Identifies a phase's entry. Steps hang off their phase to avoid collisions. */
|
|
165
|
+
export const phaseSlot = (phase) => phase;
|
|
166
|
+
export const stepSlot = (phase, step) => `${phase}::${step}`;
|
|
167
|
+
/* -------------------------------------------------------------------------- */
|
|
168
|
+
/* Shape guard */
|
|
169
|
+
/* -------------------------------------------------------------------------- */
|
|
170
|
+
/**
|
|
171
|
+
* Properties every JavaScript runtime reaches for on an arbitrary object.
|
|
172
|
+
* Trapping them breaks `await` (which probes `then`), `JSON.stringify`,
|
|
173
|
+
* `console.log` and template interpolation — so they pass straight through.
|
|
174
|
+
*/
|
|
175
|
+
const PASSTHROUGH = new Set([
|
|
176
|
+
"then",
|
|
177
|
+
"toJSON",
|
|
178
|
+
"constructor",
|
|
179
|
+
"valueOf",
|
|
180
|
+
"toString",
|
|
181
|
+
"inspect",
|
|
182
|
+
"length",
|
|
183
|
+
]);
|
|
184
|
+
/**
|
|
185
|
+
* Wrap a value read back from a store so that touching a field it does not
|
|
186
|
+
* have throws {@link CacheShapeError} instead of yielding `undefined`.
|
|
187
|
+
*
|
|
188
|
+
* Only reads are trapped. Assigning a new property is how you'd legitimately
|
|
189
|
+
* patch an entry before writing it back, so it is left alone — the lie is
|
|
190
|
+
* always on the read side, where the type promised something the JSON lacks.
|
|
191
|
+
*
|
|
192
|
+
* Absent array indices pass through too: `orders[9]` on a short list is
|
|
193
|
+
* ordinary JavaScript, not a shape mismatch.
|
|
194
|
+
*/
|
|
195
|
+
export function guardShape(value, slot, path = "") {
|
|
196
|
+
if (value === null || typeof value !== "object")
|
|
197
|
+
return value;
|
|
198
|
+
return new Proxy(value, {
|
|
199
|
+
get(target, key, receiver) {
|
|
200
|
+
const actual = Reflect.get(target, key, receiver);
|
|
201
|
+
if (typeof key === "symbol" || PASSTHROUGH.has(key))
|
|
202
|
+
return actual;
|
|
203
|
+
const isIndex = Array.isArray(target) && /^(0|[1-9]\d*)$/.test(key);
|
|
204
|
+
const known = Object.hasOwn(target, key) || key in (Object.getPrototypeOf(target) ?? {});
|
|
205
|
+
if (!isIndex && !known)
|
|
206
|
+
throw new CacheShapeError(slot, path ? `${path}.${key}` : key);
|
|
207
|
+
return guardShape(actual, slot, path ? `${path}.${key}` : key);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Build the `cache` handle a handler sees. Slots it does not know about are
|
|
213
|
+
* already unreachable through the types; at runtime they throw rather than
|
|
214
|
+
* silently doing nothing, which is what a plain `store.clear("typo")` does.
|
|
215
|
+
*/
|
|
216
|
+
export function createCacheHandle(slots) {
|
|
217
|
+
const bindingFor = (slot) => {
|
|
218
|
+
const binding = slots.get(slot);
|
|
219
|
+
if (!binding) {
|
|
220
|
+
const known = [...slots.keys()].map((name) => `"${name}"`).join(", ") || "none";
|
|
221
|
+
throw new StepDefinitionError(slot, `No cached phase named "${slot}" has been declared before this step. Known slots: ${known}.`);
|
|
222
|
+
}
|
|
223
|
+
return binding;
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
async read(slot, options) {
|
|
227
|
+
const binding = bindingFor(slot);
|
|
228
|
+
const entry = await binding.store.read(slot);
|
|
229
|
+
if (!entry)
|
|
230
|
+
return undefined;
|
|
231
|
+
// A slot with a schema was validated on the way in, so its type is
|
|
232
|
+
// already backed by a check; only the inferred kind needs guarding.
|
|
233
|
+
if (options?.raw || binding.schema)
|
|
234
|
+
return entry.value;
|
|
235
|
+
return guardShape(entry.value, slot);
|
|
236
|
+
},
|
|
237
|
+
async write(slot, value, options) {
|
|
238
|
+
const store = bindingFor(slot).store;
|
|
239
|
+
const previous = options?.keepAge ? await store.read(slot) : undefined;
|
|
240
|
+
await store.write(slot, { value, savedAt: previous?.savedAt ?? Date.now() });
|
|
241
|
+
},
|
|
242
|
+
async clear(slot) {
|
|
243
|
+
await bindingFor(slot).store.clear(slot);
|
|
244
|
+
},
|
|
245
|
+
async ageOf(slot) {
|
|
246
|
+
const entry = await bindingFor(slot).store.read(slot);
|
|
247
|
+
return entry ? Math.max(0, Date.now() - entry.savedAt) : undefined;
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CacheHandle } from "./types.js";
|
|
1
2
|
import type { StepState } from "./state.js";
|
|
2
3
|
import type { Renderer } from "./ui/renderer.js";
|
|
3
4
|
import type { RollbackContext, StepContext } from "./types.js";
|
|
@@ -5,6 +6,8 @@ export interface ContextDeps {
|
|
|
5
6
|
renderer: Renderer;
|
|
6
7
|
step: StepState;
|
|
7
8
|
phaseName: string;
|
|
9
|
+
/** Backs `context.cache`; slots declared before the current step. */
|
|
10
|
+
cache: CacheHandle<Record<string, unknown>>;
|
|
8
11
|
}
|
|
9
12
|
export declare function createStepContext<In, Ctx>(deps: ContextDeps, input: In, ctx: Ctx, signal: AbortSignal, attempt: number): StepContext<In, Ctx>;
|
|
10
13
|
export declare function createRollbackContext<In, Ctx, Out>(deps: ContextDeps, input: In, ctx: Ctx, output: Out, error: unknown, signal: AbortSignal): RollbackContext<In, Ctx, Out>;
|