@smthrs/flow 1.0.0-rc.7 → 1.0.0-rc.8
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 +3 -1
- package/dist/cjs/Poll.d.ts +3 -1
- package/dist/cjs/Poll.d.ts.map +1 -1
- package/dist/cjs/Poll.js +6 -2
- package/dist/cjs/Poll.js.map +2 -2
- package/dist/cjs/Sleep.d.ts.map +1 -1
- package/dist/cjs/Sleep.js +15 -7
- package/dist/cjs/Sleep.js.map +2 -2
- package/dist/esm/Poll.d.ts +3 -1
- package/dist/esm/Poll.d.ts.map +1 -1
- package/dist/esm/Poll.js +10 -3
- package/dist/esm/Poll.js.map +1 -1
- package/dist/esm/Sleep.d.ts.map +1 -1
- package/dist/esm/Sleep.js +30 -4
- package/dist/esm/Sleep.js.map +1 -1
- package/docs/README.md +3 -1
- package/docs/api.md +6 -6
- package/docs/guides/inspect-the-plan.md +1 -1
- package/docs/guides/poll-until-ready.md +3 -1
- package/docs/guides/wait-for-a-deadline.md +5 -5
- package/docs/installation.md +14 -9
- package/docs/quickstart.md +5 -1
- package/docs/reference/flow.md +5 -1
- package/package.json +6 -6
- package/src/Poll.ts +10 -4
- package/src/Sleep.ts +33 -4
package/README.md
CHANGED
|
@@ -44,7 +44,9 @@ none of which the flow's author arranges.
|
|
|
44
44
|
|
|
45
45
|
## Install
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
```bash
|
|
48
|
+
npm install @smthrs/flow@next effect
|
|
49
|
+
```
|
|
48
50
|
|
|
49
51
|
Node.js 26.4.0 or later. `effect` is a peer in the range `^4.0.4`: two copies of
|
|
50
52
|
`effect` in one program are two sets of service tags.
|
package/dist/cjs/Poll.d.ts
CHANGED
|
@@ -198,7 +198,9 @@ export type PayloadSchema<Input extends Schema.Struct.Fields> = Schema.Struct<In
|
|
|
198
198
|
* with a plan that looks correct. The check is on the schedule, not only on the
|
|
199
199
|
* interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }`
|
|
200
200
|
* states three finite options and asks for an infinite wait, so the LAST wait
|
|
201
|
-
* the poll can arm has to be a length too
|
|
201
|
+
* the poll can arm has to be a length too, and no longer than
|
|
202
|
+
* `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer
|
|
203
|
+
* records. `maxAttempts` is the lineage's round
|
|
202
204
|
* budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`
|
|
203
205
|
* complaint about an option the author never wrote.
|
|
204
206
|
*
|
package/dist/cjs/Poll.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Poll.d.ts","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,IAAI,MAAM,mBAAmB,CAAA;AACzC,OAAO,KAAK,KAAK,UAAU,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAE5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAA;AAExD;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK,CAAC,GAAG,CAAC,MAAM;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,MAAM,CAAC,GAAG,EACnD,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,MAAM,CAAC,OAAO,CAAA;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAiE,CAAA;;;;;;;AAElE;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,kBAWlC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,YAAY,0BAA0B,CAAA;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,QAAQ,CACrC,OAAO,YAAY,EACnB,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC,MAAM,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,OAAO,MAAM,CAAC,MAAM,CAAA;CACxC,CAAC,EACF,OAAO,MAAM,CAAC,KAAK,EACnB,OAAO,aAAa,EACpB,KAAK,CAML,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,KAAG,
|
|
1
|
+
{"version":3,"file":"Poll.d.ts","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/Poll.js
CHANGED
|
@@ -65,7 +65,11 @@ const exhausted = Action.makeSystem(exhaustedTag, {
|
|
|
65
65
|
error: PollExhausted,
|
|
66
66
|
tier: "sealed"
|
|
67
67
|
});
|
|
68
|
-
const delayMillis = (options) =>
|
|
68
|
+
const delayMillis = (options) => (
|
|
69
|
+
// A zero interval is zero under every backoff. Multiplying it by a factor
|
|
70
|
+
// that has overflowed to `Infinity` would make it `NaN`.
|
|
71
|
+
options.intervalMs === 0 ? 0 : options.intervalMs * (options.backoff === "fixed" ? 1 : options.backoff === "linear" ? options.attempt : 2 ** (options.attempt - 1))
|
|
72
|
+
);
|
|
69
73
|
const Failure = Schema.Union([PollExhausted, Sleep.SleepRequestInvalid]);
|
|
70
74
|
const payloadSchema = (input, maxAttempts) => Schema.Struct({
|
|
71
75
|
...input,
|
|
@@ -104,7 +108,7 @@ const make = (tag, options) => {
|
|
|
104
108
|
}
|
|
105
109
|
const backoff = options.backoff ?? "fixed";
|
|
106
110
|
const longestWaitMs = maxAttempts > 1 ? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 }) : 0;
|
|
107
|
-
if (!Number.isFinite(longestWaitMs)) {
|
|
111
|
+
if (!Number.isFinite(longestWaitMs) || longestWaitMs > Number.MAX_SAFE_INTEGER) {
|
|
108
112
|
throw new RangeError(
|
|
109
113
|
`Poll.make: "${tag}" asks for a wait of ${longestWaitMs} ms before its last attempt. An intervalMs of ${intervalMs} under the "${backoff}" backoff reaches a length no clock can be armed with by attempt ${maxAttempts - 1} of ${maxAttempts}. Lower maxAttempts, shorten the interval, or slow the backoff.`
|
|
110
114
|
);
|
package/dist/cjs/Poll.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/Poll.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Polling as a durable lineage: one attempt per round, a durable wait between\n * attempts, and a bound the lineage settles on.\n *\n * Smithers 0.x had a `Poller` component \u2014 a check, an interval, a backoff, an\n * attempt bound, and what to do at that bound. The pieces it was built from\n * already exist here: {@link module:Sleep.action} is the durable wait, and\n * `Flow.to` opens the next round. What was missing is the shape that puts them\n * together, and that is this module.\n *\n * A poll is a flow whose body is ONE attempt. It runs the check, and branches:\n * a satisfied check settles the lineage with the check's own output, an\n * unsatisfied one sleeps for this attempt's delay and hands off to the next\n * round with the attempt counter raised. The last attempt has no sleep and no\n * handoff \u2014 it either answers with the output it just read (`return-last`) or\n * fails {@link PollExhausted} (`fail`).\n *\n * Everything durable about it is durable for the ordinary reason. Each attempt\n * is a round with its own keyed plan nodes, so a check that already ran replays\n * from its recorded outcome instead of running again; the wait between attempts\n * is a durable timer, so a process that dies during it resumes on the round it\n * left rather than starting the poll over; and the attempt counter travels in\n * the payload, so nothing about \"which attempt is this\" lives in memory.\n *\n * The bound is stated twice on purpose. The body ends the lineage on the last\n * attempt, and `maxAttempts` is also the flow's `maxRounds` budget, so a\n * lineage that somehow opened another round is refused by the engine rather\n * than polling forever.\n *\n * @since 0.1.0\n */\n\nimport * as Node from \"@smthrs/plan/Node\"\nimport type * as Repetition from \"@smthrs/plan/Repetition\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as Flow from \"./Flow/index.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport * as Sleep from \"./Sleep.ts\"\n\n/**\n * How the wait between attempts grows.\n *\n * `fixed` waits the interval every time, `linear` waits the interval times the\n * attempt, and `exponential` doubles the interval on every attempt.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Backoff = \"fixed\" | \"linear\" | \"exponential\"\n\n/**\n * What a check reports: whether the poll is over, and the value this attempt\n * read.\n *\n * The output travels on every attempt, satisfied or not, because it is also\n * the answer a `return-last` poll settles with when it runs out of attempts.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Check<out Result> {\n readonly satisfied: boolean\n readonly output: Result\n}\n\n/**\n * The success schema a check action declares.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const CheckResult = <Result extends Schema.Top>(\n result: Result\n): Schema.Struct<{\n readonly satisfied: typeof Schema.Boolean\n readonly output: Result\n}> => Schema.Struct({ satisfied: Schema.Boolean, output: result })\n\n/**\n * A poll that used its last attempt without a satisfied check.\n *\n * It is a typed failure rather than a defect: running out of attempts is an\n * expected outcome of polling something that may never become ready, so a\n * caller recovers from it through `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class PollExhausted extends Schema.TaggedError<PollExhausted>()(\n \"@smthrs/flow/PollExhausted\",\n {\n // This wire shape freezes at 1.0.0-rc.0.\n code: Schema.Literal(\"poll_exhausted\").pipe(\n Schema.withConstructorDefault(Effect.succeed(\"poll_exhausted\"))\n ),\n poll: Schema.String,\n attempts: Schema.Number,\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the exhaustion step is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhaustedTag = \"system/poll-exhausted\"\n\n/**\n * The declared step a poll takes when its last attempt was not satisfied.\n *\n * Running out of attempts is a step rather than a bare `Effect.fail` so the\n * exhaustion is in the plan and in the journal: the round that gave up shows\n * which poll gave up and after how many attempts, exactly like every other node\n * a body drove.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhausted: Action.Declared<\n typeof exhaustedTag,\n Schema.Struct<{\n readonly poll: typeof Schema.String\n readonly attempts: typeof Schema.Number\n }>,\n typeof Schema.Never,\n typeof PollExhausted,\n never\n> = Action.makeSystem(exhaustedTag, {\n payload: { poll: Schema.String, attempts: Schema.Number },\n success: Schema.Never,\n error: PollExhausted,\n tier: \"sealed\"\n})\n\n/**\n * The wait before the attempt after this one, in milliseconds.\n *\n * @category combinators\n * @since 0.1.0\n */\nexport const delayMillis = (options: {\n readonly intervalMs: number\n readonly backoff: Backoff\n readonly attempt: number\n}): number =>\n options.intervalMs *\n (options.backoff === \"fixed\" ? 1 : options.backoff === \"linear\" ? options.attempt : 2 ** (options.attempt - 1))\n\n/**\n * What a poll round can fail with.\n *\n * {@link PollExhausted} is the poll's own failure. `SleepRequestInvalid` is the\n * wait node's, declared because the wait between attempts is an ordinary\n * {@link module:Sleep.action} node and its typed refusal is part of the\n * topology a poll carries. Its codes cover a payload that names no deadline,\n * names two deadlines, or names a value that is not a deadline. {@link make}\n * refuses invalid author schedules at construction. A caller-visible round\n * payload can still carry an invalid attempt that produces an invalid derived\n * wait, so `Sleep` refuses that wait before the round parks.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Failure: Schema.Union<\n readonly [typeof PollExhausted, typeof Sleep.SleepRequestInvalid]\n> = Schema.Union([PollExhausted, Sleep.SleepRequestInvalid])\n\n/**\n * The payload schema {@link make} derives from an author's input fields: the\n * author's own fields, plus the attempt this round is.\n *\n * `attempt` is optional so a caller starts a poll with its own fields alone;\n * the first round reads it as one.\n *\n * @category models\n * @since 0.1.0\n */\nexport type PayloadSchema<Input extends Schema.Struct.Fields> = Schema.Struct<\n Input & { readonly attempt: Schema.optional<typeof Schema.Number> }\n>\n\n/**\n * The payload schema one round is opened with: the author's fields, plus the\n * attempt counter the lineage carries between them.\n *\n * @private\n */\nconst payloadSchema = <Input extends Schema.Struct.Fields>(\n input: Input,\n maxAttempts: number\n): PayloadSchema<Input> =>\n Schema.Struct({\n ...input,\n attempt: Schema.optional(\n Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(1),\n Schema.isLessThanOrEqualTo(maxAttempts)\n )\n )\n })\n\n/**\n * The attempt a round payload carries, defaulting to the first.\n *\n * @private\n */\nconst attemptOf = (payload: object): number => {\n const attempt = (payload as { readonly attempt?: unknown }).attempt\n return typeof attempt === \"number\" ? attempt : 1\n}\n\n/**\n * Rebuilds a round payload with an explicit attempt.\n *\n * The two views of one round payload \u2014 what the check reads, and what the next\n * round is opened with \u2014 are the same fields with `attempt` resolved. TypeScript\n * cannot reduce a mapped type over an unresolved generic field set, so the\n * shape is stated once here instead of at each use.\n *\n * @private\n */\nconst withAttempt = <Payload>(payload: object, attempt: number): Payload =>\n ({ ...payload, attempt }) as unknown as Payload\n\n/**\n * Declares a durable poller.\n *\n * **When to use**\n *\n * Use it wherever a run has to keep asking until something is ready \u2014 a build\n * that finishes, a deployment that goes live, a review that lands \u2014 and the\n * waiting has to survive a restart. `check` is the one thing an author writes:\n * a body fragment that reads whatever it polls and reports a {@link Check}.\n * Everything else is schedule. Under stable callback admission (the default of\n * `Interpreter.layerWithImplementations`), wrap `check` in `Node.capture` with\n * every semantic capture, including a version for imported behavior. An\n * uncaptured check keeps the poll process-local. The helper captures its own\n * callbacks using the schedule and the check's identity.\n *\n * ```ts\n * const Deployment = Poll.make(\"deploy/wait\", {\n * input: { id: Schema.String },\n * result: Schema.String,\n * intervalMs: 5_000,\n * backoff: \"exponential\",\n * maxAttempts: 8,\n * onTimeout: \"fail\",\n * check: Node.capture(\n * { action: Status.name, implementationVersion: \"deploy/status-check/v1\" },\n * ({ attempt, id }) => Status.call({ attempt, id })\n * )\n * })\n * ```\n *\n * The check may not fail. A check that can fail states what a failure means for\n * the poll \u2014 give up, or treat it as \"not ready yet\" \u2014 with `Node.catch` inside\n * the fragment, rather than leaving the poll to guess.\n *\n * A per-attempt time limit on the check itself is NOT a poll option. A plan\n * node's duration is not something the body around it can bound; that bound\n * belongs in the check's own implementation, where `DurableDeferred.raceAll`\n * races the work against a durable clock and stays replayable.\n *\n * A schedule no clock can be armed with is refused here, with a `RangeError`\n * naming the option that is wrong. `intervalMs` becomes the `millis` of a\n * {@link module:Sleep.action} node, and a wait of `Infinity`, `NaN`, or a\n * negative length arms a timer that never fires \u2014 a poll that hangs forever\n * with a plan that looks correct. The check is on the schedule, not only on the\n * interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: \"exponential\" }`\n * states three finite options and asks for an infinite wait, so the LAST wait\n * the poll can arm has to be a length too. `maxAttempts` is the lineage's round\n * budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`\n * complaint about an option the author never wrote.\n *\n * @throws A `RangeError` when `intervalMs` is not a duration a clock accepts,\n * when `maxAttempts` is not a whole number of attempts of at least one, or when\n * the interval under the declared backoff reaches a wait no clock can be armed\n * with before the budget is spent.\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <\n const Tag extends string,\n Input extends Schema.Struct.Fields,\n Result extends Schema.Top,\n R = never\n>(\n tag: Tag,\n options: {\n readonly input: Input\n readonly result: Result\n readonly check: (\n payload: Schema.Struct.Type<Input> & { readonly attempt: number }\n ) => Node.Node<Check<Result[\"Type\"]>, never, R>\n readonly intervalMs: number\n readonly maxAttempts: number\n readonly backoff?: Backoff | undefined\n readonly onTimeout?: Repetition.AtCeiling | undefined\n }\n): Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R> => {\n type Round = Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R>\n type RoundPayload = PayloadSchema<Input>[\"Type\"]\n if (Object.prototype.hasOwnProperty.call(options.input, \"attempt\")) {\n throw new TypeError(\n `Poll.make: \"${tag}\" input cannot declare the reserved \"attempt\" field. ` +\n \"Poll owns that counter across durable rounds.\"\n )\n }\n const input = Object.freeze({ ...options.input }) as Input\n const result = options.result\n const check = options.check\n const intervalMs = options.intervalMs\n const maxAttempts = options.maxAttempts\n if (!Number.isFinite(intervalMs) || intervalMs < 0) {\n throw new RangeError(\n `Poll.make: \"${tag}\" intervalMs must be a finite number of milliseconds that is not negative, ` +\n `and was ${intervalMs}. The interval is the length of a durable wait, and a wait of ` +\n \"that length never ends.\"\n )\n }\n if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {\n throw new RangeError(\n `Poll.make: \"${tag}\" maxAttempts must be a whole number of attempts of at least one, and was ` +\n `${maxAttempts}. A poll checks at least once.`\n )\n }\n const backoff = options.backoff ?? \"fixed\"\n // The wait a poll arms is not `intervalMs`. {@link delayMillis} multiplies it\n // by the backoff, so a schedule whose options are each finite can still ask\n // for a wait that is not a length \u2014 and that wait arms the same timer that\n // never fires the interval guard above exists to refuse. The last wait is the\n // one before the final attempt, because the attempt at the budget gives up\n // rather than sleeps, and the schedule never shrinks, so the last wait is\n // also the longest: checking it checks all of them.\n const longestWaitMs = maxAttempts > 1\n ? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 })\n : 0\n if (!Number.isFinite(longestWaitMs)) {\n throw new RangeError(\n `Poll.make: \"${tag}\" asks for a wait of ${longestWaitMs} ms before its last attempt. An ` +\n `intervalMs of ${intervalMs} under the \"${backoff}\" backoff reaches a length no clock ` +\n `can be armed with by attempt ${maxAttempts - 1} of ${maxAttempts}. Lower ` +\n \"maxAttempts, shorten the interval, or slow the backoff.\"\n )\n }\n const onTimeout = options.onTimeout ?? \"fail\"\n const captures = { tag, intervalMs, backoff, maxAttempts, onTimeout, check: Node.functionIdentity(check) }\n const body = (\n payload: RoundPayload\n ): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type, R> => {\n const attempt = attemptOf(payload)\n const last = attempt >= maxAttempts\n return check(withAttempt(payload, attempt)).pipe(\n Node.branch({\n if: Node.capture(captures, (checked) => checked.satisfied),\n then: (checked) => Flow.done(checked.output),\n else: (checked): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type> =>\n last\n ? onTimeout === \"return-last\"\n ? Flow.done(checked.output)\n : exhausted.call({ poll: tag, attempts: attempt })\n : Sleep.action.call({\n millis: delayMillis({ intervalMs, backoff, attempt })\n }).pipe(\n Node.andThen(\n self.to(withAttempt<Parameters<Round[\"to\"]>[0]>(payload, attempt + 1))\n )\n )\n })\n )\n }\n // The body names the flow it is the body of, which is what makes one round\n // open the next. It is read when a round is planned, long after `Flow.make`\n // has returned, so the binding is initialized by then.\n const self: Round = Flow.make(tag, {\n payload: payloadSchema(input, maxAttempts),\n success: result,\n error: Failure,\n maxRounds: maxAttempts,\n // A captured wrapper must not admit a process-local check as stable.\n body: captures.check.algorithm === \"sha256-source-ephemeral/v4\" ? body : Node.capture(captures, body)\n })\n return self\n}\n\n/**\n * The exhaustion implementation, provided beside the other action layers a\n * poll's rounds call.\n *\n * ```ts\n * Layer.mergeAll(Poll.layer, Sleep.layer, Interpreter.layer(Deployment)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = exhausted.toLayer(\n ({ attempts, poll }) =>\n Effect.fail(\n new PollExhausted({\n poll,\n attempts,\n message: `Poll \"${poll}\" used all ${attempts} attempts without a satisfied check.`\n })\n )\n)\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCA,WAAsB;AAGtB,aAAwB;AAExB,aAAwB;AACxB,aAAwB;AACxB,WAAsB;AAEtB,YAAuB;AAkChB,MAAM,cAAc,CACzB,WAII,OAAO,OAAO,EAAE,WAAW,OAAO,SAAS,QAAQ,OAAO,CAAC;AAY1D,MAAM,sBAAsB,OAAO,YAA2B;AAAA,EACnE;AAAA,EACA;AAAA;AAAA,IAEE,MAAM,OAAO,QAAQ,gBAAgB,EAAE;AAAA,MACrC,OAAO,uBAAuB,OAAO,QAAQ,gBAAgB,CAAC;AAAA,IAChE;AAAA,IACA,MAAM,OAAO;AAAA,IACb,UAAU,OAAO;AAAA,IACjB,SAAS,OAAO;AAAA,EAClB;AACF,EAAE;AAAC;AAQI,MAAM,eAAe;AAarB,MAAM,YAST,OAAO,WAAW,cAAc;AAAA,EAClC,SAAS,EAAE,MAAM,OAAO,QAAQ,UAAU,OAAO,OAAO;AAAA,EACxD,SAAS,OAAO;AAAA,EAChB,OAAO;AAAA,EACP,MAAM;AACR,CAAC;AAQM,MAAM,cAAc,CAAC,
|
|
4
|
+
"sourcesContent": ["/**\n * Polling as a durable lineage: one attempt per round, a durable wait between\n * attempts, and a bound the lineage settles on.\n *\n * Smithers 0.x had a `Poller` component \u2014 a check, an interval, a backoff, an\n * attempt bound, and what to do at that bound. The pieces it was built from\n * already exist here: {@link module:Sleep.action} is the durable wait, and\n * `Flow.to` opens the next round. What was missing is the shape that puts them\n * together, and that is this module.\n *\n * A poll is a flow whose body is ONE attempt. It runs the check, and branches:\n * a satisfied check settles the lineage with the check's own output, an\n * unsatisfied one sleeps for this attempt's delay and hands off to the next\n * round with the attempt counter raised. The last attempt has no sleep and no\n * handoff \u2014 it either answers with the output it just read (`return-last`) or\n * fails {@link PollExhausted} (`fail`).\n *\n * Everything durable about it is durable for the ordinary reason. Each attempt\n * is a round with its own keyed plan nodes, so a check that already ran replays\n * from its recorded outcome instead of running again; the wait between attempts\n * is a durable timer, so a process that dies during it resumes on the round it\n * left rather than starting the poll over; and the attempt counter travels in\n * the payload, so nothing about \"which attempt is this\" lives in memory.\n *\n * The bound is stated twice on purpose. The body ends the lineage on the last\n * attempt, and `maxAttempts` is also the flow's `maxRounds` budget, so a\n * lineage that somehow opened another round is refused by the engine rather\n * than polling forever.\n *\n * @since 0.1.0\n */\n\nimport * as Node from \"@smthrs/plan/Node\"\nimport type * as Repetition from \"@smthrs/plan/Repetition\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as Flow from \"./Flow/index.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport * as Sleep from \"./Sleep.ts\"\n\n/**\n * How the wait between attempts grows.\n *\n * `fixed` waits the interval every time, `linear` waits the interval times the\n * attempt, and `exponential` doubles the interval on every attempt.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Backoff = \"fixed\" | \"linear\" | \"exponential\"\n\n/**\n * What a check reports: whether the poll is over, and the value this attempt\n * read.\n *\n * The output travels on every attempt, satisfied or not, because it is also\n * the answer a `return-last` poll settles with when it runs out of attempts.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Check<out Result> {\n readonly satisfied: boolean\n readonly output: Result\n}\n\n/**\n * The success schema a check action declares.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const CheckResult = <Result extends Schema.Top>(\n result: Result\n): Schema.Struct<{\n readonly satisfied: typeof Schema.Boolean\n readonly output: Result\n}> => Schema.Struct({ satisfied: Schema.Boolean, output: result })\n\n/**\n * A poll that used its last attempt without a satisfied check.\n *\n * It is a typed failure rather than a defect: running out of attempts is an\n * expected outcome of polling something that may never become ready, so a\n * caller recovers from it through `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class PollExhausted extends Schema.TaggedError<PollExhausted>()(\n \"@smthrs/flow/PollExhausted\",\n {\n // This wire shape freezes at 1.0.0-rc.0.\n code: Schema.Literal(\"poll_exhausted\").pipe(\n Schema.withConstructorDefault(Effect.succeed(\"poll_exhausted\"))\n ),\n poll: Schema.String,\n attempts: Schema.Number,\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the exhaustion step is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhaustedTag = \"system/poll-exhausted\"\n\n/**\n * The declared step a poll takes when its last attempt was not satisfied.\n *\n * Running out of attempts is a step rather than a bare `Effect.fail` so the\n * exhaustion is in the plan and in the journal: the round that gave up shows\n * which poll gave up and after how many attempts, exactly like every other node\n * a body drove.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhausted: Action.Declared<\n typeof exhaustedTag,\n Schema.Struct<{\n readonly poll: typeof Schema.String\n readonly attempts: typeof Schema.Number\n }>,\n typeof Schema.Never,\n typeof PollExhausted,\n never\n> = Action.makeSystem(exhaustedTag, {\n payload: { poll: Schema.String, attempts: Schema.Number },\n success: Schema.Never,\n error: PollExhausted,\n tier: \"sealed\"\n})\n\n/**\n * The wait before the attempt after this one, in milliseconds.\n *\n * @category combinators\n * @since 0.1.0\n */\nexport const delayMillis = (options: {\n readonly intervalMs: number\n readonly backoff: Backoff\n readonly attempt: number\n}): number =>\n // A zero interval is zero under every backoff. Multiplying it by a factor\n // that has overflowed to `Infinity` would make it `NaN`.\n options.intervalMs === 0 ? 0 : options.intervalMs *\n (options.backoff === \"fixed\" ? 1 : options.backoff === \"linear\" ? options.attempt : 2 ** (options.attempt - 1))\n\n/**\n * What a poll round can fail with.\n *\n * {@link PollExhausted} is the poll's own failure. `SleepRequestInvalid` is the\n * wait node's, declared because the wait between attempts is an ordinary\n * {@link module:Sleep.action} node and its typed refusal is part of the\n * topology a poll carries. Its codes cover a payload that names no deadline,\n * names two deadlines, or names a value that is not a deadline. {@link make}\n * refuses invalid author schedules at construction. A caller-visible round\n * payload can still carry an invalid attempt that produces an invalid derived\n * wait, so `Sleep` refuses that wait before the round parks.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Failure: Schema.Union<\n readonly [typeof PollExhausted, typeof Sleep.SleepRequestInvalid]\n> = Schema.Union([PollExhausted, Sleep.SleepRequestInvalid])\n\n/**\n * The payload schema {@link make} derives from an author's input fields: the\n * author's own fields, plus the attempt this round is.\n *\n * `attempt` is optional so a caller starts a poll with its own fields alone;\n * the first round reads it as one.\n *\n * @category models\n * @since 0.1.0\n */\nexport type PayloadSchema<Input extends Schema.Struct.Fields> = Schema.Struct<\n Input & { readonly attempt: Schema.optional<typeof Schema.Number> }\n>\n\n/**\n * The payload schema one round is opened with: the author's fields, plus the\n * attempt counter the lineage carries between them.\n *\n * @private\n */\nconst payloadSchema = <Input extends Schema.Struct.Fields>(\n input: Input,\n maxAttempts: number\n): PayloadSchema<Input> =>\n Schema.Struct({\n ...input,\n attempt: Schema.optional(\n Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(1),\n Schema.isLessThanOrEqualTo(maxAttempts)\n )\n )\n })\n\n/**\n * The attempt a round payload carries, defaulting to the first.\n *\n * @private\n */\nconst attemptOf = (payload: object): number => {\n const attempt = (payload as { readonly attempt?: unknown }).attempt\n return typeof attempt === \"number\" ? attempt : 1\n}\n\n/**\n * Rebuilds a round payload with an explicit attempt.\n *\n * The two views of one round payload \u2014 what the check reads, and what the next\n * round is opened with \u2014 are the same fields with `attempt` resolved. TypeScript\n * cannot reduce a mapped type over an unresolved generic field set, so the\n * shape is stated once here instead of at each use.\n *\n * @private\n */\nconst withAttempt = <Payload>(payload: object, attempt: number): Payload =>\n ({ ...payload, attempt }) as unknown as Payload\n\n/**\n * Declares a durable poller.\n *\n * **When to use**\n *\n * Use it wherever a run has to keep asking until something is ready \u2014 a build\n * that finishes, a deployment that goes live, a review that lands \u2014 and the\n * waiting has to survive a restart. `check` is the one thing an author writes:\n * a body fragment that reads whatever it polls and reports a {@link Check}.\n * Everything else is schedule. Under stable callback admission (the default of\n * `Interpreter.layerWithImplementations`), wrap `check` in `Node.capture` with\n * every semantic capture, including a version for imported behavior. An\n * uncaptured check keeps the poll process-local. The helper captures its own\n * callbacks using the schedule and the check's identity.\n *\n * ```ts\n * const Deployment = Poll.make(\"deploy/wait\", {\n * input: { id: Schema.String },\n * result: Schema.String,\n * intervalMs: 5_000,\n * backoff: \"exponential\",\n * maxAttempts: 8,\n * onTimeout: \"fail\",\n * check: Node.capture(\n * { action: Status.name, implementationVersion: \"deploy/status-check/v1\" },\n * ({ attempt, id }) => Status.call({ attempt, id })\n * )\n * })\n * ```\n *\n * The check may not fail. A check that can fail states what a failure means for\n * the poll \u2014 give up, or treat it as \"not ready yet\" \u2014 with `Node.catch` inside\n * the fragment, rather than leaving the poll to guess.\n *\n * A per-attempt time limit on the check itself is NOT a poll option. A plan\n * node's duration is not something the body around it can bound; that bound\n * belongs in the check's own implementation, where `DurableDeferred.raceAll`\n * races the work against a durable clock and stays replayable.\n *\n * A schedule no clock can be armed with is refused here, with a `RangeError`\n * naming the option that is wrong. `intervalMs` becomes the `millis` of a\n * {@link module:Sleep.action} node, and a wait of `Infinity`, `NaN`, or a\n * negative length arms a timer that never fires \u2014 a poll that hangs forever\n * with a plan that looks correct. The check is on the schedule, not only on the\n * interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: \"exponential\" }`\n * states three finite options and asks for an infinite wait, so the LAST wait\n * the poll can arm has to be a length too, and no longer than\n * `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer\n * records. `maxAttempts` is the lineage's round\n * budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`\n * complaint about an option the author never wrote.\n *\n * @throws A `RangeError` when `intervalMs` is not a duration a clock accepts,\n * when `maxAttempts` is not a whole number of attempts of at least one, or when\n * the interval under the declared backoff reaches a wait no clock can be armed\n * with before the budget is spent.\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <\n const Tag extends string,\n Input extends Schema.Struct.Fields,\n Result extends Schema.Top,\n R = never\n>(\n tag: Tag,\n options: {\n readonly input: Input\n readonly result: Result\n readonly check: (\n payload: Schema.Struct.Type<Input> & { readonly attempt: number }\n ) => Node.Node<Check<Result[\"Type\"]>, never, R>\n readonly intervalMs: number\n readonly maxAttempts: number\n readonly backoff?: Backoff | undefined\n readonly onTimeout?: Repetition.AtCeiling | undefined\n }\n): Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R> => {\n type Round = Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R>\n type RoundPayload = PayloadSchema<Input>[\"Type\"]\n if (Object.prototype.hasOwnProperty.call(options.input, \"attempt\")) {\n throw new TypeError(\n `Poll.make: \"${tag}\" input cannot declare the reserved \"attempt\" field. ` +\n \"Poll owns that counter across durable rounds.\"\n )\n }\n const input = Object.freeze({ ...options.input }) as Input\n const result = options.result\n const check = options.check\n const intervalMs = options.intervalMs\n const maxAttempts = options.maxAttempts\n if (!Number.isFinite(intervalMs) || intervalMs < 0) {\n throw new RangeError(\n `Poll.make: \"${tag}\" intervalMs must be a finite number of milliseconds that is not negative, ` +\n `and was ${intervalMs}. The interval is the length of a durable wait, and a wait of ` +\n \"that length never ends.\"\n )\n }\n if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {\n throw new RangeError(\n `Poll.make: \"${tag}\" maxAttempts must be a whole number of attempts of at least one, and was ` +\n `${maxAttempts}. A poll checks at least once.`\n )\n }\n const backoff = options.backoff ?? \"fixed\"\n // The wait a poll arms is not `intervalMs`. {@link delayMillis} multiplies it\n // by the backoff, so a schedule whose options are each finite can still ask\n // for a wait that is not a length \u2014 and that wait arms the same timer that\n // never fires the interval guard above exists to refuse. The last wait is the\n // one before the final attempt, because the attempt at the budget gives up\n // rather than sleeps, and the schedule never shrinks, so the last wait is\n // also the longest: checking it checks all of them.\n const longestWaitMs = maxAttempts > 1\n ? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 })\n : 0\n // A durable timer records its deadline as a safe integer of milliseconds,\n // so `Sleep` refuses a longer wait on the round that reaches it.\n if (!Number.isFinite(longestWaitMs) || longestWaitMs > Number.MAX_SAFE_INTEGER) {\n throw new RangeError(\n `Poll.make: \"${tag}\" asks for a wait of ${longestWaitMs} ms before its last attempt. An ` +\n `intervalMs of ${intervalMs} under the \"${backoff}\" backoff reaches a length no clock ` +\n `can be armed with by attempt ${maxAttempts - 1} of ${maxAttempts}. Lower ` +\n \"maxAttempts, shorten the interval, or slow the backoff.\"\n )\n }\n const onTimeout = options.onTimeout ?? \"fail\"\n const captures = { tag, intervalMs, backoff, maxAttempts, onTimeout, check: Node.functionIdentity(check) }\n const body = (\n payload: RoundPayload\n ): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type, R> => {\n const attempt = attemptOf(payload)\n const last = attempt >= maxAttempts\n return check(withAttempt(payload, attempt)).pipe(\n Node.branch({\n if: Node.capture(captures, (checked) => checked.satisfied),\n then: (checked) => Flow.done(checked.output),\n else: (checked): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type> =>\n last\n ? onTimeout === \"return-last\"\n ? Flow.done(checked.output)\n : exhausted.call({ poll: tag, attempts: attempt })\n : Sleep.action.call({\n millis: delayMillis({ intervalMs, backoff, attempt })\n }).pipe(\n Node.andThen(\n self.to(withAttempt<Parameters<Round[\"to\"]>[0]>(payload, attempt + 1))\n )\n )\n })\n )\n }\n // The body names the flow it is the body of, which is what makes one round\n // open the next. It is read when a round is planned, long after `Flow.make`\n // has returned, so the binding is initialized by then.\n const self: Round = Flow.make(tag, {\n payload: payloadSchema(input, maxAttempts),\n success: result,\n error: Failure,\n maxRounds: maxAttempts,\n // A captured wrapper must not admit a process-local check as stable.\n body: captures.check.algorithm === \"sha256-source-ephemeral/v4\" ? body : Node.capture(captures, body)\n })\n return self\n}\n\n/**\n * The exhaustion implementation, provided beside the other action layers a\n * poll's rounds call.\n *\n * ```ts\n * Layer.mergeAll(Poll.layer, Sleep.layer, Interpreter.layer(Deployment)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = exhausted.toLayer(\n ({ attempts, poll }) =>\n Effect.fail(\n new PollExhausted({\n poll,\n attempts,\n message: `Poll \"${poll}\" used all ${attempts} attempts without a satisfied check.`\n })\n )\n)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/cjs/Sleep.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sleep.d.ts","sourceRoot":"","sources":["../../src/Sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAG5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;;;;;AAG/D;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,SAAQ,wBAMxC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,GAAG,iBAAiB,CAAA;AAEjC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,QAAQ,CAClC,OAAO,GAAG,EACV,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CAC/C,CAAC,EACF,OAAO,MAAM,CAAC,IAAI,EAClB,OAAO,mBAAmB,EAC1B,KAAK,CASL,CAAA;
|
|
1
|
+
{"version":3,"file":"Sleep.d.ts","sourceRoot":"","sources":["../../src/Sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAG5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;;;;;AAG/D;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,SAAQ,wBAMxC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,GAAG,iBAAiB,CAAA;AAEjC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,QAAQ,CAClC,OAAO,GAAG,EACV,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CAC/C,CAAC,EACF,OAAO,MAAM,CAAC,IAAI,EAClB,OAAO,mBAAmB,EAC1B,KAAK,CASL,CAAA;AA0IF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,CAuCxE,CAAA"}
|
package/dist/cjs/Sleep.js
CHANGED
|
@@ -59,6 +59,11 @@ const action = Action.makeSystem(tag, {
|
|
|
59
59
|
error: SleepRequestInvalid,
|
|
60
60
|
tier: "sealed"
|
|
61
61
|
});
|
|
62
|
+
const latestDeadline = Number.MAX_SAFE_INTEGER;
|
|
63
|
+
const tooLate = (stated) => new SleepRequestInvalid({
|
|
64
|
+
code: "invalid_deadline",
|
|
65
|
+
message: `${tag} was called with ${stated}, which ends after ${latestDeadline}, the latest epoch millisecond a durable timer records.`
|
|
66
|
+
});
|
|
62
67
|
const deadlineOf = (payload, now) => {
|
|
63
68
|
if (payload.millis !== void 0 && payload.until !== void 0) {
|
|
64
69
|
return Effect.fail(
|
|
@@ -86,15 +91,18 @@ const deadlineOf = (payload, now) => {
|
|
|
86
91
|
})
|
|
87
92
|
);
|
|
88
93
|
}
|
|
89
|
-
return Effect.succeed(deadline);
|
|
94
|
+
return Math.ceil(deadline) > latestDeadline ? Effect.fail(tooLate(`millis ${payload.millis} at current time ${now}`)) : Effect.succeed(deadline);
|
|
90
95
|
}
|
|
91
96
|
if (payload.until !== void 0) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
if (!Number.isFinite(payload.until)) {
|
|
98
|
+
return Effect.fail(
|
|
99
|
+
new SleepRequestInvalid({
|
|
100
|
+
code: "invalid_deadline",
|
|
101
|
+
message: `${tag} was called with until ${payload.until}. A deadline is a finite epoch time in milliseconds; the sleep must end at a real point in time.`
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return Math.ceil(payload.until) > latestDeadline ? Effect.fail(tooLate(`until ${payload.until}`)) : Effect.succeed(payload.until);
|
|
98
106
|
}
|
|
99
107
|
return Effect.fail(
|
|
100
108
|
new SleepRequestInvalid({
|
package/dist/cjs/Sleep.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/Sleep.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The system timer, as an ordinary declared action.\n *\n * `docs/concepts/flows-and-actions.md` names the timer primitive a\n * gap and says how to close it: ship it over the existing `DurableClock`, so a\n * wait is a visible, keyed plan node rather than a second execution mechanism.\n * That is this module. {@link action} is a plain `Action.make` declaration \u2014\n * it appears in a catalog, `.call()` records a node, and `Graph.build` puts that\n * node in the plan beside every other step \u2014 and {@link layer} is the\n * implementation the hosts that can run a timer provide.\n *\n * The wait itself is `DurableClock`'s, not a new one. The implementation arms\n * the durable clock and awaits its deferred, which is what parks the execution;\n * the park is declared through the ordinary waiting vocabulary\n * ({@link module:WaitingAnnotation.annotateWaiting}) under `timer`, with the\n * deadline as `wakeAt`, so a durable driver records the same waiting row it\n * records for a hand-written `DurableClock.sleep`. Replay is the deferred's:\n * once the clock has fired, its result is persisted, so a re-driven round reads\n * it and runs straight through instead of parking a second time.\n *\n * Which clock a wait arms is the dispatch's own, read from\n * {@link module:Context.CurrentInvocationKey}: a keyed node already owns an\n * identity that survives replay and separates two identical calls, so the timer\n * inherits it rather than being addressed by the payload it was called with.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as DurableClock from \"./DurableClock.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport { annotateWaiting } from \"./FlowRuntime/WaitingAnnotation.ts\"\n\n/**\n * A sleep whose payload does not name exactly one deadline.\n *\n * A payload carries a relative `millis` or an absolute `until`, and naming\n * both, or neither, leaves no deadline to arm a clock with. It is a typed\n * failure rather than a defect because the payload of a driven node is\n * assembled from plan values, so an author's mistake is recoverable through\n * `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SleepRequestInvalid extends Schema.TaggedError<SleepRequestInvalid>()(\n \"@smthrs/flow/SleepRequestInvalid\",\n {\n code: Schema.Literals([\"missing_deadline\", \"ambiguous_deadline\", \"invalid_deadline\"]),\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the sleep declaration is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const tag = \"system/sleep\"\n\n/**\n * The declared sleep action: `millis` from now, or an absolute `until`.\n *\n * **When to use**\n *\n * Use it in a body wherever a round has to wait for time to pass \u2014\n * `Sleep.action.call({ millis: 60_000 })` \u2014 rather than reaching for\n * `DurableClock.sleep`, which is only callable from a handler and leaves\n * nothing in the plan. A sleep node is keyed, inspectable, and skippable by a\n * branch exactly like an action that does work.\n *\n * Two waits of the same length are two waits: `sleep(1h)` followed by\n * `sleep(1h)` sleeps for two hours, because each call is its own node with its\n * own identity, exactly as two calls of any other declaration are.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const action: Action.Declared<\n typeof tag,\n Schema.Struct<{\n readonly millis: Schema.optional<Schema.Number>\n readonly until: Schema.optional<Schema.Number>\n }>,\n typeof Schema.Void,\n typeof SleepRequestInvalid,\n never\n> = Action.makeSystem(tag, {\n payload: {\n millis: Schema.optional(Schema.Number),\n until: Schema.optional(Schema.Number)\n },\n success: Schema.Void,\n error: SleepRequestInvalid,\n tier: \"sealed\"\n})\n\n/**\n * The absolute deadline a payload names, in epoch milliseconds.\n *\n * DECIDED: a payload names EXACTLY one of `millis`\n * and `until`, and both other shapes are refused rather than resolved by\n * precedence. A precedence rule would silently ignore half of a payload an\n * author believed in, and the two fields are the same fact stated two ways, so\n * there is no shape where carrying both is meaningful.\n *\n * `Duration.millis` accepts every number, so an unchecked deadline changes the\n * sleep instead of refusing it. `NaN` becomes zero, a negative relative delay\n * has already passed, and `Infinity` never ends. The implementation rejects\n * those values before it annotates the instance or arms a clock. A finite\n * absolute `until` remains legal when it is in the past because that sleep can\n * settle immediately.\n *\n * @private\n */\nconst deadlineOf = (\n payload: { readonly millis?: number | undefined; readonly until?: number | undefined },\n now: number\n): Effect.Effect<number, SleepRequestInvalid> => {\n if (payload.millis !== undefined && payload.until !== undefined) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"ambiguous_deadline\",\n message: `${tag} was called with both \"millis\" and \"until\". A sleep names one deadline: ` +\n \"a relative duration, or an absolute epoch time.\"\n })\n )\n }\n if (payload.millis !== undefined) {\n if (!Number.isFinite(payload.millis) || payload.millis < 0) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis}. A deadline is a finite number of ` +\n \"milliseconds that is not negative; the sleep would be over before it started, or never over at all.\"\n })\n )\n }\n const deadline = now + payload.millis\n if (!Number.isFinite(deadline)) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis} at current time ${now}, which produces ` +\n `${deadline}. A deadline is a finite epoch time in milliseconds.`\n })\n )\n }\n return Effect.succeed(deadline)\n }\n if (payload.until !== undefined) {\n return Number.isFinite(payload.until)\n ? Effect.succeed(payload.until)\n : Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message:\n `${tag} was called with until ${payload.until}. A deadline is a finite epoch time in milliseconds; ` +\n \"the sleep must end at a real point in time.\"\n })\n )\n }\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"missing_deadline\",\n message: `${tag} was called with neither \"millis\" nor \"until\", so there is no deadline to wait for.`\n })\n )\n}\n\n/**\n * The durable clock the dispatch currently executing arms.\n *\n * DECIDED: the name is derived from the ENGINE's\n * per-dispatch identity \u2014 the invocation key it already allocated \u2014 rather than\n * from the payload. A durable clock's deferred is addressed by (execution,\n * name), so the name has to be stable across replays of one node and distinct\n * between two identical nodes, and the invocation key is exactly that: it is\n * allocated on every dispatch including a replayed one, because the driver\n * addresses the recorded outcome by it. A payload-derived name has only the\n * first property \u2014 `sleep(1h); work(); sleep(1h)` would address ONE wake and\n * sleep once \u2014 and closing that with a discriminator in the payload would make\n * an author carry identity that a keyed plan node already has.\n *\n * DECIDED: a runtime that supplies no dispatch\n * identity is refused as a defect rather than falling back to the payload. The\n * fallback is unobservable in the one-wait case and silently collapses two\n * waits in every other, so it would trade a wiring error a host fixes once for\n * a run that quietly sleeps for half as long as it declared.\n *\n * @private\n */\nconst clockName: Effect.Effect<string> = Effect.gen(function*() {\n const invocation = yield* Action.CurrentInvocationKey\n if (invocation === undefined) {\n return yield* Effect.die(\n `${tag} ran under a runtime that supplies no Action.CurrentInvocationKey. ` +\n \"A durable timer is named by the dispatch that armed it, so a runtime that does not \" +\n \"identify the dispatch cannot arm one.\"\n )\n }\n return `Sleep/${invocation}`\n})\n\n/**\n * The sleep implementation: arm the durable clock, park under `timer`, wake.\n *\n * Provide it beside the other action implementation layers a body calls, over\n * the {@link module:Implementations.layerImplementations} table they file\n * themselves in:\n *\n * ```ts\n * Layer.mergeAll(Sleep.layer, Interpreter.layer(Pipeline)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = action.toLayer((payload) =>\n Effect.gen(function*() {\n // Whole milliseconds, both of them. A durable deadline lands in an INTEGER\n // column whose CHECK constraint refuses anything else\n // (`flows_runs.waiting_wake_at_ms`, `flows_clock_deadlines.due_at_ms`), and\n // `Clock.currentTimeMillis` is not integral under every clock: a test clock\n // advanced by a fractional-millisecond sleep reports a fractional now, and\n // a fractional `millis` or `until` in the payload does the same. The\n // unrounded deadline reached the park write as a raw SQL constraint\n // failure, which killed the drive and left the run `running` with nothing\n // left to wake it. `now` floors, and the deadline ceils so a rounded sleep\n // never ends before the instant it was asked to wait for.\n const now = Math.floor(yield* Clock.currentTimeMillis)\n const wakeAt = Math.ceil(yield* deadlineOf(payload, now))\n const remaining = wakeAt - now\n // DECIDED: a deadline that has already passed\n // settles the node instead of parking it. A durable wait exists to survive\n // the gap until a deadline, and a run that resumes after its own deadline\n // has to make progress rather than park again on a timer that can only fire\n // in the past.\n if (remaining <= 0) return\n const name = yield* clockName\n // DECIDED: the park is declared rather than\n // left to a driver's derivation, so a host with no clock table still parks\n // under `timer`. A `millis` deadline is recomputed from the current clock,\n // so a round re-driven before its timer fires re-declares a `wakeAt` later\n // than the armed one; the wake itself is unaffected, because the durable\n // clock row is keyed by name and keeps the deadline it was first armed\n // with, and `wakeAt` is a sweeper hint rather than the wake.\n yield* annotateWaiting({ reason: \"timer\", wakeAt })\n yield* DurableClock.sleep({\n name,\n duration: Duration.millis(remaining),\n // The wait is the point: a sleep called as a plan node parks the\n // execution rather than holding a fiber open, so the in-memory shortcut\n // is disabled instead of being left at its 60-second default.\n inMemoryThreshold: Duration.zero\n })\n })\n)\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BA,YAAuB;AAEvB,eAA0B;AAC1B,aAAwB;AAExB,aAAwB;AACxB,aAAwB;AACxB,mBAA8B;AAE9B,+BAAgC;AAczB,MAAM,4BAA4B,OAAO,YAAiC;AAAA,EAC/E;AAAA,EACA;AAAA,IACE,MAAM,OAAO,SAAS,CAAC,oBAAoB,sBAAsB,kBAAkB,CAAC;AAAA,IACpF,SAAS,OAAO;AAAA,EAClB;AACF,EAAE;AAAC;AAQI,MAAM,MAAM;AAoBZ,MAAM,SAST,OAAO,WAAW,KAAK;AAAA,EACzB,SAAS;AAAA,IACP,QAAQ,OAAO,SAAS,OAAO,MAAM;AAAA,IACrC,OAAO,OAAO,SAAS,OAAO,MAAM;AAAA,EACtC;AAAA,EACA,SAAS,OAAO;AAAA,EAChB,OAAO;AAAA,EACP,MAAM;AACR,CAAC;
|
|
4
|
+
"sourcesContent": ["/**\n * The system timer, as an ordinary declared action.\n *\n * `docs/concepts/flows-and-actions.md` names the timer primitive a\n * gap and says how to close it: ship it over the existing `DurableClock`, so a\n * wait is a visible, keyed plan node rather than a second execution mechanism.\n * That is this module. {@link action} is a plain `Action.make` declaration \u2014\n * it appears in a catalog, `.call()` records a node, and `Graph.build` puts that\n * node in the plan beside every other step \u2014 and {@link layer} is the\n * implementation the hosts that can run a timer provide.\n *\n * The wait itself is `DurableClock`'s, not a new one. The implementation arms\n * the durable clock and awaits its deferred, which is what parks the execution;\n * the park is declared through the ordinary waiting vocabulary\n * ({@link module:WaitingAnnotation.annotateWaiting}) under `timer`, with the\n * deadline as `wakeAt`, so a durable driver records the same waiting row it\n * records for a hand-written `DurableClock.sleep`. Replay is the deferred's:\n * once the clock has fired, its result is persisted, so a re-driven round reads\n * it and runs straight through instead of parking a second time.\n *\n * Which clock a wait arms is the dispatch's own, read from\n * {@link module:Context.CurrentInvocationKey}: a keyed node already owns an\n * identity that survives replay and separates two identical calls, so the timer\n * inherits it rather than being addressed by the payload it was called with.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as DurableClock from \"./DurableClock.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport { annotateWaiting } from \"./FlowRuntime/WaitingAnnotation.ts\"\n\n/**\n * A sleep whose payload does not name exactly one deadline.\n *\n * A payload carries a relative `millis` or an absolute `until`, and naming\n * both, or neither, leaves no deadline to arm a clock with. It is a typed\n * failure rather than a defect because the payload of a driven node is\n * assembled from plan values, so an author's mistake is recoverable through\n * `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SleepRequestInvalid extends Schema.TaggedError<SleepRequestInvalid>()(\n \"@smthrs/flow/SleepRequestInvalid\",\n {\n code: Schema.Literals([\"missing_deadline\", \"ambiguous_deadline\", \"invalid_deadline\"]),\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the sleep declaration is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const tag = \"system/sleep\"\n\n/**\n * The declared sleep action: `millis` from now, or an absolute `until`.\n *\n * **When to use**\n *\n * Use it in a body wherever a round has to wait for time to pass \u2014\n * `Sleep.action.call({ millis: 60_000 })` \u2014 rather than reaching for\n * `DurableClock.sleep`, which is only callable from a handler and leaves\n * nothing in the plan. A sleep node is keyed, inspectable, and skippable by a\n * branch exactly like an action that does work.\n *\n * Two waits of the same length are two waits: `sleep(1h)` followed by\n * `sleep(1h)` sleeps for two hours, because each call is its own node with its\n * own identity, exactly as two calls of any other declaration are.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const action: Action.Declared<\n typeof tag,\n Schema.Struct<{\n readonly millis: Schema.optional<Schema.Number>\n readonly until: Schema.optional<Schema.Number>\n }>,\n typeof Schema.Void,\n typeof SleepRequestInvalid,\n never\n> = Action.makeSystem(tag, {\n payload: {\n millis: Schema.optional(Schema.Number),\n until: Schema.optional(Schema.Number)\n },\n success: Schema.Void,\n error: SleepRequestInvalid,\n tier: \"sealed\"\n})\n\n/**\n * The latest deadline a sleep can arm, in epoch milliseconds.\n *\n * A durable store records `wakeAt` and a clock's due time as a non-negative\n * safe integer. A finite deadline past this one passed every other check and\n * then failed the park write, so it is refused with the payload's other\n * invalid deadlines instead.\n *\n * @private\n */\nconst latestDeadline = Number.MAX_SAFE_INTEGER\n\n/**\n * The refusal for a deadline later than {@link latestDeadline}.\n *\n * @private\n */\nconst tooLate = (stated: string): SleepRequestInvalid =>\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with ${stated}, which ends after ${latestDeadline}, the latest epoch ` +\n \"millisecond a durable timer records.\"\n })\n\n/**\n * The absolute deadline a payload names, in epoch milliseconds.\n *\n * DECIDED: a payload names EXACTLY one of `millis`\n * and `until`, and both other shapes are refused rather than resolved by\n * precedence. A precedence rule would silently ignore half of a payload an\n * author believed in, and the two fields are the same fact stated two ways, so\n * there is no shape where carrying both is meaningful.\n *\n * `Duration.millis` accepts every number, so an unchecked deadline changes the\n * sleep instead of refusing it. `NaN` becomes zero, a negative relative delay\n * has already passed, and `Infinity` never ends. The implementation rejects\n * those values before it annotates the instance or arms a clock. A finite\n * absolute `until` remains legal when it is in the past because that sleep can\n * settle immediately.\n *\n * @private\n */\nconst deadlineOf = (\n payload: { readonly millis?: number | undefined; readonly until?: number | undefined },\n now: number\n): Effect.Effect<number, SleepRequestInvalid> => {\n if (payload.millis !== undefined && payload.until !== undefined) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"ambiguous_deadline\",\n message: `${tag} was called with both \"millis\" and \"until\". A sleep names one deadline: ` +\n \"a relative duration, or an absolute epoch time.\"\n })\n )\n }\n if (payload.millis !== undefined) {\n if (!Number.isFinite(payload.millis) || payload.millis < 0) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis}. A deadline is a finite number of ` +\n \"milliseconds that is not negative; the sleep would be over before it started, or never over at all.\"\n })\n )\n }\n const deadline = now + payload.millis\n if (!Number.isFinite(deadline)) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis} at current time ${now}, which produces ` +\n `${deadline}. A deadline is a finite epoch time in milliseconds.`\n })\n )\n }\n return Math.ceil(deadline) > latestDeadline\n ? Effect.fail(tooLate(`millis ${payload.millis} at current time ${now}`))\n : Effect.succeed(deadline)\n }\n if (payload.until !== undefined) {\n if (!Number.isFinite(payload.until)) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message:\n `${tag} was called with until ${payload.until}. A deadline is a finite epoch time in milliseconds; ` +\n \"the sleep must end at a real point in time.\"\n })\n )\n }\n return Math.ceil(payload.until) > latestDeadline\n ? Effect.fail(tooLate(`until ${payload.until}`))\n : Effect.succeed(payload.until)\n }\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"missing_deadline\",\n message: `${tag} was called with neither \"millis\" nor \"until\", so there is no deadline to wait for.`\n })\n )\n}\n\n/**\n * The durable clock the dispatch currently executing arms.\n *\n * DECIDED: the name is derived from the ENGINE's\n * per-dispatch identity \u2014 the invocation key it already allocated \u2014 rather than\n * from the payload. A durable clock's deferred is addressed by (execution,\n * name), so the name has to be stable across replays of one node and distinct\n * between two identical nodes, and the invocation key is exactly that: it is\n * allocated on every dispatch including a replayed one, because the driver\n * addresses the recorded outcome by it. A payload-derived name has only the\n * first property \u2014 `sleep(1h); work(); sleep(1h)` would address ONE wake and\n * sleep once \u2014 and closing that with a discriminator in the payload would make\n * an author carry identity that a keyed plan node already has.\n *\n * DECIDED: a runtime that supplies no dispatch\n * identity is refused as a defect rather than falling back to the payload. The\n * fallback is unobservable in the one-wait case and silently collapses two\n * waits in every other, so it would trade a wiring error a host fixes once for\n * a run that quietly sleeps for half as long as it declared.\n *\n * @private\n */\nconst clockName: Effect.Effect<string> = Effect.gen(function*() {\n const invocation = yield* Action.CurrentInvocationKey\n if (invocation === undefined) {\n return yield* Effect.die(\n `${tag} ran under a runtime that supplies no Action.CurrentInvocationKey. ` +\n \"A durable timer is named by the dispatch that armed it, so a runtime that does not \" +\n \"identify the dispatch cannot arm one.\"\n )\n }\n return `Sleep/${invocation}`\n})\n\n/**\n * The sleep implementation: arm the durable clock, park under `timer`, wake.\n *\n * Provide it beside the other action implementation layers a body calls, over\n * the {@link module:Implementations.layerImplementations} table they file\n * themselves in:\n *\n * ```ts\n * Layer.mergeAll(Sleep.layer, Interpreter.layer(Pipeline)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = action.toLayer((payload) =>\n Effect.gen(function*() {\n // Whole milliseconds, both of them. A durable deadline lands in an INTEGER\n // column whose CHECK constraint refuses anything else\n // (`flows_runs.waiting_wake_at_ms`, `flows_clock_deadlines.due_at_ms`), and\n // `Clock.currentTimeMillis` is not integral under every clock: a test clock\n // advanced by a fractional-millisecond sleep reports a fractional now, and\n // a fractional `millis` or `until` in the payload does the same. The\n // unrounded deadline reached the park write as a raw SQL constraint\n // failure, which killed the drive and left the run `running` with nothing\n // left to wake it. `now` floors, and the deadline ceils so a rounded sleep\n // never ends before the instant it was asked to wait for.\n const now = Math.floor(yield* Clock.currentTimeMillis)\n const wakeAt = Math.ceil(yield* deadlineOf(payload, now))\n const remaining = wakeAt - now\n // DECIDED: a deadline that has already passed\n // settles the node instead of parking it. A durable wait exists to survive\n // the gap until a deadline, and a run that resumes after its own deadline\n // has to make progress rather than park again on a timer that can only fire\n // in the past.\n if (remaining <= 0) return\n const name = yield* clockName\n // DECIDED: the park is declared rather than\n // left to a driver's derivation, so a host with no clock table still parks\n // under `timer`. A `millis` deadline is recomputed from the current clock,\n // so a round re-driven before its timer fires re-declares a `wakeAt` later\n // than the armed one; the wake itself is unaffected, because the durable\n // clock row is keyed by name and keeps the deadline it was first armed\n // with, and `wakeAt` is a sweeper hint rather than the wake.\n yield* annotateWaiting({ reason: \"timer\", wakeAt })\n yield* DurableClock.sleep({\n name,\n duration: Duration.millis(remaining),\n // The wait is the point: a sleep called as a plan node parks the\n // execution rather than holding a fiber open, so the in-memory shortcut\n // is disabled instead of being left at its 60-second default.\n inMemoryThreshold: Duration.zero\n })\n })\n)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/esm/Poll.d.ts
CHANGED
|
@@ -198,7 +198,9 @@ export type PayloadSchema<Input extends Schema.Struct.Fields> = Schema.Struct<In
|
|
|
198
198
|
* with a plan that looks correct. The check is on the schedule, not only on the
|
|
199
199
|
* interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }`
|
|
200
200
|
* states three finite options and asks for an infinite wait, so the LAST wait
|
|
201
|
-
* the poll can arm has to be a length too
|
|
201
|
+
* the poll can arm has to be a length too, and no longer than
|
|
202
|
+
* `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer
|
|
203
|
+
* records. `maxAttempts` is the lineage's round
|
|
202
204
|
* budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`
|
|
203
205
|
* complaint about an option the author never wrote.
|
|
204
206
|
*
|
package/dist/esm/Poll.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Poll.d.ts","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,IAAI,MAAM,mBAAmB,CAAA;AACzC,OAAO,KAAK,KAAK,UAAU,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAE5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAA;AAExD;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK,CAAC,GAAG,CAAC,MAAM;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,MAAM,CAAC,GAAG,EACnD,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,MAAM,CAAC,OAAO,CAAA;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAiE,CAAA;;;;;;;AAElE;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,kBAWlC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,YAAY,0BAA0B,CAAA;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,QAAQ,CACrC,OAAO,YAAY,EACnB,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC,MAAM,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,OAAO,MAAM,CAAC,MAAM,CAAA;CACxC,CAAC,EACF,OAAO,MAAM,CAAC,KAAK,EACnB,OAAO,aAAa,EACpB,KAAK,CAML,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,KAAG,
|
|
1
|
+
{"version":3,"file":"Poll.d.ts","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,IAAI,MAAM,mBAAmB,CAAA;AACzC,OAAO,KAAK,KAAK,UAAU,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAE5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAA;AAExD;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK,CAAC,GAAG,CAAC,MAAM;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,MAAM,CAAC,GAAG,EACnD,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,MAAM,CAAC,OAAO,CAAA;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAiE,CAAA;;;;;;;AAElE;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,kBAWlC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,YAAY,0BAA0B,CAAA;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,QAAQ,CACrC,OAAO,YAAY,EACnB,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC,MAAM,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,OAAO,MAAM,CAAC,MAAM,CAAA;CACxC,CAAC,EACF,OAAO,MAAM,CAAC,KAAK,EACnB,OAAO,aAAa,EACpB,KAAK,CAML,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,KAAG,MAI+G,CAAA;AAEnH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,KAAK,CAChC,SAAS,CAAC,OAAO,aAAa,EAAE,OAAO,KAAK,CAAC,mBAAmB,CAAC,CACP,CAAA;AAE5D;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAC3E,KAAK,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAA;CAAE,CACpE,CAAA;AA6CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,eAAO,MAAM,IAAI,GACf,KAAK,CAAC,GAAG,SAAS,MAAM,EACxB,KAAK,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,EAClC,MAAM,SAAS,MAAM,CAAC,GAAG,EACzB,CAAC,GAAG,KAAK,EAET,KAAK,GAAG,EACR,SAAS;IACP,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,CACd,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAC9D,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,SAAS,GAAG,SAAS,CAAA;CACtD,KACA,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,OAAO,EAAE,CAAC,CAsFhE,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,CASxE,CAAA"}
|
package/dist/esm/Poll.js
CHANGED
|
@@ -90,7 +90,10 @@ export const exhausted = Action.makeSystem(exhaustedTag, {
|
|
|
90
90
|
* @category combinators
|
|
91
91
|
* @since 0.1.0
|
|
92
92
|
*/
|
|
93
|
-
export const delayMillis = (options) =>
|
|
93
|
+
export const delayMillis = (options) =>
|
|
94
|
+
// A zero interval is zero under every backoff. Multiplying it by a factor
|
|
95
|
+
// that has overflowed to `Infinity` would make it `NaN`.
|
|
96
|
+
options.intervalMs === 0 ? 0 : options.intervalMs *
|
|
94
97
|
(options.backoff === "fixed" ? 1 : options.backoff === "linear" ? options.attempt : 2 ** (options.attempt - 1));
|
|
95
98
|
/**
|
|
96
99
|
* What a poll round can fail with.
|
|
@@ -184,7 +187,9 @@ const withAttempt = (payload, attempt) => ({ ...payload, attempt });
|
|
|
184
187
|
* with a plan that looks correct. The check is on the schedule, not only on the
|
|
185
188
|
* interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }`
|
|
186
189
|
* states three finite options and asks for an infinite wait, so the LAST wait
|
|
187
|
-
* the poll can arm has to be a length too
|
|
190
|
+
* the poll can arm has to be a length too, and no longer than
|
|
191
|
+
* `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer
|
|
192
|
+
* records. `maxAttempts` is the lineage's round
|
|
188
193
|
* budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`
|
|
189
194
|
* complaint about an option the author never wrote.
|
|
190
195
|
*
|
|
@@ -225,7 +230,9 @@ export const make = (tag, options) => {
|
|
|
225
230
|
const longestWaitMs = maxAttempts > 1
|
|
226
231
|
? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 })
|
|
227
232
|
: 0;
|
|
228
|
-
|
|
233
|
+
// A durable timer records its deadline as a safe integer of milliseconds,
|
|
234
|
+
// so `Sleep` refuses a longer wait on the round that reaches it.
|
|
235
|
+
if (!Number.isFinite(longestWaitMs) || longestWaitMs > Number.MAX_SAFE_INTEGER) {
|
|
229
236
|
throw new RangeError(`Poll.make: "${tag}" asks for a wait of ${longestWaitMs} ms before its last attempt. An ` +
|
|
230
237
|
`intervalMs of ${intervalMs} under the "${backoff}" backoff reaches a length no clock ` +
|
|
231
238
|
`can be armed with by attempt ${maxAttempts - 1} of ${maxAttempts}. Lower ` +
|
package/dist/esm/Poll.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Poll.js","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Polling as a durable lineage: one attempt per round, a durable wait between\n * attempts, and a bound the lineage settles on.\n *\n * Smithers 0.x had a `Poller` component — a check, an interval, a backoff, an\n * attempt bound, and what to do at that bound. The pieces it was built from\n * already exist here: {@link module:Sleep.action} is the durable wait, and\n * `Flow.to` opens the next round. What was missing is the shape that puts them\n * together, and that is this module.\n *\n * A poll is a flow whose body is ONE attempt. It runs the check, and branches:\n * a satisfied check settles the lineage with the check's own output, an\n * unsatisfied one sleeps for this attempt's delay and hands off to the next\n * round with the attempt counter raised. The last attempt has no sleep and no\n * handoff — it either answers with the output it just read (`return-last`) or\n * fails {@link PollExhausted} (`fail`).\n *\n * Everything durable about it is durable for the ordinary reason. Each attempt\n * is a round with its own keyed plan nodes, so a check that already ran replays\n * from its recorded outcome instead of running again; the wait between attempts\n * is a durable timer, so a process that dies during it resumes on the round it\n * left rather than starting the poll over; and the attempt counter travels in\n * the payload, so nothing about \"which attempt is this\" lives in memory.\n *\n * The bound is stated twice on purpose. The body ends the lineage on the last\n * attempt, and `maxAttempts` is also the flow's `maxRounds` budget, so a\n * lineage that somehow opened another round is refused by the engine rather\n * than polling forever.\n *\n * @since 0.1.0\n */\n\nimport * as Node from \"@smthrs/plan/Node\"\nimport type * as Repetition from \"@smthrs/plan/Repetition\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as Flow from \"./Flow/index.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport * as Sleep from \"./Sleep.ts\"\n\n/**\n * How the wait between attempts grows.\n *\n * `fixed` waits the interval every time, `linear` waits the interval times the\n * attempt, and `exponential` doubles the interval on every attempt.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Backoff = \"fixed\" | \"linear\" | \"exponential\"\n\n/**\n * What a check reports: whether the poll is over, and the value this attempt\n * read.\n *\n * The output travels on every attempt, satisfied or not, because it is also\n * the answer a `return-last` poll settles with when it runs out of attempts.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Check<out Result> {\n readonly satisfied: boolean\n readonly output: Result\n}\n\n/**\n * The success schema a check action declares.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const CheckResult = <Result extends Schema.Top>(\n result: Result\n): Schema.Struct<{\n readonly satisfied: typeof Schema.Boolean\n readonly output: Result\n}> => Schema.Struct({ satisfied: Schema.Boolean, output: result })\n\n/**\n * A poll that used its last attempt without a satisfied check.\n *\n * It is a typed failure rather than a defect: running out of attempts is an\n * expected outcome of polling something that may never become ready, so a\n * caller recovers from it through `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class PollExhausted extends Schema.TaggedError<PollExhausted>()(\n \"@smthrs/flow/PollExhausted\",\n {\n // This wire shape freezes at 1.0.0-rc.0.\n code: Schema.Literal(\"poll_exhausted\").pipe(\n Schema.withConstructorDefault(Effect.succeed(\"poll_exhausted\"))\n ),\n poll: Schema.String,\n attempts: Schema.Number,\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the exhaustion step is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhaustedTag = \"system/poll-exhausted\"\n\n/**\n * The declared step a poll takes when its last attempt was not satisfied.\n *\n * Running out of attempts is a step rather than a bare `Effect.fail` so the\n * exhaustion is in the plan and in the journal: the round that gave up shows\n * which poll gave up and after how many attempts, exactly like every other node\n * a body drove.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhausted: Action.Declared<\n typeof exhaustedTag,\n Schema.Struct<{\n readonly poll: typeof Schema.String\n readonly attempts: typeof Schema.Number\n }>,\n typeof Schema.Never,\n typeof PollExhausted,\n never\n> = Action.makeSystem(exhaustedTag, {\n payload: { poll: Schema.String, attempts: Schema.Number },\n success: Schema.Never,\n error: PollExhausted,\n tier: \"sealed\"\n})\n\n/**\n * The wait before the attempt after this one, in milliseconds.\n *\n * @category combinators\n * @since 0.1.0\n */\nexport const delayMillis = (options: {\n readonly intervalMs: number\n readonly backoff: Backoff\n readonly attempt: number\n}): number =>\n options.intervalMs *\n (options.backoff === \"fixed\" ? 1 : options.backoff === \"linear\" ? options.attempt : 2 ** (options.attempt - 1))\n\n/**\n * What a poll round can fail with.\n *\n * {@link PollExhausted} is the poll's own failure. `SleepRequestInvalid` is the\n * wait node's, declared because the wait between attempts is an ordinary\n * {@link module:Sleep.action} node and its typed refusal is part of the\n * topology a poll carries. Its codes cover a payload that names no deadline,\n * names two deadlines, or names a value that is not a deadline. {@link make}\n * refuses invalid author schedules at construction. A caller-visible round\n * payload can still carry an invalid attempt that produces an invalid derived\n * wait, so `Sleep` refuses that wait before the round parks.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Failure: Schema.Union<\n readonly [typeof PollExhausted, typeof Sleep.SleepRequestInvalid]\n> = Schema.Union([PollExhausted, Sleep.SleepRequestInvalid])\n\n/**\n * The payload schema {@link make} derives from an author's input fields: the\n * author's own fields, plus the attempt this round is.\n *\n * `attempt` is optional so a caller starts a poll with its own fields alone;\n * the first round reads it as one.\n *\n * @category models\n * @since 0.1.0\n */\nexport type PayloadSchema<Input extends Schema.Struct.Fields> = Schema.Struct<\n Input & { readonly attempt: Schema.optional<typeof Schema.Number> }\n>\n\n/**\n * The payload schema one round is opened with: the author's fields, plus the\n * attempt counter the lineage carries between them.\n *\n * @private\n */\nconst payloadSchema = <Input extends Schema.Struct.Fields>(\n input: Input,\n maxAttempts: number\n): PayloadSchema<Input> =>\n Schema.Struct({\n ...input,\n attempt: Schema.optional(\n Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(1),\n Schema.isLessThanOrEqualTo(maxAttempts)\n )\n )\n })\n\n/**\n * The attempt a round payload carries, defaulting to the first.\n *\n * @private\n */\nconst attemptOf = (payload: object): number => {\n const attempt = (payload as { readonly attempt?: unknown }).attempt\n return typeof attempt === \"number\" ? attempt : 1\n}\n\n/**\n * Rebuilds a round payload with an explicit attempt.\n *\n * The two views of one round payload — what the check reads, and what the next\n * round is opened with — are the same fields with `attempt` resolved. TypeScript\n * cannot reduce a mapped type over an unresolved generic field set, so the\n * shape is stated once here instead of at each use.\n *\n * @private\n */\nconst withAttempt = <Payload>(payload: object, attempt: number): Payload =>\n ({ ...payload, attempt }) as unknown as Payload\n\n/**\n * Declares a durable poller.\n *\n * **When to use**\n *\n * Use it wherever a run has to keep asking until something is ready — a build\n * that finishes, a deployment that goes live, a review that lands — and the\n * waiting has to survive a restart. `check` is the one thing an author writes:\n * a body fragment that reads whatever it polls and reports a {@link Check}.\n * Everything else is schedule. Under stable callback admission (the default of\n * `Interpreter.layerWithImplementations`), wrap `check` in `Node.capture` with\n * every semantic capture, including a version for imported behavior. An\n * uncaptured check keeps the poll process-local. The helper captures its own\n * callbacks using the schedule and the check's identity.\n *\n * ```ts\n * const Deployment = Poll.make(\"deploy/wait\", {\n * input: { id: Schema.String },\n * result: Schema.String,\n * intervalMs: 5_000,\n * backoff: \"exponential\",\n * maxAttempts: 8,\n * onTimeout: \"fail\",\n * check: Node.capture(\n * { action: Status.name, implementationVersion: \"deploy/status-check/v1\" },\n * ({ attempt, id }) => Status.call({ attempt, id })\n * )\n * })\n * ```\n *\n * The check may not fail. A check that can fail states what a failure means for\n * the poll — give up, or treat it as \"not ready yet\" — with `Node.catch` inside\n * the fragment, rather than leaving the poll to guess.\n *\n * A per-attempt time limit on the check itself is NOT a poll option. A plan\n * node's duration is not something the body around it can bound; that bound\n * belongs in the check's own implementation, where `DurableDeferred.raceAll`\n * races the work against a durable clock and stays replayable.\n *\n * A schedule no clock can be armed with is refused here, with a `RangeError`\n * naming the option that is wrong. `intervalMs` becomes the `millis` of a\n * {@link module:Sleep.action} node, and a wait of `Infinity`, `NaN`, or a\n * negative length arms a timer that never fires — a poll that hangs forever\n * with a plan that looks correct. The check is on the schedule, not only on the\n * interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: \"exponential\" }`\n * states three finite options and asks for an infinite wait, so the LAST wait\n * the poll can arm has to be a length too. `maxAttempts` is the lineage's round\n * budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`\n * complaint about an option the author never wrote.\n *\n * @throws A `RangeError` when `intervalMs` is not a duration a clock accepts,\n * when `maxAttempts` is not a whole number of attempts of at least one, or when\n * the interval under the declared backoff reaches a wait no clock can be armed\n * with before the budget is spent.\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <\n const Tag extends string,\n Input extends Schema.Struct.Fields,\n Result extends Schema.Top,\n R = never\n>(\n tag: Tag,\n options: {\n readonly input: Input\n readonly result: Result\n readonly check: (\n payload: Schema.Struct.Type<Input> & { readonly attempt: number }\n ) => Node.Node<Check<Result[\"Type\"]>, never, R>\n readonly intervalMs: number\n readonly maxAttempts: number\n readonly backoff?: Backoff | undefined\n readonly onTimeout?: Repetition.AtCeiling | undefined\n }\n): Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R> => {\n type Round = Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R>\n type RoundPayload = PayloadSchema<Input>[\"Type\"]\n if (Object.prototype.hasOwnProperty.call(options.input, \"attempt\")) {\n throw new TypeError(\n `Poll.make: \"${tag}\" input cannot declare the reserved \"attempt\" field. ` +\n \"Poll owns that counter across durable rounds.\"\n )\n }\n const input = Object.freeze({ ...options.input }) as Input\n const result = options.result\n const check = options.check\n const intervalMs = options.intervalMs\n const maxAttempts = options.maxAttempts\n if (!Number.isFinite(intervalMs) || intervalMs < 0) {\n throw new RangeError(\n `Poll.make: \"${tag}\" intervalMs must be a finite number of milliseconds that is not negative, ` +\n `and was ${intervalMs}. The interval is the length of a durable wait, and a wait of ` +\n \"that length never ends.\"\n )\n }\n if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {\n throw new RangeError(\n `Poll.make: \"${tag}\" maxAttempts must be a whole number of attempts of at least one, and was ` +\n `${maxAttempts}. A poll checks at least once.`\n )\n }\n const backoff = options.backoff ?? \"fixed\"\n // The wait a poll arms is not `intervalMs`. {@link delayMillis} multiplies it\n // by the backoff, so a schedule whose options are each finite can still ask\n // for a wait that is not a length — and that wait arms the same timer that\n // never fires the interval guard above exists to refuse. The last wait is the\n // one before the final attempt, because the attempt at the budget gives up\n // rather than sleeps, and the schedule never shrinks, so the last wait is\n // also the longest: checking it checks all of them.\n const longestWaitMs = maxAttempts > 1\n ? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 })\n : 0\n if (!Number.isFinite(longestWaitMs)) {\n throw new RangeError(\n `Poll.make: \"${tag}\" asks for a wait of ${longestWaitMs} ms before its last attempt. An ` +\n `intervalMs of ${intervalMs} under the \"${backoff}\" backoff reaches a length no clock ` +\n `can be armed with by attempt ${maxAttempts - 1} of ${maxAttempts}. Lower ` +\n \"maxAttempts, shorten the interval, or slow the backoff.\"\n )\n }\n const onTimeout = options.onTimeout ?? \"fail\"\n const captures = { tag, intervalMs, backoff, maxAttempts, onTimeout, check: Node.functionIdentity(check) }\n const body = (\n payload: RoundPayload\n ): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type, R> => {\n const attempt = attemptOf(payload)\n const last = attempt >= maxAttempts\n return check(withAttempt(payload, attempt)).pipe(\n Node.branch({\n if: Node.capture(captures, (checked) => checked.satisfied),\n then: (checked) => Flow.done(checked.output),\n else: (checked): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type> =>\n last\n ? onTimeout === \"return-last\"\n ? Flow.done(checked.output)\n : exhausted.call({ poll: tag, attempts: attempt })\n : Sleep.action.call({\n millis: delayMillis({ intervalMs, backoff, attempt })\n }).pipe(\n Node.andThen(\n self.to(withAttempt<Parameters<Round[\"to\"]>[0]>(payload, attempt + 1))\n )\n )\n })\n )\n }\n // The body names the flow it is the body of, which is what makes one round\n // open the next. It is read when a round is planned, long after `Flow.make`\n // has returned, so the binding is initialized by then.\n const self: Round = Flow.make(tag, {\n payload: payloadSchema(input, maxAttempts),\n success: result,\n error: Failure,\n maxRounds: maxAttempts,\n // A captured wrapper must not admit a process-local check as stable.\n body: captures.check.algorithm === \"sha256-source-ephemeral/v4\" ? body : Node.capture(captures, body)\n })\n return self\n}\n\n/**\n * The exhaustion implementation, provided beside the other action layers a\n * poll's rounds call.\n *\n * ```ts\n * Layer.mergeAll(Poll.layer, Sleep.layer, Interpreter.layer(Deployment)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = exhausted.toLayer(\n ({ attempts, poll }) =>\n Effect.fail(\n new PollExhausted({\n poll,\n attempts,\n message: `Poll \"${poll}\" used all ${attempts} attempts without a satisfied check.`\n })\n )\n)\n"]}
|
|
1
|
+
{"version":3,"file":"Poll.js","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,IAAI,MAAM,mBAAmB,CAAA;AAGzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAA;AAEvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AA4BnC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,MAAc,EAIb,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAElE;;;;;;;;;GASG;AACH,MAAM,OAAO,aAAc,SAAQ,MAAM,CAAC,WAAW,EAAiB,CACpE,4BAA4B,EAC5B;IACE,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,IAAI,CACzC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAChE;IACD,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM;CACvB,CACF;CAAG;AAEJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,uBAAuB,CAAA;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GASlB,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE;IAClC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IACzD,OAAO,EAAE,MAAM,CAAC,KAAK;IACrB,KAAK,EAAE,aAAa;IACpB,IAAI,EAAE,QAAQ;CACf,CAAC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAI3B,EAAU,EAAE;AACX,0EAA0E;AAC1E,yDAAyD;AACzD,OAAO,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU;IAC/C,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAA;AAEnH;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,OAAO,GAEhB,MAAM,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAA;AAgB5D;;;;;GAKG;AACH,MAAM,aAAa,GAAG,CACpB,KAAY,EACZ,WAAmB,EACG,EAAE,CACxB,MAAM,CAAC,MAAM,CAAC;IACZ,GAAG,KAAK;IACR,OAAO,EAAE,MAAM,CAAC,QAAQ,CACtB,MAAM,CAAC,GAAG,CAAC,KAAK,CACd,MAAM,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAChC,MAAM,CAAC,mBAAmB,CAAC,WAAW,CAAC,CACxC,CACF;CACF,CAAC,CAAA;AAEJ;;;;GAIG;AACH,MAAM,SAAS,GAAG,CAAC,OAAe,EAAU,EAAE;IAC5C,MAAM,OAAO,GAAI,OAA0C,CAAC,OAAO,CAAA;IACnE,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;AAClD,CAAC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,GAAG,CAAU,OAAe,EAAE,OAAe,EAAW,EAAE,CACzE,CAAC,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAuB,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAMlB,GAAQ,EACR,OAUC,EACgE,EAAE;IAGnE,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,SAAS,CACjB,eAAe,GAAG,uDAAuD;YACvE,+CAA+C,CAClD,CAAA;IACH,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,CAAU,CAAA;IAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;IAC3B,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAA;IACrC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAA;IACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,UAAU,CAClB,eAAe,GAAG,6EAA6E;YAC7F,WAAW,UAAU,gEAAgE;YACrF,yBAAyB,CAC5B,CAAA;IACH,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,UAAU,CAClB,eAAe,GAAG,4EAA4E;YAC5F,GAAG,WAAW,gCAAgC,CACjD,CAAA;IACH,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAA;IAC1C,8EAA8E;IAC9E,4EAA4E;IAC5E,2EAA2E;IAC3E,8EAA8E;IAC9E,2EAA2E;IAC3E,0EAA0E;IAC1E,oDAAoD;IACpD,MAAM,aAAa,GAAG,WAAW,GAAG,CAAC;QACnC,CAAC,CAAC,WAAW,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,CAAC,EAAE,CAAC;QAChE,CAAC,CAAC,CAAC,CAAA;IACL,0EAA0E;IAC1E,iEAAiE;IACjE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,aAAa,GAAG,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC/E,MAAM,IAAI,UAAU,CAClB,eAAe,GAAG,wBAAwB,aAAa,kCAAkC;YACvF,iBAAiB,UAAU,eAAe,OAAO,sCAAsC;YACvF,gCAAgC,WAAW,GAAG,CAAC,OAAO,WAAW,UAAU;YAC3E,yDAAyD,CAC5D,CAAA;IACH,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAA;IAC7C,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAA;IAC1G,MAAM,IAAI,GAAG,CACX,OAAqB,EACgD,EAAE;QACvE,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAA;QAClC,MAAM,IAAI,GAAG,OAAO,IAAI,WAAW,CAAA;QACnC,OAAO,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAC9C,IAAI,CAAC,MAAM,CAAC;YACV,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;YAC1D,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YAC5C,IAAI,EAAE,CAAC,OAAO,EAAoE,EAAE,CAClF,IAAI;gBACF,CAAC,CAAC,SAAS,KAAK,aAAa;oBAC3B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;oBAC3B,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;gBACpD,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;oBAClB,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;iBACtD,CAAC,CAAC,IAAI,CACL,IAAI,CAAC,OAAO,CACV,IAAI,CAAC,EAAE,CAAC,WAAW,CAA6B,OAAO,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CACvE,CACF;SACN,CAAC,CACH,CAAA;IACH,CAAC,CAAA;IACD,2EAA2E;IAC3E,4EAA4E;IAC5E,uDAAuD;IACvD,MAAM,IAAI,GAAU,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;QACjC,OAAO,EAAE,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC;QAC1C,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,OAAO;QACd,SAAS,EAAE,WAAW;QACtB,qEAAqE;QACrE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,KAAK,4BAA4B,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;KACtG,CAAC,CAAA;IACF,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,KAAK,GAA2D,SAAS,CAAC,OAAO,CAC5F,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CACrB,MAAM,CAAC,IAAI,CACT,IAAI,aAAa,CAAC;IAChB,IAAI;IACJ,QAAQ;IACR,OAAO,EAAE,SAAS,IAAI,cAAc,QAAQ,sCAAsC;CACnF,CAAC,CACH,CACJ,CAAA","sourcesContent":["/**\n * Polling as a durable lineage: one attempt per round, a durable wait between\n * attempts, and a bound the lineage settles on.\n *\n * Smithers 0.x had a `Poller` component — a check, an interval, a backoff, an\n * attempt bound, and what to do at that bound. The pieces it was built from\n * already exist here: {@link module:Sleep.action} is the durable wait, and\n * `Flow.to` opens the next round. What was missing is the shape that puts them\n * together, and that is this module.\n *\n * A poll is a flow whose body is ONE attempt. It runs the check, and branches:\n * a satisfied check settles the lineage with the check's own output, an\n * unsatisfied one sleeps for this attempt's delay and hands off to the next\n * round with the attempt counter raised. The last attempt has no sleep and no\n * handoff — it either answers with the output it just read (`return-last`) or\n * fails {@link PollExhausted} (`fail`).\n *\n * Everything durable about it is durable for the ordinary reason. Each attempt\n * is a round with its own keyed plan nodes, so a check that already ran replays\n * from its recorded outcome instead of running again; the wait between attempts\n * is a durable timer, so a process that dies during it resumes on the round it\n * left rather than starting the poll over; and the attempt counter travels in\n * the payload, so nothing about \"which attempt is this\" lives in memory.\n *\n * The bound is stated twice on purpose. The body ends the lineage on the last\n * attempt, and `maxAttempts` is also the flow's `maxRounds` budget, so a\n * lineage that somehow opened another round is refused by the engine rather\n * than polling forever.\n *\n * @since 0.1.0\n */\n\nimport * as Node from \"@smthrs/plan/Node\"\nimport type * as Repetition from \"@smthrs/plan/Repetition\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as Flow from \"./Flow/index.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport * as Sleep from \"./Sleep.ts\"\n\n/**\n * How the wait between attempts grows.\n *\n * `fixed` waits the interval every time, `linear` waits the interval times the\n * attempt, and `exponential` doubles the interval on every attempt.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Backoff = \"fixed\" | \"linear\" | \"exponential\"\n\n/**\n * What a check reports: whether the poll is over, and the value this attempt\n * read.\n *\n * The output travels on every attempt, satisfied or not, because it is also\n * the answer a `return-last` poll settles with when it runs out of attempts.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Check<out Result> {\n readonly satisfied: boolean\n readonly output: Result\n}\n\n/**\n * The success schema a check action declares.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const CheckResult = <Result extends Schema.Top>(\n result: Result\n): Schema.Struct<{\n readonly satisfied: typeof Schema.Boolean\n readonly output: Result\n}> => Schema.Struct({ satisfied: Schema.Boolean, output: result })\n\n/**\n * A poll that used its last attempt without a satisfied check.\n *\n * It is a typed failure rather than a defect: running out of attempts is an\n * expected outcome of polling something that may never become ready, so a\n * caller recovers from it through `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class PollExhausted extends Schema.TaggedError<PollExhausted>()(\n \"@smthrs/flow/PollExhausted\",\n {\n // This wire shape freezes at 1.0.0-rc.0.\n code: Schema.Literal(\"poll_exhausted\").pipe(\n Schema.withConstructorDefault(Effect.succeed(\"poll_exhausted\"))\n ),\n poll: Schema.String,\n attempts: Schema.Number,\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the exhaustion step is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhaustedTag = \"system/poll-exhausted\"\n\n/**\n * The declared step a poll takes when its last attempt was not satisfied.\n *\n * Running out of attempts is a step rather than a bare `Effect.fail` so the\n * exhaustion is in the plan and in the journal: the round that gave up shows\n * which poll gave up and after how many attempts, exactly like every other node\n * a body drove.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const exhausted: Action.Declared<\n typeof exhaustedTag,\n Schema.Struct<{\n readonly poll: typeof Schema.String\n readonly attempts: typeof Schema.Number\n }>,\n typeof Schema.Never,\n typeof PollExhausted,\n never\n> = Action.makeSystem(exhaustedTag, {\n payload: { poll: Schema.String, attempts: Schema.Number },\n success: Schema.Never,\n error: PollExhausted,\n tier: \"sealed\"\n})\n\n/**\n * The wait before the attempt after this one, in milliseconds.\n *\n * @category combinators\n * @since 0.1.0\n */\nexport const delayMillis = (options: {\n readonly intervalMs: number\n readonly backoff: Backoff\n readonly attempt: number\n}): number =>\n // A zero interval is zero under every backoff. Multiplying it by a factor\n // that has overflowed to `Infinity` would make it `NaN`.\n options.intervalMs === 0 ? 0 : options.intervalMs *\n (options.backoff === \"fixed\" ? 1 : options.backoff === \"linear\" ? options.attempt : 2 ** (options.attempt - 1))\n\n/**\n * What a poll round can fail with.\n *\n * {@link PollExhausted} is the poll's own failure. `SleepRequestInvalid` is the\n * wait node's, declared because the wait between attempts is an ordinary\n * {@link module:Sleep.action} node and its typed refusal is part of the\n * topology a poll carries. Its codes cover a payload that names no deadline,\n * names two deadlines, or names a value that is not a deadline. {@link make}\n * refuses invalid author schedules at construction. A caller-visible round\n * payload can still carry an invalid attempt that produces an invalid derived\n * wait, so `Sleep` refuses that wait before the round parks.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Failure: Schema.Union<\n readonly [typeof PollExhausted, typeof Sleep.SleepRequestInvalid]\n> = Schema.Union([PollExhausted, Sleep.SleepRequestInvalid])\n\n/**\n * The payload schema {@link make} derives from an author's input fields: the\n * author's own fields, plus the attempt this round is.\n *\n * `attempt` is optional so a caller starts a poll with its own fields alone;\n * the first round reads it as one.\n *\n * @category models\n * @since 0.1.0\n */\nexport type PayloadSchema<Input extends Schema.Struct.Fields> = Schema.Struct<\n Input & { readonly attempt: Schema.optional<typeof Schema.Number> }\n>\n\n/**\n * The payload schema one round is opened with: the author's fields, plus the\n * attempt counter the lineage carries between them.\n *\n * @private\n */\nconst payloadSchema = <Input extends Schema.Struct.Fields>(\n input: Input,\n maxAttempts: number\n): PayloadSchema<Input> =>\n Schema.Struct({\n ...input,\n attempt: Schema.optional(\n Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(1),\n Schema.isLessThanOrEqualTo(maxAttempts)\n )\n )\n })\n\n/**\n * The attempt a round payload carries, defaulting to the first.\n *\n * @private\n */\nconst attemptOf = (payload: object): number => {\n const attempt = (payload as { readonly attempt?: unknown }).attempt\n return typeof attempt === \"number\" ? attempt : 1\n}\n\n/**\n * Rebuilds a round payload with an explicit attempt.\n *\n * The two views of one round payload — what the check reads, and what the next\n * round is opened with — are the same fields with `attempt` resolved. TypeScript\n * cannot reduce a mapped type over an unresolved generic field set, so the\n * shape is stated once here instead of at each use.\n *\n * @private\n */\nconst withAttempt = <Payload>(payload: object, attempt: number): Payload =>\n ({ ...payload, attempt }) as unknown as Payload\n\n/**\n * Declares a durable poller.\n *\n * **When to use**\n *\n * Use it wherever a run has to keep asking until something is ready — a build\n * that finishes, a deployment that goes live, a review that lands — and the\n * waiting has to survive a restart. `check` is the one thing an author writes:\n * a body fragment that reads whatever it polls and reports a {@link Check}.\n * Everything else is schedule. Under stable callback admission (the default of\n * `Interpreter.layerWithImplementations`), wrap `check` in `Node.capture` with\n * every semantic capture, including a version for imported behavior. An\n * uncaptured check keeps the poll process-local. The helper captures its own\n * callbacks using the schedule and the check's identity.\n *\n * ```ts\n * const Deployment = Poll.make(\"deploy/wait\", {\n * input: { id: Schema.String },\n * result: Schema.String,\n * intervalMs: 5_000,\n * backoff: \"exponential\",\n * maxAttempts: 8,\n * onTimeout: \"fail\",\n * check: Node.capture(\n * { action: Status.name, implementationVersion: \"deploy/status-check/v1\" },\n * ({ attempt, id }) => Status.call({ attempt, id })\n * )\n * })\n * ```\n *\n * The check may not fail. A check that can fail states what a failure means for\n * the poll — give up, or treat it as \"not ready yet\" — with `Node.catch` inside\n * the fragment, rather than leaving the poll to guess.\n *\n * A per-attempt time limit on the check itself is NOT a poll option. A plan\n * node's duration is not something the body around it can bound; that bound\n * belongs in the check's own implementation, where `DurableDeferred.raceAll`\n * races the work against a durable clock and stays replayable.\n *\n * A schedule no clock can be armed with is refused here, with a `RangeError`\n * naming the option that is wrong. `intervalMs` becomes the `millis` of a\n * {@link module:Sleep.action} node, and a wait of `Infinity`, `NaN`, or a\n * negative length arms a timer that never fires — a poll that hangs forever\n * with a plan that looks correct. The check is on the schedule, not only on the\n * interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: \"exponential\" }`\n * states three finite options and asks for an infinite wait, so the LAST wait\n * the poll can arm has to be a length too, and no longer than\n * `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer\n * records. `maxAttempts` is the lineage's round\n * budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`\n * complaint about an option the author never wrote.\n *\n * @throws A `RangeError` when `intervalMs` is not a duration a clock accepts,\n * when `maxAttempts` is not a whole number of attempts of at least one, or when\n * the interval under the declared backoff reaches a wait no clock can be armed\n * with before the budget is spent.\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <\n const Tag extends string,\n Input extends Schema.Struct.Fields,\n Result extends Schema.Top,\n R = never\n>(\n tag: Tag,\n options: {\n readonly input: Input\n readonly result: Result\n readonly check: (\n payload: Schema.Struct.Type<Input> & { readonly attempt: number }\n ) => Node.Node<Check<Result[\"Type\"]>, never, R>\n readonly intervalMs: number\n readonly maxAttempts: number\n readonly backoff?: Backoff | undefined\n readonly onTimeout?: Repetition.AtCeiling | undefined\n }\n): Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R> => {\n type Round = Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R>\n type RoundPayload = PayloadSchema<Input>[\"Type\"]\n if (Object.prototype.hasOwnProperty.call(options.input, \"attempt\")) {\n throw new TypeError(\n `Poll.make: \"${tag}\" input cannot declare the reserved \"attempt\" field. ` +\n \"Poll owns that counter across durable rounds.\"\n )\n }\n const input = Object.freeze({ ...options.input }) as Input\n const result = options.result\n const check = options.check\n const intervalMs = options.intervalMs\n const maxAttempts = options.maxAttempts\n if (!Number.isFinite(intervalMs) || intervalMs < 0) {\n throw new RangeError(\n `Poll.make: \"${tag}\" intervalMs must be a finite number of milliseconds that is not negative, ` +\n `and was ${intervalMs}. The interval is the length of a durable wait, and a wait of ` +\n \"that length never ends.\"\n )\n }\n if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {\n throw new RangeError(\n `Poll.make: \"${tag}\" maxAttempts must be a whole number of attempts of at least one, and was ` +\n `${maxAttempts}. A poll checks at least once.`\n )\n }\n const backoff = options.backoff ?? \"fixed\"\n // The wait a poll arms is not `intervalMs`. {@link delayMillis} multiplies it\n // by the backoff, so a schedule whose options are each finite can still ask\n // for a wait that is not a length — and that wait arms the same timer that\n // never fires the interval guard above exists to refuse. The last wait is the\n // one before the final attempt, because the attempt at the budget gives up\n // rather than sleeps, and the schedule never shrinks, so the last wait is\n // also the longest: checking it checks all of them.\n const longestWaitMs = maxAttempts > 1\n ? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 })\n : 0\n // A durable timer records its deadline as a safe integer of milliseconds,\n // so `Sleep` refuses a longer wait on the round that reaches it.\n if (!Number.isFinite(longestWaitMs) || longestWaitMs > Number.MAX_SAFE_INTEGER) {\n throw new RangeError(\n `Poll.make: \"${tag}\" asks for a wait of ${longestWaitMs} ms before its last attempt. An ` +\n `intervalMs of ${intervalMs} under the \"${backoff}\" backoff reaches a length no clock ` +\n `can be armed with by attempt ${maxAttempts - 1} of ${maxAttempts}. Lower ` +\n \"maxAttempts, shorten the interval, or slow the backoff.\"\n )\n }\n const onTimeout = options.onTimeout ?? \"fail\"\n const captures = { tag, intervalMs, backoff, maxAttempts, onTimeout, check: Node.functionIdentity(check) }\n const body = (\n payload: RoundPayload\n ): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type, R> => {\n const attempt = attemptOf(payload)\n const last = attempt >= maxAttempts\n return check(withAttempt(payload, attempt)).pipe(\n Node.branch({\n if: Node.capture(captures, (checked) => checked.satisfied),\n then: (checked) => Flow.done(checked.output),\n else: (checked): Node.Node<Flow.BodySuccess<Result[\"Type\"]>, typeof Failure.Type> =>\n last\n ? onTimeout === \"return-last\"\n ? Flow.done(checked.output)\n : exhausted.call({ poll: tag, attempts: attempt })\n : Sleep.action.call({\n millis: delayMillis({ intervalMs, backoff, attempt })\n }).pipe(\n Node.andThen(\n self.to(withAttempt<Parameters<Round[\"to\"]>[0]>(payload, attempt + 1))\n )\n )\n })\n )\n }\n // The body names the flow it is the body of, which is what makes one round\n // open the next. It is read when a round is planned, long after `Flow.make`\n // has returned, so the binding is initialized by then.\n const self: Round = Flow.make(tag, {\n payload: payloadSchema(input, maxAttempts),\n success: result,\n error: Failure,\n maxRounds: maxAttempts,\n // A captured wrapper must not admit a process-local check as stable.\n body: captures.check.algorithm === \"sha256-source-ephemeral/v4\" ? body : Node.capture(captures, body)\n })\n return self\n}\n\n/**\n * The exhaustion implementation, provided beside the other action layers a\n * poll's rounds call.\n *\n * ```ts\n * Layer.mergeAll(Poll.layer, Sleep.layer, Interpreter.layer(Deployment)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = exhausted.toLayer(\n ({ attempts, poll }) =>\n Effect.fail(\n new PollExhausted({\n poll,\n attempts,\n message: `Poll \"${poll}\" used all ${attempts} attempts without a satisfied check.`\n })\n )\n)\n"]}
|
package/dist/esm/Sleep.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sleep.d.ts","sourceRoot":"","sources":["../../src/Sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAG5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;;;;;AAG/D;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,SAAQ,wBAMxC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,GAAG,iBAAiB,CAAA;AAEjC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,QAAQ,CAClC,OAAO,GAAG,EACV,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CAC/C,CAAC,EACF,OAAO,MAAM,CAAC,IAAI,EAClB,OAAO,mBAAmB,EAC1B,KAAK,CASL,CAAA;
|
|
1
|
+
{"version":3,"file":"Sleep.d.ts","sourceRoot":"","sources":["../../src/Sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAG5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;;;;;AAG/D;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,SAAQ,wBAMxC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,GAAG,iBAAiB,CAAA;AAEjC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,QAAQ,CAClC,OAAO,GAAG,EACV,MAAM,CAAC,MAAM,CAAC;IACZ,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CAC/C,CAAC,EACF,OAAO,MAAM,CAAC,IAAI,EAClB,OAAO,mBAAmB,EAC1B,KAAK,CASL,CAAA;AA0IF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,CAuCxE,CAAA"}
|
package/dist/esm/Sleep.js
CHANGED
|
@@ -83,6 +83,27 @@ export const action = Action.makeSystem(tag, {
|
|
|
83
83
|
error: SleepRequestInvalid,
|
|
84
84
|
tier: "sealed"
|
|
85
85
|
});
|
|
86
|
+
/**
|
|
87
|
+
* The latest deadline a sleep can arm, in epoch milliseconds.
|
|
88
|
+
*
|
|
89
|
+
* A durable store records `wakeAt` and a clock's due time as a non-negative
|
|
90
|
+
* safe integer. A finite deadline past this one passed every other check and
|
|
91
|
+
* then failed the park write, so it is refused with the payload's other
|
|
92
|
+
* invalid deadlines instead.
|
|
93
|
+
*
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
const latestDeadline = Number.MAX_SAFE_INTEGER;
|
|
97
|
+
/**
|
|
98
|
+
* The refusal for a deadline later than {@link latestDeadline}.
|
|
99
|
+
*
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
const tooLate = (stated) => new SleepRequestInvalid({
|
|
103
|
+
code: "invalid_deadline",
|
|
104
|
+
message: `${tag} was called with ${stated}, which ends after ${latestDeadline}, the latest epoch ` +
|
|
105
|
+
"millisecond a durable timer records."
|
|
106
|
+
});
|
|
86
107
|
/**
|
|
87
108
|
* The absolute deadline a payload names, in epoch milliseconds.
|
|
88
109
|
*
|
|
@@ -125,16 +146,21 @@ const deadlineOf = (payload, now) => {
|
|
|
125
146
|
`${deadline}. A deadline is a finite epoch time in milliseconds.`
|
|
126
147
|
}));
|
|
127
148
|
}
|
|
128
|
-
return
|
|
149
|
+
return Math.ceil(deadline) > latestDeadline
|
|
150
|
+
? Effect.fail(tooLate(`millis ${payload.millis} at current time ${now}`))
|
|
151
|
+
: Effect.succeed(deadline);
|
|
129
152
|
}
|
|
130
153
|
if (payload.until !== undefined) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: Effect.fail(new SleepRequestInvalid({
|
|
154
|
+
if (!Number.isFinite(payload.until)) {
|
|
155
|
+
return Effect.fail(new SleepRequestInvalid({
|
|
134
156
|
code: "invalid_deadline",
|
|
135
157
|
message: `${tag} was called with until ${payload.until}. A deadline is a finite epoch time in milliseconds; ` +
|
|
136
158
|
"the sleep must end at a real point in time."
|
|
137
159
|
}));
|
|
160
|
+
}
|
|
161
|
+
return Math.ceil(payload.until) > latestDeadline
|
|
162
|
+
? Effect.fail(tooLate(`until ${payload.until}`))
|
|
163
|
+
: Effect.succeed(payload.until);
|
|
138
164
|
}
|
|
139
165
|
return Effect.fail(new SleepRequestInvalid({
|
|
140
166
|
code: "missing_deadline",
|
package/dist/esm/Sleep.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sleep.js","sourceRoot":"","sources":["../../src/Sleep.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The system timer, as an ordinary declared action.\n *\n * `docs/concepts/flows-and-actions.md` names the timer primitive a\n * gap and says how to close it: ship it over the existing `DurableClock`, so a\n * wait is a visible, keyed plan node rather than a second execution mechanism.\n * That is this module. {@link action} is a plain `Action.make` declaration —\n * it appears in a catalog, `.call()` records a node, and `Graph.build` puts that\n * node in the plan beside every other step — and {@link layer} is the\n * implementation the hosts that can run a timer provide.\n *\n * The wait itself is `DurableClock`'s, not a new one. The implementation arms\n * the durable clock and awaits its deferred, which is what parks the execution;\n * the park is declared through the ordinary waiting vocabulary\n * ({@link module:WaitingAnnotation.annotateWaiting}) under `timer`, with the\n * deadline as `wakeAt`, so a durable driver records the same waiting row it\n * records for a hand-written `DurableClock.sleep`. Replay is the deferred's:\n * once the clock has fired, its result is persisted, so a re-driven round reads\n * it and runs straight through instead of parking a second time.\n *\n * Which clock a wait arms is the dispatch's own, read from\n * {@link module:Context.CurrentInvocationKey}: a keyed node already owns an\n * identity that survives replay and separates two identical calls, so the timer\n * inherits it rather than being addressed by the payload it was called with.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as DurableClock from \"./DurableClock.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport { annotateWaiting } from \"./FlowRuntime/WaitingAnnotation.ts\"\n\n/**\n * A sleep whose payload does not name exactly one deadline.\n *\n * A payload carries a relative `millis` or an absolute `until`, and naming\n * both, or neither, leaves no deadline to arm a clock with. It is a typed\n * failure rather than a defect because the payload of a driven node is\n * assembled from plan values, so an author's mistake is recoverable through\n * `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SleepRequestInvalid extends Schema.TaggedError<SleepRequestInvalid>()(\n \"@smthrs/flow/SleepRequestInvalid\",\n {\n code: Schema.Literals([\"missing_deadline\", \"ambiguous_deadline\", \"invalid_deadline\"]),\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the sleep declaration is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const tag = \"system/sleep\"\n\n/**\n * The declared sleep action: `millis` from now, or an absolute `until`.\n *\n * **When to use**\n *\n * Use it in a body wherever a round has to wait for time to pass —\n * `Sleep.action.call({ millis: 60_000 })` — rather than reaching for\n * `DurableClock.sleep`, which is only callable from a handler and leaves\n * nothing in the plan. A sleep node is keyed, inspectable, and skippable by a\n * branch exactly like an action that does work.\n *\n * Two waits of the same length are two waits: `sleep(1h)` followed by\n * `sleep(1h)` sleeps for two hours, because each call is its own node with its\n * own identity, exactly as two calls of any other declaration are.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const action: Action.Declared<\n typeof tag,\n Schema.Struct<{\n readonly millis: Schema.optional<Schema.Number>\n readonly until: Schema.optional<Schema.Number>\n }>,\n typeof Schema.Void,\n typeof SleepRequestInvalid,\n never\n> = Action.makeSystem(tag, {\n payload: {\n millis: Schema.optional(Schema.Number),\n until: Schema.optional(Schema.Number)\n },\n success: Schema.Void,\n error: SleepRequestInvalid,\n tier: \"sealed\"\n})\n\n/**\n * The absolute deadline a payload names, in epoch milliseconds.\n *\n * DECIDED: a payload names EXACTLY one of `millis`\n * and `until`, and both other shapes are refused rather than resolved by\n * precedence. A precedence rule would silently ignore half of a payload an\n * author believed in, and the two fields are the same fact stated two ways, so\n * there is no shape where carrying both is meaningful.\n *\n * `Duration.millis` accepts every number, so an unchecked deadline changes the\n * sleep instead of refusing it. `NaN` becomes zero, a negative relative delay\n * has already passed, and `Infinity` never ends. The implementation rejects\n * those values before it annotates the instance or arms a clock. A finite\n * absolute `until` remains legal when it is in the past because that sleep can\n * settle immediately.\n *\n * @private\n */\nconst deadlineOf = (\n payload: { readonly millis?: number | undefined; readonly until?: number | undefined },\n now: number\n): Effect.Effect<number, SleepRequestInvalid> => {\n if (payload.millis !== undefined && payload.until !== undefined) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"ambiguous_deadline\",\n message: `${tag} was called with both \"millis\" and \"until\". A sleep names one deadline: ` +\n \"a relative duration, or an absolute epoch time.\"\n })\n )\n }\n if (payload.millis !== undefined) {\n if (!Number.isFinite(payload.millis) || payload.millis < 0) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis}. A deadline is a finite number of ` +\n \"milliseconds that is not negative; the sleep would be over before it started, or never over at all.\"\n })\n )\n }\n const deadline = now + payload.millis\n if (!Number.isFinite(deadline)) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis} at current time ${now}, which produces ` +\n `${deadline}. A deadline is a finite epoch time in milliseconds.`\n })\n )\n }\n return Effect.succeed(deadline)\n }\n if (payload.until !== undefined) {\n return Number.isFinite(payload.until)\n ? Effect.succeed(payload.until)\n : Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message:\n `${tag} was called with until ${payload.until}. A deadline is a finite epoch time in milliseconds; ` +\n \"the sleep must end at a real point in time.\"\n })\n )\n }\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"missing_deadline\",\n message: `${tag} was called with neither \"millis\" nor \"until\", so there is no deadline to wait for.`\n })\n )\n}\n\n/**\n * The durable clock the dispatch currently executing arms.\n *\n * DECIDED: the name is derived from the ENGINE's\n * per-dispatch identity — the invocation key it already allocated — rather than\n * from the payload. A durable clock's deferred is addressed by (execution,\n * name), so the name has to be stable across replays of one node and distinct\n * between two identical nodes, and the invocation key is exactly that: it is\n * allocated on every dispatch including a replayed one, because the driver\n * addresses the recorded outcome by it. A payload-derived name has only the\n * first property — `sleep(1h); work(); sleep(1h)` would address ONE wake and\n * sleep once — and closing that with a discriminator in the payload would make\n * an author carry identity that a keyed plan node already has.\n *\n * DECIDED: a runtime that supplies no dispatch\n * identity is refused as a defect rather than falling back to the payload. The\n * fallback is unobservable in the one-wait case and silently collapses two\n * waits in every other, so it would trade a wiring error a host fixes once for\n * a run that quietly sleeps for half as long as it declared.\n *\n * @private\n */\nconst clockName: Effect.Effect<string> = Effect.gen(function*() {\n const invocation = yield* Action.CurrentInvocationKey\n if (invocation === undefined) {\n return yield* Effect.die(\n `${tag} ran under a runtime that supplies no Action.CurrentInvocationKey. ` +\n \"A durable timer is named by the dispatch that armed it, so a runtime that does not \" +\n \"identify the dispatch cannot arm one.\"\n )\n }\n return `Sleep/${invocation}`\n})\n\n/**\n * The sleep implementation: arm the durable clock, park under `timer`, wake.\n *\n * Provide it beside the other action implementation layers a body calls, over\n * the {@link module:Implementations.layerImplementations} table they file\n * themselves in:\n *\n * ```ts\n * Layer.mergeAll(Sleep.layer, Interpreter.layer(Pipeline)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = action.toLayer((payload) =>\n Effect.gen(function*() {\n // Whole milliseconds, both of them. A durable deadline lands in an INTEGER\n // column whose CHECK constraint refuses anything else\n // (`flows_runs.waiting_wake_at_ms`, `flows_clock_deadlines.due_at_ms`), and\n // `Clock.currentTimeMillis` is not integral under every clock: a test clock\n // advanced by a fractional-millisecond sleep reports a fractional now, and\n // a fractional `millis` or `until` in the payload does the same. The\n // unrounded deadline reached the park write as a raw SQL constraint\n // failure, which killed the drive and left the run `running` with nothing\n // left to wake it. `now` floors, and the deadline ceils so a rounded sleep\n // never ends before the instant it was asked to wait for.\n const now = Math.floor(yield* Clock.currentTimeMillis)\n const wakeAt = Math.ceil(yield* deadlineOf(payload, now))\n const remaining = wakeAt - now\n // DECIDED: a deadline that has already passed\n // settles the node instead of parking it. A durable wait exists to survive\n // the gap until a deadline, and a run that resumes after its own deadline\n // has to make progress rather than park again on a timer that can only fire\n // in the past.\n if (remaining <= 0) return\n const name = yield* clockName\n // DECIDED: the park is declared rather than\n // left to a driver's derivation, so a host with no clock table still parks\n // under `timer`. A `millis` deadline is recomputed from the current clock,\n // so a round re-driven before its timer fires re-declares a `wakeAt` later\n // than the armed one; the wake itself is unaffected, because the durable\n // clock row is keyed by name and keeps the deadline it was first armed\n // with, and `wakeAt` is a sweeper hint rather than the wake.\n yield* annotateWaiting({ reason: \"timer\", wakeAt })\n yield* DurableClock.sleep({\n name,\n duration: Duration.millis(remaining),\n // The wait is the point: a sleep called as a plan node parks the\n // execution rather than holding a fiber open, so the in-memory shortcut\n // is disabled instead of being left at its 60-second default.\n inMemoryThreshold: Duration.zero\n })\n })\n)\n"]}
|
|
1
|
+
{"version":3,"file":"Sleep.js","sourceRoot":"","sources":["../../src/Sleep.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The system timer, as an ordinary declared action.\n *\n * `docs/concepts/flows-and-actions.md` names the timer primitive a\n * gap and says how to close it: ship it over the existing `DurableClock`, so a\n * wait is a visible, keyed plan node rather than a second execution mechanism.\n * That is this module. {@link action} is a plain `Action.make` declaration —\n * it appears in a catalog, `.call()` records a node, and `Graph.build` puts that\n * node in the plan beside every other step — and {@link layer} is the\n * implementation the hosts that can run a timer provide.\n *\n * The wait itself is `DurableClock`'s, not a new one. The implementation arms\n * the durable clock and awaits its deferred, which is what parks the execution;\n * the park is declared through the ordinary waiting vocabulary\n * ({@link module:WaitingAnnotation.annotateWaiting}) under `timer`, with the\n * deadline as `wakeAt`, so a durable driver records the same waiting row it\n * records for a hand-written `DurableClock.sleep`. Replay is the deferred's:\n * once the clock has fired, its result is persisted, so a re-driven round reads\n * it and runs straight through instead of parking a second time.\n *\n * Which clock a wait arms is the dispatch's own, read from\n * {@link module:Context.CurrentInvocationKey}: a keyed node already owns an\n * identity that survives replay and separates two identical calls, so the timer\n * inherits it rather than being addressed by the payload it was called with.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Action from \"./Action/index.ts\"\nimport * as DurableClock from \"./DurableClock.ts\"\nimport type { FlowRuntime } from \"./FlowRuntime/FlowRuntime.ts\"\nimport { annotateWaiting } from \"./FlowRuntime/WaitingAnnotation.ts\"\n\n/**\n * A sleep whose payload does not name exactly one deadline.\n *\n * A payload carries a relative `millis` or an absolute `until`, and naming\n * both, or neither, leaves no deadline to arm a clock with. It is a typed\n * failure rather than a defect because the payload of a driven node is\n * assembled from plan values, so an author's mistake is recoverable through\n * `Node.catch` like any other declared failure.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SleepRequestInvalid extends Schema.TaggedError<SleepRequestInvalid>()(\n \"@smthrs/flow/SleepRequestInvalid\",\n {\n code: Schema.Literals([\"missing_deadline\", \"ambiguous_deadline\", \"invalid_deadline\"]),\n message: Schema.String\n }\n) {}\n\n/**\n * The tag the sleep declaration is catalogued and resolved under.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const tag = \"system/sleep\"\n\n/**\n * The declared sleep action: `millis` from now, or an absolute `until`.\n *\n * **When to use**\n *\n * Use it in a body wherever a round has to wait for time to pass —\n * `Sleep.action.call({ millis: 60_000 })` — rather than reaching for\n * `DurableClock.sleep`, which is only callable from a handler and leaves\n * nothing in the plan. A sleep node is keyed, inspectable, and skippable by a\n * branch exactly like an action that does work.\n *\n * Two waits of the same length are two waits: `sleep(1h)` followed by\n * `sleep(1h)` sleeps for two hours, because each call is its own node with its\n * own identity, exactly as two calls of any other declaration are.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const action: Action.Declared<\n typeof tag,\n Schema.Struct<{\n readonly millis: Schema.optional<Schema.Number>\n readonly until: Schema.optional<Schema.Number>\n }>,\n typeof Schema.Void,\n typeof SleepRequestInvalid,\n never\n> = Action.makeSystem(tag, {\n payload: {\n millis: Schema.optional(Schema.Number),\n until: Schema.optional(Schema.Number)\n },\n success: Schema.Void,\n error: SleepRequestInvalid,\n tier: \"sealed\"\n})\n\n/**\n * The latest deadline a sleep can arm, in epoch milliseconds.\n *\n * A durable store records `wakeAt` and a clock's due time as a non-negative\n * safe integer. A finite deadline past this one passed every other check and\n * then failed the park write, so it is refused with the payload's other\n * invalid deadlines instead.\n *\n * @private\n */\nconst latestDeadline = Number.MAX_SAFE_INTEGER\n\n/**\n * The refusal for a deadline later than {@link latestDeadline}.\n *\n * @private\n */\nconst tooLate = (stated: string): SleepRequestInvalid =>\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with ${stated}, which ends after ${latestDeadline}, the latest epoch ` +\n \"millisecond a durable timer records.\"\n })\n\n/**\n * The absolute deadline a payload names, in epoch milliseconds.\n *\n * DECIDED: a payload names EXACTLY one of `millis`\n * and `until`, and both other shapes are refused rather than resolved by\n * precedence. A precedence rule would silently ignore half of a payload an\n * author believed in, and the two fields are the same fact stated two ways, so\n * there is no shape where carrying both is meaningful.\n *\n * `Duration.millis` accepts every number, so an unchecked deadline changes the\n * sleep instead of refusing it. `NaN` becomes zero, a negative relative delay\n * has already passed, and `Infinity` never ends. The implementation rejects\n * those values before it annotates the instance or arms a clock. A finite\n * absolute `until` remains legal when it is in the past because that sleep can\n * settle immediately.\n *\n * @private\n */\nconst deadlineOf = (\n payload: { readonly millis?: number | undefined; readonly until?: number | undefined },\n now: number\n): Effect.Effect<number, SleepRequestInvalid> => {\n if (payload.millis !== undefined && payload.until !== undefined) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"ambiguous_deadline\",\n message: `${tag} was called with both \"millis\" and \"until\". A sleep names one deadline: ` +\n \"a relative duration, or an absolute epoch time.\"\n })\n )\n }\n if (payload.millis !== undefined) {\n if (!Number.isFinite(payload.millis) || payload.millis < 0) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis}. A deadline is a finite number of ` +\n \"milliseconds that is not negative; the sleep would be over before it started, or never over at all.\"\n })\n )\n }\n const deadline = now + payload.millis\n if (!Number.isFinite(deadline)) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message: `${tag} was called with millis ${payload.millis} at current time ${now}, which produces ` +\n `${deadline}. A deadline is a finite epoch time in milliseconds.`\n })\n )\n }\n return Math.ceil(deadline) > latestDeadline\n ? Effect.fail(tooLate(`millis ${payload.millis} at current time ${now}`))\n : Effect.succeed(deadline)\n }\n if (payload.until !== undefined) {\n if (!Number.isFinite(payload.until)) {\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"invalid_deadline\",\n message:\n `${tag} was called with until ${payload.until}. A deadline is a finite epoch time in milliseconds; ` +\n \"the sleep must end at a real point in time.\"\n })\n )\n }\n return Math.ceil(payload.until) > latestDeadline\n ? Effect.fail(tooLate(`until ${payload.until}`))\n : Effect.succeed(payload.until)\n }\n return Effect.fail(\n new SleepRequestInvalid({\n code: \"missing_deadline\",\n message: `${tag} was called with neither \"millis\" nor \"until\", so there is no deadline to wait for.`\n })\n )\n}\n\n/**\n * The durable clock the dispatch currently executing arms.\n *\n * DECIDED: the name is derived from the ENGINE's\n * per-dispatch identity — the invocation key it already allocated — rather than\n * from the payload. A durable clock's deferred is addressed by (execution,\n * name), so the name has to be stable across replays of one node and distinct\n * between two identical nodes, and the invocation key is exactly that: it is\n * allocated on every dispatch including a replayed one, because the driver\n * addresses the recorded outcome by it. A payload-derived name has only the\n * first property — `sleep(1h); work(); sleep(1h)` would address ONE wake and\n * sleep once — and closing that with a discriminator in the payload would make\n * an author carry identity that a keyed plan node already has.\n *\n * DECIDED: a runtime that supplies no dispatch\n * identity is refused as a defect rather than falling back to the payload. The\n * fallback is unobservable in the one-wait case and silently collapses two\n * waits in every other, so it would trade a wiring error a host fixes once for\n * a run that quietly sleeps for half as long as it declared.\n *\n * @private\n */\nconst clockName: Effect.Effect<string> = Effect.gen(function*() {\n const invocation = yield* Action.CurrentInvocationKey\n if (invocation === undefined) {\n return yield* Effect.die(\n `${tag} ran under a runtime that supplies no Action.CurrentInvocationKey. ` +\n \"A durable timer is named by the dispatch that armed it, so a runtime that does not \" +\n \"identify the dispatch cannot arm one.\"\n )\n }\n return `Sleep/${invocation}`\n})\n\n/**\n * The sleep implementation: arm the durable clock, park under `timer`, wake.\n *\n * Provide it beside the other action implementation layers a body calls, over\n * the {@link module:Implementations.layerImplementations} table they file\n * themselves in:\n *\n * ```ts\n * Layer.mergeAll(Sleep.layer, Interpreter.layer(Pipeline)).pipe(\n * Layer.provideMerge(Action.layerImplementations)\n * )\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<never, never, Crypto.Crypto | FlowRuntime> = action.toLayer((payload) =>\n Effect.gen(function*() {\n // Whole milliseconds, both of them. A durable deadline lands in an INTEGER\n // column whose CHECK constraint refuses anything else\n // (`flows_runs.waiting_wake_at_ms`, `flows_clock_deadlines.due_at_ms`), and\n // `Clock.currentTimeMillis` is not integral under every clock: a test clock\n // advanced by a fractional-millisecond sleep reports a fractional now, and\n // a fractional `millis` or `until` in the payload does the same. The\n // unrounded deadline reached the park write as a raw SQL constraint\n // failure, which killed the drive and left the run `running` with nothing\n // left to wake it. `now` floors, and the deadline ceils so a rounded sleep\n // never ends before the instant it was asked to wait for.\n const now = Math.floor(yield* Clock.currentTimeMillis)\n const wakeAt = Math.ceil(yield* deadlineOf(payload, now))\n const remaining = wakeAt - now\n // DECIDED: a deadline that has already passed\n // settles the node instead of parking it. A durable wait exists to survive\n // the gap until a deadline, and a run that resumes after its own deadline\n // has to make progress rather than park again on a timer that can only fire\n // in the past.\n if (remaining <= 0) return\n const name = yield* clockName\n // DECIDED: the park is declared rather than\n // left to a driver's derivation, so a host with no clock table still parks\n // under `timer`. A `millis` deadline is recomputed from the current clock,\n // so a round re-driven before its timer fires re-declares a `wakeAt` later\n // than the armed one; the wake itself is unaffected, because the durable\n // clock row is keyed by name and keeps the deadline it was first armed\n // with, and `wakeAt` is a sweeper hint rather than the wake.\n yield* annotateWaiting({ reason: \"timer\", wakeAt })\n yield* DurableClock.sleep({\n name,\n duration: Duration.millis(remaining),\n // The wait is the point: a sleep called as a plan node parks the\n // execution rather than holding a fiber open, so the in-memory shortcut\n // is disabled instead of being left at its 60-second default.\n inMemoryThreshold: Duration.zero\n })\n })\n)\n"]}
|
package/docs/README.md
CHANGED
|
@@ -36,7 +36,9 @@ restarted. Skip it when a plain `Effect` retry loop covers the whole problem.
|
|
|
36
36
|
|
|
37
37
|
## Install
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
```bash
|
|
40
|
+
npm install @smthrs/flow@next effect
|
|
41
|
+
```
|
|
40
42
|
|
|
41
43
|
Node.js 26.4.0 or later. [Installation](./installation.md) covers the import
|
|
42
44
|
forms, and what each companion package supplies.
|
package/docs/api.md
CHANGED
|
@@ -219,11 +219,11 @@ Sleep.action.call({ millis: 60_000 })
|
|
|
219
219
|
|
|
220
220
|
A payload names exactly one deadline: a relative `millis`, or an absolute `until` in epoch milliseconds. `SleepRequestInvalid` is the typed refusal, with three codes:
|
|
221
221
|
|
|
222
|
-
| Code | Meaning
|
|
223
|
-
| -------------------- |
|
|
224
|
-
| `missing_deadline` | the payload named neither `millis` nor `until`
|
|
225
|
-
| `ambiguous_deadline` | it named both, which are the same fact stated two ways
|
|
226
|
-
| `invalid_deadline` | it named a number that is not a length of time: a non-finite or negative `millis`, a non-finite `until`,
|
|
222
|
+
| Code | Meaning |
|
|
223
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `missing_deadline` | the payload named neither `millis` nor `until` |
|
|
225
|
+
| `ambiguous_deadline` | it named both, which are the same fact stated two ways |
|
|
226
|
+
| `invalid_deadline` | it named a number that is not a length of time: a non-finite or negative `millis`, a non-finite `until`, an addition that overflows, or a deadline after epoch millisecond `Number.MAX_SAFE_INTEGER` |
|
|
227
227
|
|
|
228
228
|
Two waits of the same length are two waits: each call is its own node with its own identity, so `sleep` followed by `sleep` waits twice. A deadline that has already passed settles the node instead of parking it, because a run that resumes after its own deadline has to make progress.
|
|
229
229
|
|
|
@@ -254,7 +254,7 @@ yield * DurableDeferred.succeed(gate, { token, value: { approved: true } })
|
|
|
254
254
|
|
|
255
255
|
`delayMillis` is the exported schedule function, `CheckResult(result)` is the success schema a check action declares, `Failure` is the union a poll's rounds can fail with, and `Poll.layer` implements the `system/poll-exhausted` step. `PollExhausted` carries the stable code `poll_exhausted`.
|
|
256
256
|
|
|
257
|
-
`Poll.make` refuses a schedule no clock can keep, with a `RangeError` naming the option that is wrong: an `intervalMs` that is not finite or is negative becomes a `system/sleep` node whose timer never fires, and a `maxAttempts` below one whole attempt reaches `Flow.make` as a complaint about `maxRounds`, an option the author never wrote. The check is on the schedule rather than on the interval alone, because `delayMillis` multiplies the interval by the backoff: `{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }` states three finite options and still asks for a wait of `Infinity`. The last wait a poll can arm, the one before the final attempt, has to be a length too, since the attempt at the budget gives up rather than sleeps.
|
|
257
|
+
`Poll.make` refuses a schedule no clock can keep, with a `RangeError` naming the option that is wrong: an `intervalMs` that is not finite or is negative becomes a `system/sleep` node whose timer never fires, and a `maxAttempts` below one whole attempt reaches `Flow.make` as a complaint about `maxRounds`, an option the author never wrote. The check is on the schedule rather than on the interval alone, because `delayMillis` multiplies the interval by the backoff: `{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }` states three finite options and still asks for a wait of `Infinity`. The last wait a poll can arm, the one before the final attempt, has to be a length too, since the attempt at the budget gives up rather than sleeps, and no longer than `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer records.
|
|
258
258
|
|
|
259
259
|
### Bounding a check that can hang
|
|
260
260
|
|
|
@@ -19,7 +19,7 @@ engine, database, or jj: planning reads declarations only. They import
|
|
|
19
19
|
add both at the version of `@smthrs/flows` you have:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install @smthrs/flow@1.0.0-rc.
|
|
22
|
+
npm install @smthrs/flow@1.0.0-rc.8 @smthrs/plan@1.0.0-rc.8
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
`@smthrs/flows` re-exports the same modules, so `import { Graph } from "@smthrs/flows"`
|
|
@@ -107,7 +107,9 @@ because the backoff multiplies the interval:
|
|
|
107
107
|
`{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }` states three
|
|
108
108
|
finite options and still asks for a wait of `Infinity`. The check looks at the
|
|
109
109
|
last wait a poll can arm, the one before the final attempt, since the attempt at
|
|
110
|
-
the budget gives up rather than sleeps.
|
|
110
|
+
the budget gives up rather than sleeps. That wait must also be at most
|
|
111
|
+
`Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer
|
|
112
|
+
records, so a one-millisecond interval doubling allows at most 54 attempts.
|
|
111
113
|
|
|
112
114
|
## Handle exhaustion
|
|
113
115
|
|
|
@@ -62,11 +62,11 @@ milliseconds, and exactly one of them. `Sleep.SleepRequestInvalid` is the typed
|
|
|
62
62
|
refusal, so a body can recover from it with `Node.catch` like any other declared
|
|
63
63
|
failure:
|
|
64
64
|
|
|
65
|
-
| Code | What was wrong
|
|
66
|
-
| -------------------- |
|
|
67
|
-
| `missing_deadline` | The payload named neither `millis` nor `until`.
|
|
68
|
-
| `ambiguous_deadline` | It named both, which are the same fact stated twice.
|
|
69
|
-
| `invalid_deadline` | It named a number that is not a length of time: a non-finite or negative `millis`, a non-finite `until`,
|
|
65
|
+
| Code | What was wrong |
|
|
66
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `missing_deadline` | The payload named neither `millis` nor `until`. |
|
|
68
|
+
| `ambiguous_deadline` | It named both, which are the same fact stated twice. |
|
|
69
|
+
| `invalid_deadline` | It named a number that is not a length of time: a non-finite or negative `millis`, a non-finite `until`, an addition that overflows, or a deadline after epoch millisecond `Number.MAX_SAFE_INTEGER`. |
|
|
70
70
|
|
|
71
71
|
Two behaviors are worth knowing before you write the body:
|
|
72
72
|
|
package/docs/installation.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Installation"
|
|
3
|
-
description: "Install @smthrs/flow, its runtime requirements and effect range, its import forms, and the packages a runnable composition adds."
|
|
3
|
+
description: "Install @smthrs/flow from npm, its runtime requirements and effect range, its import forms, and the packages a runnable composition adds."
|
|
4
4
|
sidebar:
|
|
5
5
|
order: 1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Install the package
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install @smthrs/flow@next effect
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The `next` tag carries the 1.0 release candidates.
|
|
11
15
|
|
|
12
16
|
`effect` is a peer dependency of this package, in the range `^4.0.4`. Add one
|
|
13
17
|
`effect` in that range to your own dependencies: your declarations import
|
|
@@ -23,17 +27,18 @@ from, and the canonical JSON checks applied to values.
|
|
|
23
27
|
|
|
24
28
|
## Use the libraries
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
Every `@smthrs` library installs from npm the same way. Name each package your
|
|
31
|
+
code imports with the `next` tag, and add one `effect` in the `^4.0.4` range
|
|
32
|
+
the libraries peer. The [Quickstart](./quickstart.md) program needs these:
|
|
29
33
|
|
|
30
34
|
```bash
|
|
31
|
-
|
|
32
|
-
echo '{ "type": "module" }' > package.json
|
|
33
|
-
ln -s /path/to/smithers/examples/node_modules node_modules
|
|
35
|
+
npm install @smthrs/flow@next @smthrs/engine@next @smthrs/plan@next @effect/platform-node effect
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
`next` resolves every `@smthrs` package to the same release candidate. With
|
|
39
|
+
pnpm 11, name that version instead of the tag; the
|
|
40
|
+
[Flows installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/docs/installation.md)
|
|
41
|
+
explains why.
|
|
37
42
|
|
|
38
43
|
## Import forms
|
|
39
44
|
|
package/docs/quickstart.md
CHANGED
|
@@ -15,7 +15,11 @@ A runnable copy of this program is published in the Smithers examples,
|
|
|
15
15
|
## Prerequisites
|
|
16
16
|
|
|
17
17
|
- Node.js 26.4.0 or later.
|
|
18
|
-
- A
|
|
18
|
+
- A project with the packages installed:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @smthrs/flow@next @smthrs/engine@next @smthrs/plan@next @effect/platform-node effect
|
|
22
|
+
```
|
|
19
23
|
|
|
20
24
|
## Declare the action and the flow
|
|
21
25
|
|
package/docs/reference/flow.md
CHANGED
|
@@ -5,7 +5,11 @@ area: api
|
|
|
5
5
|
order: 10
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Install the package with its `effect` peer
|
|
8
|
+
Install the package with its `effect` peer:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @smthrs/flow@next effect
|
|
12
|
+
```
|
|
9
13
|
|
|
10
14
|
## Entry points
|
|
11
15
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/flow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.8",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "engine"
|
|
@@ -289,11 +289,11 @@
|
|
|
289
289
|
"tag": "next"
|
|
290
290
|
},
|
|
291
291
|
"dependencies": {
|
|
292
|
-
"@smthrs/crypto": "1.0.0-rc.
|
|
293
|
-
"@smthrs/keys": "1.0.0-rc.
|
|
294
|
-
"@smthrs/plan": "1.0.0-rc.
|
|
295
|
-
"@smthrs/canonical": "1.0.0-rc.
|
|
296
|
-
"@smthrs/capability": "1.0.0-rc.
|
|
292
|
+
"@smthrs/crypto": "1.0.0-rc.8",
|
|
293
|
+
"@smthrs/keys": "1.0.0-rc.8",
|
|
294
|
+
"@smthrs/plan": "1.0.0-rc.8",
|
|
295
|
+
"@smthrs/canonical": "1.0.0-rc.8",
|
|
296
|
+
"@smthrs/capability": "1.0.0-rc.8"
|
|
297
297
|
},
|
|
298
298
|
"devDependencies": {
|
|
299
299
|
"@effect/language-service": "0.87.2",
|
package/src/Poll.ts
CHANGED
|
@@ -149,8 +149,10 @@ export const delayMillis = (options: {
|
|
|
149
149
|
readonly backoff: Backoff
|
|
150
150
|
readonly attempt: number
|
|
151
151
|
}): number =>
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
// A zero interval is zero under every backoff. Multiplying it by a factor
|
|
153
|
+
// that has overflowed to `Infinity` would make it `NaN`.
|
|
154
|
+
options.intervalMs === 0 ? 0 : options.intervalMs *
|
|
155
|
+
(options.backoff === "fixed" ? 1 : options.backoff === "linear" ? options.attempt : 2 ** (options.attempt - 1))
|
|
154
156
|
|
|
155
157
|
/**
|
|
156
158
|
* What a poll round can fail with.
|
|
@@ -274,7 +276,9 @@ const withAttempt = <Payload>(payload: object, attempt: number): Payload =>
|
|
|
274
276
|
* with a plan that looks correct. The check is on the schedule, not only on the
|
|
275
277
|
* interval: `{ intervalMs: 1000, maxAttempts: 2000, backoff: "exponential" }`
|
|
276
278
|
* states three finite options and asks for an infinite wait, so the LAST wait
|
|
277
|
-
* the poll can arm has to be a length too
|
|
279
|
+
* the poll can arm has to be a length too, and no longer than
|
|
280
|
+
* `Number.MAX_SAFE_INTEGER` milliseconds, the latest deadline a durable timer
|
|
281
|
+
* records. `maxAttempts` is the lineage's round
|
|
278
282
|
* budget, and a budget below one attempt reaches `Flow.make` as a `maxRounds`
|
|
279
283
|
* complaint about an option the author never wrote.
|
|
280
284
|
*
|
|
@@ -341,7 +345,9 @@ export const make = <
|
|
|
341
345
|
const longestWaitMs = maxAttempts > 1
|
|
342
346
|
? delayMillis({ intervalMs, backoff, attempt: maxAttempts - 1 })
|
|
343
347
|
: 0
|
|
344
|
-
|
|
348
|
+
// A durable timer records its deadline as a safe integer of milliseconds,
|
|
349
|
+
// so `Sleep` refuses a longer wait on the round that reaches it.
|
|
350
|
+
if (!Number.isFinite(longestWaitMs) || longestWaitMs > Number.MAX_SAFE_INTEGER) {
|
|
345
351
|
throw new RangeError(
|
|
346
352
|
`Poll.make: "${tag}" asks for a wait of ${longestWaitMs} ms before its last attempt. An ` +
|
|
347
353
|
`intervalMs of ${intervalMs} under the "${backoff}" backoff reaches a length no clock ` +
|
package/src/Sleep.ts
CHANGED
|
@@ -102,6 +102,30 @@ export const action: Action.Declared<
|
|
|
102
102
|
tier: "sealed"
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The latest deadline a sleep can arm, in epoch milliseconds.
|
|
107
|
+
*
|
|
108
|
+
* A durable store records `wakeAt` and a clock's due time as a non-negative
|
|
109
|
+
* safe integer. A finite deadline past this one passed every other check and
|
|
110
|
+
* then failed the park write, so it is refused with the payload's other
|
|
111
|
+
* invalid deadlines instead.
|
|
112
|
+
*
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
const latestDeadline = Number.MAX_SAFE_INTEGER
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The refusal for a deadline later than {@link latestDeadline}.
|
|
119
|
+
*
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
const tooLate = (stated: string): SleepRequestInvalid =>
|
|
123
|
+
new SleepRequestInvalid({
|
|
124
|
+
code: "invalid_deadline",
|
|
125
|
+
message: `${tag} was called with ${stated}, which ends after ${latestDeadline}, the latest epoch ` +
|
|
126
|
+
"millisecond a durable timer records."
|
|
127
|
+
})
|
|
128
|
+
|
|
105
129
|
/**
|
|
106
130
|
* The absolute deadline a payload names, in epoch milliseconds.
|
|
107
131
|
*
|
|
@@ -153,12 +177,13 @@ const deadlineOf = (
|
|
|
153
177
|
})
|
|
154
178
|
)
|
|
155
179
|
}
|
|
156
|
-
return
|
|
180
|
+
return Math.ceil(deadline) > latestDeadline
|
|
181
|
+
? Effect.fail(tooLate(`millis ${payload.millis} at current time ${now}`))
|
|
182
|
+
: Effect.succeed(deadline)
|
|
157
183
|
}
|
|
158
184
|
if (payload.until !== undefined) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
: Effect.fail(
|
|
185
|
+
if (!Number.isFinite(payload.until)) {
|
|
186
|
+
return Effect.fail(
|
|
162
187
|
new SleepRequestInvalid({
|
|
163
188
|
code: "invalid_deadline",
|
|
164
189
|
message:
|
|
@@ -166,6 +191,10 @@ const deadlineOf = (
|
|
|
166
191
|
"the sleep must end at a real point in time."
|
|
167
192
|
})
|
|
168
193
|
)
|
|
194
|
+
}
|
|
195
|
+
return Math.ceil(payload.until) > latestDeadline
|
|
196
|
+
? Effect.fail(tooLate(`until ${payload.until}`))
|
|
197
|
+
: Effect.succeed(payload.until)
|
|
169
198
|
}
|
|
170
199
|
return Effect.fail(
|
|
171
200
|
new SleepRequestInvalid({
|