@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 CHANGED
@@ -44,7 +44,9 @@ none of which the flow's author arranges.
44
44
 
45
45
  ## Install
46
46
 
47
- Not on npm yet; see [Installation](./docs/installation.md#use-the-libraries).
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.
@@ -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. `maxAttempts` is the lineage's round
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Poll.d.ts","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"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"}
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) => options.intervalMs * (options.backoff === "fixed" ? 1 : options.backoff === "linear" ? options.attempt : 2 ** (options.attempt - 1));
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
  );
@@ -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": "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",
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
  }
@@ -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;AA6GF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,CAuCxE,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
- return Number.isFinite(payload.until) ? Effect.succeed(payload.until) : Effect.fail(
93
- new SleepRequestInvalid({
94
- code: "invalid_deadline",
95
- 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.`
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({
@@ -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": "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",
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
  }
@@ -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. `maxAttempts` is the lineage's round
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Poll.d.ts","sourceRoot":"","sources":["../../src/Poll.ts"],"names":[],"mappings":"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"}
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) => options.intervalMs *
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. `maxAttempts` is the lineage's round
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
- if (!Number.isFinite(longestWaitMs)) {
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 ` +
@@ -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":"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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 // 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"]}
@@ -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;AA6GF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,WAAW,CAuCxE,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 Effect.succeed(deadline);
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
- return Number.isFinite(payload.until)
132
- ? Effect.succeed(payload.until)
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",
@@ -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
- Not on npm yet; see [Installation](./installation.md#use-the-libraries).
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`, or an addition that overflows |
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.7 @smthrs/plan@1.0.0-rc.7
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`, or an addition that overflows. |
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
 
@@ -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
- `@smthrs/flow` is not on npm yet. Link the source packages as described in [Use the libraries](#use-the-libraries).
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
- First [install the source checkout](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/installation.md#install-the-cli). The checkout's `examples` workspace carries the packages used by the flow tutorials and their pinned `effect@4.0.4`.
27
-
28
- Create a project beside the checkout:
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
- mkdir my-flows && cd my-flows
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
- Replace `/path/to/smithers` with the absolute path to your installed checkout. Run programs with `node <file>.ts`. Packages absent from the examples workspace, such as `@smthrs/artifacts`, resolve only inside the checkout; use the owning package's workspace for those examples. This linking recipe uses source exports and requires no package build.
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
 
@@ -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 package with the dependencies installed. Not on npm yet; see [Installation](./installation.md#use-the-libraries).
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
 
@@ -5,7 +5,11 @@ area: api
5
5
  order: 10
6
6
  ---
7
7
 
8
- Install the package with its `effect` peer. Not on npm yet; see [Installation](../installation.md#use-the-libraries).
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.7",
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.7",
293
- "@smthrs/keys": "1.0.0-rc.7",
294
- "@smthrs/plan": "1.0.0-rc.7",
295
- "@smthrs/canonical": "1.0.0-rc.7",
296
- "@smthrs/capability": "1.0.0-rc.7"
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
- options.intervalMs *
153
- (options.backoff === "fixed" ? 1 : options.backoff === "linear" ? options.attempt : 2 ** (options.attempt - 1))
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. `maxAttempts` is the lineage's round
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
- if (!Number.isFinite(longestWaitMs)) {
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 Effect.succeed(deadline)
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
- return Number.isFinite(payload.until)
160
- ? Effect.succeed(payload.until)
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({