@smthrs/triggers 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +219 -0
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/cjs/CatchUp.d.ts +25 -0
- package/dist/cjs/CatchUp.d.ts.map +1 -0
- package/dist/cjs/CatchUp.js +72 -0
- package/dist/cjs/CatchUp.js.map +7 -0
- package/dist/cjs/Channel.d.ts +108 -0
- package/dist/cjs/Channel.d.ts.map +1 -0
- package/dist/cjs/Channel.js +25 -0
- package/dist/cjs/Channel.js.map +7 -0
- package/dist/cjs/ClaimDecision.d.ts +183 -0
- package/dist/cjs/ClaimDecision.d.ts.map +1 -0
- package/dist/cjs/ClaimDecision.js +144 -0
- package/dist/cjs/ClaimDecision.js.map +7 -0
- package/dist/cjs/Cron.d.ts +91 -0
- package/dist/cjs/Cron.d.ts.map +1 -0
- package/dist/cjs/Cron.js +108 -0
- package/dist/cjs/Cron.js.map +7 -0
- package/dist/cjs/DispatchReader.d.ts +77 -0
- package/dist/cjs/DispatchReader.d.ts.map +1 -0
- package/dist/cjs/DispatchReader.js +121 -0
- package/dist/cjs/DispatchReader.js.map +7 -0
- package/dist/cjs/Overlap.d.ts +40 -0
- package/dist/cjs/Overlap.d.ts.map +1 -0
- package/dist/cjs/Overlap.js +37 -0
- package/dist/cjs/Overlap.js.map +7 -0
- package/dist/cjs/Schedule.d.ts +96 -0
- package/dist/cjs/Schedule.d.ts.map +1 -0
- package/dist/cjs/Schedule.js +61 -0
- package/dist/cjs/Schedule.js.map +7 -0
- package/dist/cjs/Scheduler.d.ts +248 -0
- package/dist/cjs/Scheduler.d.ts.map +1 -0
- package/dist/cjs/Scheduler.js +647 -0
- package/dist/cjs/Scheduler.js.map +7 -0
- package/dist/cjs/SqlTriggerStore.d.ts +29 -0
- package/dist/cjs/SqlTriggerStore.d.ts.map +1 -0
- package/dist/cjs/SqlTriggerStore.js +483 -0
- package/dist/cjs/SqlTriggerStore.js.map +7 -0
- package/dist/cjs/Trigger.d.ts +81 -0
- package/dist/cjs/Trigger.d.ts.map +1 -0
- package/dist/cjs/Trigger.js +54 -0
- package/dist/cjs/Trigger.js.map +7 -0
- package/dist/cjs/TriggerError.d.ts +58 -0
- package/dist/cjs/TriggerError.d.ts.map +1 -0
- package/dist/cjs/TriggerError.js +83 -0
- package/dist/cjs/TriggerError.js.map +7 -0
- package/dist/cjs/TriggerStore.d.ts +411 -0
- package/dist/cjs/TriggerStore.d.ts.map +1 -0
- package/dist/cjs/TriggerStore.js +127 -0
- package/dist/cjs/TriggerStore.js.map +7 -0
- package/dist/cjs/Webhook.d.ts +118 -0
- package/dist/cjs/Webhook.d.ts.map +1 -0
- package/dist/cjs/Webhook.js +153 -0
- package/dist/cjs/Webhook.js.map +7 -0
- package/dist/cjs/index.d.ts +30 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +57 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ActiveRuns.d.ts +51 -0
- package/dist/cjs/internal/ActiveRuns.d.ts.map +1 -0
- package/dist/cjs/internal/ActiveRuns.js +56 -0
- package/dist/cjs/internal/ActiveRuns.js.map +7 -0
- package/dist/cjs/internal/DueOccurrences.d.ts +32 -0
- package/dist/cjs/internal/DueOccurrences.d.ts.map +1 -0
- package/dist/cjs/internal/DueOccurrences.js +80 -0
- package/dist/cjs/internal/DueOccurrences.js.map +7 -0
- package/dist/cjs/internal/WallClock.d.ts +59 -0
- package/dist/cjs/internal/WallClock.d.ts.map +1 -0
- package/dist/cjs/internal/WallClock.js +132 -0
- package/dist/cjs/internal/WallClock.js.map +7 -0
- package/dist/cjs/migrations/0001_triggers.d.ts +15 -0
- package/dist/cjs/migrations/0001_triggers.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_triggers.js +64 -0
- package/dist/cjs/migrations/0001_triggers.js.map +7 -0
- package/dist/cjs/migrations/0002_reservation_lease.d.ts +15 -0
- package/dist/cjs/migrations/0002_reservation_lease.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_reservation_lease.js +41 -0
- package/dist/cjs/migrations/0002_reservation_lease.js.map +7 -0
- package/dist/cjs/migrations/0003_heartbeat.d.ts +16 -0
- package/dist/cjs/migrations/0003_heartbeat.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_heartbeat.js +44 -0
- package/dist/cjs/migrations/0003_heartbeat.js.map +7 -0
- package/dist/cjs/migrations/0004_fire_run_index.d.ts +17 -0
- package/dist/cjs/migrations/0004_fire_run_index.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_fire_run_index.js +40 -0
- package/dist/cjs/migrations/0004_fire_run_index.js.map +7 -0
- package/dist/cjs/migrations/index.d.ts +33 -0
- package/dist/cjs/migrations/index.d.ts.map +1 -0
- package/dist/cjs/migrations/index.js +50 -0
- package/dist/cjs/migrations/index.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestTriggers.d.ts +12 -0
- package/dist/cjs/test/TestTriggers.d.ts.map +1 -0
- package/dist/cjs/test/TestTriggers.js +421 -0
- package/dist/cjs/test/TestTriggers.js.map +7 -0
- package/dist/esm/CatchUp.d.ts +25 -0
- package/dist/esm/CatchUp.d.ts.map +1 -0
- package/dist/esm/CatchUp.js +52 -0
- package/dist/esm/CatchUp.js.map +1 -0
- package/dist/esm/Channel.d.ts +108 -0
- package/dist/esm/Channel.d.ts.map +1 -0
- package/dist/esm/Channel.js +19 -0
- package/dist/esm/Channel.js.map +1 -0
- package/dist/esm/ClaimDecision.d.ts +183 -0
- package/dist/esm/ClaimDecision.d.ts.map +1 -0
- package/dist/esm/ClaimDecision.js +172 -0
- package/dist/esm/ClaimDecision.js.map +1 -0
- package/dist/esm/Cron.d.ts +91 -0
- package/dist/esm/Cron.d.ts.map +1 -0
- package/dist/esm/Cron.js +132 -0
- package/dist/esm/Cron.js.map +1 -0
- package/dist/esm/DispatchReader.d.ts +77 -0
- package/dist/esm/DispatchReader.d.ts.map +1 -0
- package/dist/esm/DispatchReader.js +152 -0
- package/dist/esm/DispatchReader.js.map +1 -0
- package/dist/esm/Overlap.d.ts +40 -0
- package/dist/esm/Overlap.d.ts.map +1 -0
- package/dist/esm/Overlap.js +31 -0
- package/dist/esm/Overlap.js.map +1 -0
- package/dist/esm/Schedule.d.ts +96 -0
- package/dist/esm/Schedule.d.ts.map +1 -0
- package/dist/esm/Schedule.js +73 -0
- package/dist/esm/Schedule.js.map +1 -0
- package/dist/esm/Scheduler.d.ts +248 -0
- package/dist/esm/Scheduler.d.ts.map +1 -0
- package/dist/esm/Scheduler.js +734 -0
- package/dist/esm/Scheduler.js.map +1 -0
- package/dist/esm/SqlTriggerStore.d.ts +29 -0
- package/dist/esm/SqlTriggerStore.d.ts.map +1 -0
- package/dist/esm/SqlTriggerStore.js +483 -0
- package/dist/esm/SqlTriggerStore.js.map +1 -0
- package/dist/esm/Trigger.d.ts +81 -0
- package/dist/esm/Trigger.d.ts.map +1 -0
- package/dist/esm/Trigger.js +56 -0
- package/dist/esm/Trigger.js.map +1 -0
- package/dist/esm/TriggerError.d.ts +58 -0
- package/dist/esm/TriggerError.d.ts.map +1 -0
- package/dist/esm/TriggerError.js +110 -0
- package/dist/esm/TriggerError.js.map +1 -0
- package/dist/esm/TriggerStore.d.ts +411 -0
- package/dist/esm/TriggerStore.d.ts.map +1 -0
- package/dist/esm/TriggerStore.js +209 -0
- package/dist/esm/TriggerStore.js.map +1 -0
- package/dist/esm/Webhook.d.ts +118 -0
- package/dist/esm/Webhook.d.ts.map +1 -0
- package/dist/esm/Webhook.js +186 -0
- package/dist/esm/Webhook.js.map +1 -0
- package/dist/esm/index.d.ts +30 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +30 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ActiveRuns.d.ts +51 -0
- package/dist/esm/internal/ActiveRuns.d.ts.map +1 -0
- package/dist/esm/internal/ActiveRuns.js +34 -0
- package/dist/esm/internal/ActiveRuns.js.map +1 -0
- package/dist/esm/internal/DueOccurrences.d.ts +32 -0
- package/dist/esm/internal/DueOccurrences.d.ts.map +1 -0
- package/dist/esm/internal/DueOccurrences.js +55 -0
- package/dist/esm/internal/DueOccurrences.js.map +1 -0
- package/dist/esm/internal/WallClock.d.ts +59 -0
- package/dist/esm/internal/WallClock.d.ts.map +1 -0
- package/dist/esm/internal/WallClock.js +155 -0
- package/dist/esm/internal/WallClock.js.map +1 -0
- package/dist/esm/migrations/0001_triggers.d.ts +15 -0
- package/dist/esm/migrations/0001_triggers.d.ts.map +1 -0
- package/dist/esm/migrations/0001_triggers.js +42 -0
- package/dist/esm/migrations/0001_triggers.js.map +1 -0
- package/dist/esm/migrations/0002_reservation_lease.d.ts +15 -0
- package/dist/esm/migrations/0002_reservation_lease.d.ts.map +1 -0
- package/dist/esm/migrations/0002_reservation_lease.js +19 -0
- package/dist/esm/migrations/0002_reservation_lease.js.map +1 -0
- package/dist/esm/migrations/0003_heartbeat.d.ts +16 -0
- package/dist/esm/migrations/0003_heartbeat.d.ts.map +1 -0
- package/dist/esm/migrations/0003_heartbeat.js +23 -0
- package/dist/esm/migrations/0003_heartbeat.js.map +1 -0
- package/dist/esm/migrations/0004_fire_run_index.d.ts +17 -0
- package/dist/esm/migrations/0004_fire_run_index.d.ts.map +1 -0
- package/dist/esm/migrations/0004_fire_run_index.js +20 -0
- package/dist/esm/migrations/0004_fire_run_index.js.map +1 -0
- package/dist/esm/migrations/index.d.ts +33 -0
- package/dist/esm/migrations/index.d.ts.map +1 -0
- package/dist/esm/migrations/index.js +37 -0
- package/dist/esm/migrations/index.js.map +1 -0
- package/dist/esm/test/TestTriggers.d.ts +12 -0
- package/dist/esm/test/TestTriggers.d.ts.map +1 -0
- package/dist/esm/test/TestTriggers.js +439 -0
- package/dist/esm/test/TestTriggers.js.map +1 -0
- package/docs/README.md +140 -0
- package/docs/api.md +1186 -0
- package/docs/concepts/channels.md +103 -0
- package/docs/concepts/claim-protocol.md +229 -0
- package/docs/concepts/policies.md +124 -0
- package/docs/concepts/schedules.md +134 -0
- package/docs/guides/choose-a-policy.md +113 -0
- package/docs/guides/declare-a-trigger.md +136 -0
- package/docs/guides/ingest-a-webhook.md +164 -0
- package/docs/guides/run-the-scheduler.md +173 -0
- package/docs/guides/testing.md +157 -0
- package/docs/installation.md +81 -0
- package/docs/quickstart.md +163 -0
- package/docs/troubleshooting.md +281 -0
- package/package.json +241 -3
- package/src/CatchUp.ts +73 -0
- package/src/Channel.ts +121 -0
- package/src/ClaimDecision.ts +303 -0
- package/src/Cron.ts +173 -0
- package/src/DispatchReader.ts +179 -0
- package/src/Overlap.ts +53 -0
- package/src/Schedule.ts +112 -0
- package/src/Scheduler.ts +1154 -0
- package/src/SqlTriggerStore.ts +638 -0
- package/src/Trigger.ts +84 -0
- package/src/TriggerError.ts +129 -0
- package/src/TriggerStore.ts +514 -0
- package/src/Webhook.ts +270 -0
- package/src/index.ts +30 -0
- package/src/internal/ActiveRuns.ts +83 -0
- package/src/internal/DueOccurrences.ts +101 -0
- package/src/internal/WallClock.ts +172 -0
- package/src/migrations/0001_triggers.ts +43 -0
- package/src/migrations/0002_reservation_lease.ts +20 -0
- package/src/migrations/0003_heartbeat.ts +24 -0
- package/src/migrations/0004_fire_run_index.ts +21 -0
- package/src/migrations/index.ts +40 -0
- package/src/test/TestTriggers.ts +563 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var Scheduler_exports = {};
|
|
30
|
+
__export(Scheduler_exports, {
|
|
31
|
+
Runner: () => Runner,
|
|
32
|
+
Scheduler: () => Scheduler,
|
|
33
|
+
defaultConcurrency: () => defaultConcurrency,
|
|
34
|
+
defaultFireRetention: () => defaultFireRetention,
|
|
35
|
+
defaultHost: () => defaultHost,
|
|
36
|
+
durationBoundaries: () => durationBoundaries,
|
|
37
|
+
idempotencyKey: () => idempotencyKey,
|
|
38
|
+
layer: () => layer,
|
|
39
|
+
layerControlRunner: () => layerControlRunner,
|
|
40
|
+
layerNoop: () => layerNoop,
|
|
41
|
+
layerNoopRunner: () => layerNoopRunner,
|
|
42
|
+
make: () => make,
|
|
43
|
+
makeNoop: () => makeNoop,
|
|
44
|
+
makeNoopRunner: () => makeNoopRunner,
|
|
45
|
+
makeRunner: () => makeRunner,
|
|
46
|
+
parkedAttempts: () => parkedAttempts
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(Scheduler_exports);
|
|
49
|
+
var Control = __toESM(require("@smthrs/control/Control"), 1);
|
|
50
|
+
var Cause = __toESM(require("effect/Cause"), 1);
|
|
51
|
+
var Clock = __toESM(require("effect/Clock"), 1);
|
|
52
|
+
var Context = __toESM(require("effect/Context"), 1);
|
|
53
|
+
var Duration = __toESM(require("effect/Duration"), 1);
|
|
54
|
+
var Effect = __toESM(require("effect/Effect"), 1);
|
|
55
|
+
var Fiber = __toESM(require("effect/Fiber"), 1);
|
|
56
|
+
var Layer = __toESM(require("effect/Layer"), 1);
|
|
57
|
+
var Metric = __toESM(require("effect/Metric"), 1);
|
|
58
|
+
var Option = __toESM(require("effect/Option"), 1);
|
|
59
|
+
var Ref = __toESM(require("effect/Ref"), 1);
|
|
60
|
+
var Result = __toESM(require("effect/Result"), 1);
|
|
61
|
+
var Semaphore = __toESM(require("effect/Semaphore"), 1);
|
|
62
|
+
var ActiveRuns = __toESM(require("./internal/ActiveRuns.js"), 1);
|
|
63
|
+
var DueOccurrences = __toESM(require("./internal/DueOccurrences.js"), 1);
|
|
64
|
+
var import_TriggerError = require("./TriggerError.js");
|
|
65
|
+
var import_TriggerStore = require("./TriggerStore.js");
|
|
66
|
+
class Runner extends Context.Service()("flows/triggers/Scheduler/Runner") {
|
|
67
|
+
}
|
|
68
|
+
const makeRunner = (implementation) => Runner.of(implementation);
|
|
69
|
+
const makeNoopRunner = (overrides = {}) => makeRunner({
|
|
70
|
+
start: (input) => Effect.succeed(input.idempotencyKey),
|
|
71
|
+
inspect: () => Effect.succeed("completed"),
|
|
72
|
+
cancel: () => Effect.void,
|
|
73
|
+
...overrides
|
|
74
|
+
});
|
|
75
|
+
const layerNoopRunner = (overrides = {}) => Layer.succeed(Runner)(makeNoopRunner(overrides));
|
|
76
|
+
const runnerError = (message, cause) => new import_TriggerError.TriggerError({
|
|
77
|
+
code: "runner",
|
|
78
|
+
message,
|
|
79
|
+
...cause === void 0 ? {} : { cause }
|
|
80
|
+
});
|
|
81
|
+
const translateRunnerFailure = (effect, message) => effect.pipe(
|
|
82
|
+
Effect.mapError((error) => error instanceof import_TriggerError.TriggerError ? error : runnerError(message, error)),
|
|
83
|
+
Effect.catchDefect((defect) => Effect.fail(runnerError(message, defect)))
|
|
84
|
+
);
|
|
85
|
+
const invalidOption = (field, requirement) => new import_TriggerError.TriggerError({
|
|
86
|
+
code: "invalid_options",
|
|
87
|
+
message: `${field} must be ${requirement}`,
|
|
88
|
+
path: field
|
|
89
|
+
});
|
|
90
|
+
const duration = (input, field) => Option.match(Duration.fromInput(input), {
|
|
91
|
+
onNone: () => Effect.fail(invalidOption(field, "a valid Effect duration")),
|
|
92
|
+
onSome: (value) => (
|
|
93
|
+
// Zero polls a CPU-tight loop and an infinite interval never completes,
|
|
94
|
+
// and `Duration.fromInput` accepts both.
|
|
95
|
+
Duration.isFinite(value) && Duration.toMillis(value) > 0 ? Effect.succeed(value) : Effect.fail(invalidOption(field, "a finite positive duration"))
|
|
96
|
+
)
|
|
97
|
+
});
|
|
98
|
+
const stoppedMessage = (runId, state) => {
|
|
99
|
+
switch (state) {
|
|
100
|
+
case "failed":
|
|
101
|
+
return `Run ${runId} failed`;
|
|
102
|
+
case "cancelled":
|
|
103
|
+
return `Run ${runId} was cancelled`;
|
|
104
|
+
case "missing":
|
|
105
|
+
return `Run ${runId} is unknown to the runner`;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const runIdFromReceipt = (receipt) => {
|
|
109
|
+
switch (receipt._tag) {
|
|
110
|
+
case "Accepted":
|
|
111
|
+
case "AlreadyApplied":
|
|
112
|
+
return receipt.runId === void 0 ? Effect.fail(runnerError(`Control ${receipt._tag} receipt did not include a run id`)) : Effect.succeed(receipt.runId);
|
|
113
|
+
case "Terminal":
|
|
114
|
+
return Effect.succeed(receipt.runId);
|
|
115
|
+
case "Conflict":
|
|
116
|
+
return Effect.fail(runnerError(`Control rejected the scheduled run: ${receipt.message}`));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const parkedAttempts = 8;
|
|
120
|
+
const runApprovedPlan = (control, plan, key, attempt) => control.run({
|
|
121
|
+
_tag: "Plan",
|
|
122
|
+
planId: plan.planId,
|
|
123
|
+
digest: plan.digest,
|
|
124
|
+
envelope: plan.envelope,
|
|
125
|
+
idempotencyKey: key
|
|
126
|
+
}).pipe(
|
|
127
|
+
Effect.mapError((error) => runnerError("Control could not launch the scheduled run", error)),
|
|
128
|
+
Effect.flatMap((receipt) => {
|
|
129
|
+
if (receipt._tag !== "Parked") return runIdFromReceipt(receipt);
|
|
130
|
+
if (attempt >= parkedAttempts) {
|
|
131
|
+
return Effect.fail(
|
|
132
|
+
runnerError(
|
|
133
|
+
`Control plan ${plan.planId} is still parked awaiting approval after ${attempt} attempts`
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return Effect.logInfo(`A scheduled plan is parked awaiting approval, attempt ${attempt}`).pipe(
|
|
138
|
+
Effect.andThen(Effect.sleep(Duration.millis(1e3 * 2 ** (attempt - 1)))),
|
|
139
|
+
Effect.andThen(Effect.suspend(() => runApprovedPlan(control, plan, key, attempt + 1)))
|
|
140
|
+
);
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
const settledState = (status) => status === "cancelled" || status === "completed" || status === "failed" ? status : void 0;
|
|
144
|
+
const layerControlRunner = Layer.effect(
|
|
145
|
+
Runner,
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
const control = yield* Control.Control;
|
|
148
|
+
return makeRunner({
|
|
149
|
+
start: (input) => translateRunnerFailure(
|
|
150
|
+
control.plan(input).pipe(
|
|
151
|
+
Effect.flatMap((plan) => runApprovedPlan(control, plan, input.idempotencyKey, 1))
|
|
152
|
+
),
|
|
153
|
+
"Control could not launch the scheduled run"
|
|
154
|
+
),
|
|
155
|
+
inspect: (runId) => translateRunnerFailure(
|
|
156
|
+
control.list({ _tag: "runs", filters: { runId }, limit: 1 }).pipe(
|
|
157
|
+
Effect.map((response) => {
|
|
158
|
+
const run = response._tag === "runs" ? response.items.find((candidate) => candidate.runId === runId) : void 0;
|
|
159
|
+
if (run === void 0) return "missing";
|
|
160
|
+
return settledState(run.status) ?? "active";
|
|
161
|
+
})
|
|
162
|
+
),
|
|
163
|
+
`Control could not inspect run ${runId}`
|
|
164
|
+
),
|
|
165
|
+
cancel: (runId) => translateRunnerFailure(
|
|
166
|
+
control.cancel({
|
|
167
|
+
runId,
|
|
168
|
+
idempotencyKey: `trigger-cancel:${runId}`
|
|
169
|
+
}).pipe(
|
|
170
|
+
Effect.flatMap((receipt) => {
|
|
171
|
+
switch (receipt._tag) {
|
|
172
|
+
case "Accepted":
|
|
173
|
+
case "AlreadyApplied":
|
|
174
|
+
case "Terminal":
|
|
175
|
+
return Effect.void;
|
|
176
|
+
case "Conflict":
|
|
177
|
+
return Effect.fail(runnerError(`Control refused scheduled cancellation: ${receipt.message}`));
|
|
178
|
+
case "Parked":
|
|
179
|
+
return Effect.fail(runnerError(`Control returned a Parked receipt for cancellation of run ${runId}`));
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
),
|
|
183
|
+
`Control could not cancel run ${runId}`
|
|
184
|
+
)
|
|
185
|
+
});
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
const defaultHost = "local";
|
|
189
|
+
const defaultConcurrency = 4;
|
|
190
|
+
const defaultFireRetention = Duration.days(30);
|
|
191
|
+
const pruneEvery = 60 * 60 * 1e3;
|
|
192
|
+
const durationBoundaries = [10, 100, 1e3, 1e4, 6e4, 3e5];
|
|
193
|
+
const firesRecorded = Metric.counter("smithers.triggers.fires");
|
|
194
|
+
const tickDuration = Metric.histogram("smithers.triggers.tick_duration_ms", { boundaries: durationBoundaries });
|
|
195
|
+
const launchDuration = Metric.histogram("smithers.triggers.launch_duration_ms", { boundaries: durationBoundaries });
|
|
196
|
+
const timed = (histogram, effect) => Effect.gen(function* () {
|
|
197
|
+
const started = yield* Clock.currentTimeMillis;
|
|
198
|
+
return yield* effect.pipe(
|
|
199
|
+
Effect.ensuring(
|
|
200
|
+
Effect.flatMap(Clock.currentTimeMillis, (ended) => Metric.update(histogram, ended - started))
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
const deadline = (effect, limit, what) => Effect.timeoutOrElse(effect, {
|
|
205
|
+
duration: limit,
|
|
206
|
+
orElse: () => Effect.fail(
|
|
207
|
+
new import_TriggerError.TriggerError({
|
|
208
|
+
code: "runner_timeout",
|
|
209
|
+
message: `${what} did not answer within ${Duration.toMillis(limit)} ms`
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
});
|
|
213
|
+
class Scheduler extends Context.Service()("flows/triggers/Scheduler") {
|
|
214
|
+
}
|
|
215
|
+
const idempotencyKey = (triggerId, occurrence) => `${triggerId}:${new Date(occurrence).toISOString()}`;
|
|
216
|
+
const attempted = (annotations, work, effect) => Effect.catchCause(Effect.as(effect, true), (cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.as(
|
|
217
|
+
Effect.annotateLogs(Effect.logWarning(`A trigger ${work} failed`, cause), annotations),
|
|
218
|
+
false
|
|
219
|
+
));
|
|
220
|
+
const isolate = (annotations, work, effect) => Effect.asVoid(attempted(annotations, work, effect));
|
|
221
|
+
const make = (options = {}) => Effect.gen(function* () {
|
|
222
|
+
const parentScope = yield* Effect.scope;
|
|
223
|
+
const providedStore = yield* import_TriggerStore.TriggerStore;
|
|
224
|
+
const store = {
|
|
225
|
+
...providedStore,
|
|
226
|
+
recordResult: (result) => providedStore.recordResult(result).pipe(
|
|
227
|
+
Effect.tap(() => Metric.update(firesRecorded.pipe(Metric.withAttributes({ outcome: result.outcome })), 1))
|
|
228
|
+
)
|
|
229
|
+
};
|
|
230
|
+
const active = yield* ActiveRuns.make;
|
|
231
|
+
const observedAt = yield* Ref.make(/* @__PURE__ */ new Map());
|
|
232
|
+
const semaphore = yield* Semaphore.make(1);
|
|
233
|
+
const runPollInterval = yield* duration(options.runPollInterval ?? "15 seconds", "runPollInterval");
|
|
234
|
+
const concurrency = options.concurrency ?? defaultConcurrency;
|
|
235
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
236
|
+
return yield* Effect.fail(invalidOption("concurrency", "a positive integer"));
|
|
237
|
+
}
|
|
238
|
+
const startTimeout = yield* duration(options.startTimeout ?? "4 minutes", "startTimeout");
|
|
239
|
+
const inspectTimeout = yield* duration(options.inspectTimeout ?? "30 seconds", "inspectTimeout");
|
|
240
|
+
const cancelTimeout = yield* duration(options.cancelTimeout ?? "30 seconds", "cancelTimeout");
|
|
241
|
+
const fireRetention = yield* duration(options.fireRetention ?? defaultFireRetention, "fireRetention");
|
|
242
|
+
const lastPrunedAt = yield* Ref.make(void 0);
|
|
243
|
+
const provided = yield* Runner;
|
|
244
|
+
const runner = {
|
|
245
|
+
start: (input) => deadline(provided.start(input), startTimeout, `Runner.start for ${input.idempotencyKey}`),
|
|
246
|
+
inspect: (runId) => deadline(provided.inspect(runId), inspectTimeout, `Runner.inspect for run ${runId}`),
|
|
247
|
+
cancel: (runId) => deadline(provided.cancel(runId), cancelTimeout, `Runner.cancel for run ${runId}`)
|
|
248
|
+
};
|
|
249
|
+
const observe = (triggerId, occurrence) => Ref.update(observedAt, (current) => {
|
|
250
|
+
const existing = current.get(triggerId);
|
|
251
|
+
if (existing !== void 0 && existing >= occurrence) return current;
|
|
252
|
+
return new Map(current).set(triggerId, occurrence);
|
|
253
|
+
});
|
|
254
|
+
const inspectStored = (runId) => (0, import_TriggerStore.isReservation)(runId) ? Effect.succeed("active") : runner.inspect(runId);
|
|
255
|
+
const occurrenceOf = (triggerId, runId) => {
|
|
256
|
+
const reserved = (0, import_TriggerStore.reservationOccurrence)(runId);
|
|
257
|
+
if (reserved !== void 0) return Effect.succeed(reserved);
|
|
258
|
+
return store.activeOccurrence(triggerId, runId).pipe(
|
|
259
|
+
Effect.map((occurrence) => Option.isSome(occurrence) ? occurrence.value : Number.NEGATIVE_INFINITY)
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
const recordSettled = (triggerId, occurrence, runId, state) => state === "completed" ? store.recordResult({ triggerId, occurrence, outcome: "completed", runId }) : store.recordResult({ triggerId, occurrence, outcome: "failed", runId, error: stoppedMessage(runId, state) });
|
|
263
|
+
const settleRecovered = (triggerId, occurrence, runId, state) => Number.isFinite(occurrence) ? recordSettled(triggerId, occurrence, runId, state) : store.clearActive(triggerId, runId);
|
|
264
|
+
const storedActive = (triggerId, held) => held !== void 0 && held.activeRunId === void 0 ? Effect.succeed(Option.none()) : store.activeRun(triggerId);
|
|
265
|
+
const recoverStored = (trigger, runId) => Effect.gen(function* () {
|
|
266
|
+
const occurrence = yield* occurrenceOf(trigger.id, runId);
|
|
267
|
+
const state = yield* inspectStored(runId);
|
|
268
|
+
if (state !== "active") {
|
|
269
|
+
yield* settleRecovered(trigger.id, occurrence, runId, state);
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
const recovered = { occurrence, runId };
|
|
273
|
+
yield* active.take(trigger.id, recovered);
|
|
274
|
+
return recovered;
|
|
275
|
+
});
|
|
276
|
+
const resolveActive = (trigger, held) => Effect.gen(function* () {
|
|
277
|
+
const local = yield* active.get(trigger.id);
|
|
278
|
+
if (local !== void 0) {
|
|
279
|
+
if (local.fiber !== void 0) return local;
|
|
280
|
+
if (!(0, import_TriggerStore.isReservation)(local.runId)) {
|
|
281
|
+
const state = yield* inspectStored(local.runId);
|
|
282
|
+
if (state === "active") return local;
|
|
283
|
+
yield* settleRecovered(trigger.id, local.occurrence, local.runId, state);
|
|
284
|
+
yield* active.remove(trigger.id, local.occurrence);
|
|
285
|
+
} else {
|
|
286
|
+
const stored2 = yield* storedActive(trigger.id, held);
|
|
287
|
+
if (Option.isNone(stored2)) {
|
|
288
|
+
yield* active.remove(trigger.id, local.occurrence);
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
if (stored2.value === local.runId) return local;
|
|
292
|
+
yield* active.remove(trigger.id, local.occurrence);
|
|
293
|
+
return yield* recoverStored(trigger, stored2.value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const stored = yield* storedActive(trigger.id, held);
|
|
297
|
+
if (Option.isNone(stored)) return void 0;
|
|
298
|
+
return yield* recoverStored(trigger, stored.value);
|
|
299
|
+
});
|
|
300
|
+
const recordFailed = (trigger, occurrence, error, runId) => store.recordResult({
|
|
301
|
+
triggerId: trigger.id,
|
|
302
|
+
occurrence,
|
|
303
|
+
outcome: "failed",
|
|
304
|
+
error: error.message,
|
|
305
|
+
runId
|
|
306
|
+
});
|
|
307
|
+
const inspectRun = (triggerId, runId) => Effect.gen(function* () {
|
|
308
|
+
for (let attempt = 0; ; attempt++) {
|
|
309
|
+
const inspected = yield* runner.inspect(runId).pipe(
|
|
310
|
+
Effect.map(Option.some),
|
|
311
|
+
Effect.catchCause(
|
|
312
|
+
(cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.logWarning("A trigger run inspection failed", cause).pipe(
|
|
313
|
+
Effect.annotateLogs({ triggerId, runId, attempt: String(attempt + 1) }),
|
|
314
|
+
Effect.as(Option.none())
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
if (Option.isSome(inspected)) return inspected;
|
|
319
|
+
if (attempt === 3) {
|
|
320
|
+
yield* Effect.logWarning("A trigger run monitor detached after inspection retries").pipe(
|
|
321
|
+
Effect.annotateLogs({ triggerId, runId })
|
|
322
|
+
);
|
|
323
|
+
return Option.none();
|
|
324
|
+
}
|
|
325
|
+
yield* Effect.sleep(Math.min(Duration.toMillis(runPollInterval) * 2 ** attempt, 6e4));
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const launch = (trigger, occurrence, reservation, preserveBuffered) => Effect.gen(function* () {
|
|
329
|
+
yield* active.take(trigger.id, { occurrence, runId: reservation });
|
|
330
|
+
let runId;
|
|
331
|
+
let launchRecorded = false;
|
|
332
|
+
let completed = false;
|
|
333
|
+
const lifecycle = Effect.gen(function* () {
|
|
334
|
+
runId = yield* timed(
|
|
335
|
+
launchDuration,
|
|
336
|
+
runner.start({
|
|
337
|
+
flowId: trigger.flowId,
|
|
338
|
+
input: trigger.input,
|
|
339
|
+
idempotencyKey: idempotencyKey(trigger.id, occurrence)
|
|
340
|
+
})
|
|
341
|
+
).pipe(
|
|
342
|
+
Effect.withSpan("Scheduler.launch", {
|
|
343
|
+
attributes: { triggerId: trigger.id, occurrence: new Date(occurrence).toISOString() }
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
const startedRunId = runId;
|
|
347
|
+
yield* active.update(trigger.id, occurrence, (entry) => ({ ...entry, runId: startedRunId }));
|
|
348
|
+
yield* store.recordResult({
|
|
349
|
+
triggerId: trigger.id,
|
|
350
|
+
occurrence,
|
|
351
|
+
outcome: "launched",
|
|
352
|
+
runId,
|
|
353
|
+
reservationId: reservation
|
|
354
|
+
});
|
|
355
|
+
launchRecorded = true;
|
|
356
|
+
let state;
|
|
357
|
+
while (true) {
|
|
358
|
+
const inspected = yield* inspectRun(trigger.id, runId);
|
|
359
|
+
if (Option.isNone(inspected)) return;
|
|
360
|
+
state = inspected.value;
|
|
361
|
+
if (state !== "active") break;
|
|
362
|
+
yield* Effect.sleep(runPollInterval);
|
|
363
|
+
}
|
|
364
|
+
yield* recordSettled(trigger.id, occurrence, runId, state);
|
|
365
|
+
completed = true;
|
|
366
|
+
}).pipe(
|
|
367
|
+
Effect.catch(
|
|
368
|
+
(error) => Effect.gen(function* () {
|
|
369
|
+
if (launchRecorded) {
|
|
370
|
+
yield* Effect.logWarning("A trigger run monitor could not persist completion", error).pipe(
|
|
371
|
+
Effect.annotateLogs({ triggerId: trigger.id, runId })
|
|
372
|
+
);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (error.code === "stale_owner" && runId !== void 0) {
|
|
376
|
+
yield* Effect.logWarning("A trigger launch lost its reservation", error).pipe(
|
|
377
|
+
Effect.annotateLogs({ triggerId: trigger.id, occurrence: String(occurrence) })
|
|
378
|
+
);
|
|
379
|
+
const losingRunId = runId;
|
|
380
|
+
yield* isolate(
|
|
381
|
+
{ triggerId: trigger.id, runId },
|
|
382
|
+
"losing run reconciliation",
|
|
383
|
+
Effect.gen(function* () {
|
|
384
|
+
const held = yield* store.inspect(trigger.id);
|
|
385
|
+
if (held.activeRunId === losingRunId || held.activeRunId !== void 0 && (0, import_TriggerStore.reservationOccurrence)(held.activeRunId) === occurrence) return;
|
|
386
|
+
yield* runner.cancel(losingRunId);
|
|
387
|
+
})
|
|
388
|
+
).pipe(Effect.ignore);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (preserveBuffered) {
|
|
392
|
+
yield* isolate(
|
|
393
|
+
{ triggerId: trigger.id },
|
|
394
|
+
"buffered launch compensation",
|
|
395
|
+
store.restorePending({ triggerId: trigger.id, occurrence, reservationId: reservation })
|
|
396
|
+
).pipe(Effect.ignore);
|
|
397
|
+
} else {
|
|
398
|
+
if (runId !== void 0 || error.code === "runner_timeout") {
|
|
399
|
+
yield* isolate(
|
|
400
|
+
{ triggerId: trigger.id },
|
|
401
|
+
"launch compensation",
|
|
402
|
+
store.restorePending({
|
|
403
|
+
triggerId: trigger.id,
|
|
404
|
+
occurrence,
|
|
405
|
+
reservationId: reservation
|
|
406
|
+
})
|
|
407
|
+
).pipe(Effect.ignore);
|
|
408
|
+
} else {
|
|
409
|
+
yield* recordFailed(trigger, occurrence, error, reservation).pipe(Effect.ignore);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
yield* Effect.logWarning("A trigger launch failed", error).pipe(
|
|
413
|
+
Effect.annotateLogs({ triggerId: trigger.id, occurrence: String(occurrence) })
|
|
414
|
+
);
|
|
415
|
+
})
|
|
416
|
+
),
|
|
417
|
+
// Interrupting this fiber detaches the monitor; it never cancels the
|
|
418
|
+
// run. The run is durable and outlives this process, so a deploy or
|
|
419
|
+
// any other scope closure must leave it alone: the next incarnation
|
|
420
|
+
// re-attaches through `resolveActive`. Cancellation is a deliberate
|
|
421
|
+
// act and belongs to `cancelActive` alone.
|
|
422
|
+
Effect.ensuring(Effect.suspend(
|
|
423
|
+
() => launchRecorded && !completed ? active.update(trigger.id, occurrence, (entry) => ({ ...entry, fiber: void 0 })) : active.remove(trigger.id, occurrence)
|
|
424
|
+
))
|
|
425
|
+
);
|
|
426
|
+
const fiber = yield* Effect.forkIn(
|
|
427
|
+
Effect.scoped(lifecycle).pipe(
|
|
428
|
+
Effect.onExit(
|
|
429
|
+
(exit) => exit._tag === "Failure" && !Cause.hasInterrupts(exit.cause) ? Effect.logWarning(
|
|
430
|
+
launchRecorded ? "A trigger run monitor failed" : "A trigger launch failed",
|
|
431
|
+
exit.cause
|
|
432
|
+
).pipe(
|
|
433
|
+
Effect.annotateLogs({
|
|
434
|
+
triggerId: trigger.id,
|
|
435
|
+
occurrence: String(occurrence),
|
|
436
|
+
runId: runId ?? reservation
|
|
437
|
+
})
|
|
438
|
+
) : Effect.void
|
|
439
|
+
)
|
|
440
|
+
),
|
|
441
|
+
parentScope,
|
|
442
|
+
{ startImmediately: true }
|
|
443
|
+
);
|
|
444
|
+
if (fiber.pollUnsafe() === void 0) {
|
|
445
|
+
yield* active.update(trigger.id, occurrence, (entry) => ({ ...entry, fiber }));
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const cancelActive = (trigger, prior, replacementOccurrence, replacementReservation, queueReplacement) => Effect.gen(function* () {
|
|
449
|
+
if (!(0, import_TriggerStore.isReservation)(prior.runId)) {
|
|
450
|
+
yield* runner.cancel(prior.runId).pipe(
|
|
451
|
+
Effect.catch((error) => {
|
|
452
|
+
const restore = queueReplacement ? isolate(
|
|
453
|
+
{ triggerId: trigger.id },
|
|
454
|
+
"supersede compensation",
|
|
455
|
+
store.restorePending({
|
|
456
|
+
triggerId: trigger.id,
|
|
457
|
+
occurrence: replacementOccurrence,
|
|
458
|
+
reservationId: replacementReservation
|
|
459
|
+
})
|
|
460
|
+
) : Effect.void;
|
|
461
|
+
return restore.pipe(
|
|
462
|
+
Effect.andThen(Effect.fail(error))
|
|
463
|
+
);
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (Number.isFinite(prior.occurrence) && !(0, import_TriggerStore.isReservation)(prior.runId)) {
|
|
468
|
+
yield* store.recordResult({
|
|
469
|
+
triggerId: trigger.id,
|
|
470
|
+
occurrence: prior.occurrence,
|
|
471
|
+
outcome: "superseded",
|
|
472
|
+
runId: prior.runId
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (prior.fiber !== void 0) yield* Fiber.interrupt(prior.fiber);
|
|
476
|
+
yield* active.remove(trigger.id, prior.occurrence);
|
|
477
|
+
});
|
|
478
|
+
const dispatchClaimed = (trigger, occurrence, claim, resumeBuffered = false) => Effect.gen(function* () {
|
|
479
|
+
switch (claim.action) {
|
|
480
|
+
case "fire":
|
|
481
|
+
yield* launch(trigger, occurrence, claim.reservationId, resumeBuffered);
|
|
482
|
+
return;
|
|
483
|
+
case "skip":
|
|
484
|
+
yield* store.recordResult({
|
|
485
|
+
triggerId: trigger.id,
|
|
486
|
+
occurrence,
|
|
487
|
+
outcome: "skipped"
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
case "buffer":
|
|
491
|
+
yield* store.recordResult({
|
|
492
|
+
triggerId: trigger.id,
|
|
493
|
+
occurrence,
|
|
494
|
+
outcome: "buffered"
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
case "supersede": {
|
|
498
|
+
const superseded = claim.activeRunId;
|
|
499
|
+
if (superseded !== void 0) {
|
|
500
|
+
const local = yield* active.get(trigger.id);
|
|
501
|
+
yield* cancelActive(
|
|
502
|
+
trigger,
|
|
503
|
+
local !== void 0 && local.runId === superseded ? { ...local, runId: superseded } : {
|
|
504
|
+
occurrence: yield* occurrenceOf(trigger.id, superseded),
|
|
505
|
+
runId: superseded
|
|
506
|
+
},
|
|
507
|
+
occurrence,
|
|
508
|
+
claim.reservationId,
|
|
509
|
+
!resumeBuffered
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
yield* launch(trigger, occurrence, claim.reservationId, resumeBuffered);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
const claimOnce = (trigger, occurrence) => store.claimFire({
|
|
518
|
+
triggerId: trigger.id,
|
|
519
|
+
occurrence,
|
|
520
|
+
expectedRevision: trigger.revision
|
|
521
|
+
}).pipe(
|
|
522
|
+
Effect.flatMap((claim) => claim.claimed ? dispatchClaimed(trigger, occurrence, claim) : Effect.void)
|
|
523
|
+
);
|
|
524
|
+
const withRevisionRefresh = (trigger, claim) => claim(trigger).pipe(
|
|
525
|
+
Effect.catch(
|
|
526
|
+
(error) => error.code !== "revision_mismatch" ? Effect.fail(error) : store.get(trigger.id).pipe(
|
|
527
|
+
Effect.flatMap(
|
|
528
|
+
(refreshed) => Option.isNone(refreshed) || refreshed.value.revision === trigger.revision ? Effect.fail(error) : claim(refreshed.value)
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
const resumePending = (trigger, held) => (
|
|
534
|
+
// A listed row holding neither a buffered occurrence nor a run has
|
|
535
|
+
// nothing for `claimPending` to take: only expiring a reservation
|
|
536
|
+
// re-arms a pending occurrence, and there is no reservation to expire.
|
|
537
|
+
held !== void 0 && held.pendingAt === void 0 && held.activeRunId === void 0 ? Effect.void : withRevisionRefresh(trigger, (current) => store.claimPending({
|
|
538
|
+
triggerId: current.id,
|
|
539
|
+
expectedRevision: current.revision
|
|
540
|
+
}).pipe(
|
|
541
|
+
Effect.flatMap((pending) => {
|
|
542
|
+
if (Option.isNone(pending)) return Effect.void;
|
|
543
|
+
const occurrence = pending.value.occurrence;
|
|
544
|
+
const claim = pending.value.claim;
|
|
545
|
+
if (!claim.claimed) return Effect.void;
|
|
546
|
+
return dispatchClaimed(
|
|
547
|
+
current,
|
|
548
|
+
occurrence,
|
|
549
|
+
claim,
|
|
550
|
+
true
|
|
551
|
+
).pipe(
|
|
552
|
+
Effect.onError(
|
|
553
|
+
() => isolate(
|
|
554
|
+
{ triggerId: current.id },
|
|
555
|
+
"buffered launch compensation",
|
|
556
|
+
claim.action === "fire" || claim.action === "supersede" ? store.restorePending({ triggerId: current.id, occurrence, reservationId: claim.reservationId }) : store.setPending({ triggerId: current.id, occurrence })
|
|
557
|
+
).pipe(Effect.ignore)
|
|
558
|
+
)
|
|
559
|
+
);
|
|
560
|
+
})
|
|
561
|
+
))
|
|
562
|
+
);
|
|
563
|
+
const processTrigger = (trigger, held, refreshed = false) => Effect.gen(function* () {
|
|
564
|
+
const now = yield* Clock.currentTimeMillis;
|
|
565
|
+
const running = yield* resolveActive(trigger, held);
|
|
566
|
+
if (!trigger.enabled) return;
|
|
567
|
+
if (running === void 0 || trigger.overlap === "supersede") yield* resumePending(trigger, held);
|
|
568
|
+
const observed = (yield* Ref.get(observedAt)).get(trigger.id);
|
|
569
|
+
const due = yield* DueOccurrences.compute(trigger, now, observed);
|
|
570
|
+
let dispatched;
|
|
571
|
+
let interrupted = false;
|
|
572
|
+
let stale = false;
|
|
573
|
+
for (const occurrence of due.occurrences) {
|
|
574
|
+
const settledHere = yield* attempted(
|
|
575
|
+
{ triggerId: trigger.id },
|
|
576
|
+
`dispatch of occurrence ${occurrence}`,
|
|
577
|
+
claimOnce(trigger, occurrence).pipe(
|
|
578
|
+
Effect.catch(
|
|
579
|
+
(error) => error.code === "revision_mismatch" ? Effect.sync(() => {
|
|
580
|
+
stale = true;
|
|
581
|
+
}) : Effect.fail(error)
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
if (stale) break;
|
|
586
|
+
if (settledHere && !interrupted) dispatched = occurrence;
|
|
587
|
+
if (!settledHere) interrupted = true;
|
|
588
|
+
}
|
|
589
|
+
if (stale) {
|
|
590
|
+
if (dispatched !== void 0) yield* observe(trigger.id, dispatched);
|
|
591
|
+
if (refreshed) return;
|
|
592
|
+
const current = yield* store.get(trigger.id);
|
|
593
|
+
if (Option.isNone(current) || current.value.revision === trigger.revision) return;
|
|
594
|
+
return yield* processTrigger(current.value, void 0, true);
|
|
595
|
+
}
|
|
596
|
+
if (!interrupted) return yield* observe(trigger.id, due.watermark);
|
|
597
|
+
if (dispatched !== void 0) yield* observe(trigger.id, dispatched);
|
|
598
|
+
}).pipe(Effect.withSpan("Scheduler.processTrigger", { attributes: { triggerId: trigger.id } }));
|
|
599
|
+
const host = options.host ?? defaultHost;
|
|
600
|
+
const runOnce = semaphore.withPermits(1)(
|
|
601
|
+
Effect.gen(function* () {
|
|
602
|
+
yield* isolate({ host }, "heartbeat", store.heartbeat(host));
|
|
603
|
+
const now = yield* Clock.currentTimeMillis;
|
|
604
|
+
const last = yield* Ref.get(lastPrunedAt);
|
|
605
|
+
if (last === void 0 || now - last >= pruneEvery) {
|
|
606
|
+
yield* Ref.set(lastPrunedAt, now);
|
|
607
|
+
yield* isolate(
|
|
608
|
+
{ host },
|
|
609
|
+
"fire ledger prune",
|
|
610
|
+
Effect.asVoid(store.pruneFires({ olderThan: now - Duration.toMillis(fireRetention) }))
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const listing = yield* store.list();
|
|
614
|
+
yield* Effect.forEach(
|
|
615
|
+
listing,
|
|
616
|
+
(row) => isolate(
|
|
617
|
+
{ triggerId: row.triggerId },
|
|
618
|
+
"tick",
|
|
619
|
+
Result.isFailure(row.trigger) ? Effect.fail(row.trigger.failure) : processTrigger(row.trigger.success, row)
|
|
620
|
+
),
|
|
621
|
+
{ concurrency, discard: true }
|
|
622
|
+
);
|
|
623
|
+
}).pipe((tick) => timed(tickDuration, tick), Effect.withSpan("Scheduler.runOnce", { attributes: { host } }))
|
|
624
|
+
);
|
|
625
|
+
return Scheduler.of({ runOnce });
|
|
626
|
+
});
|
|
627
|
+
const makeNoop = () => Scheduler.of({ runOnce: Effect.void });
|
|
628
|
+
const layer = (options = {}) => Layer.effect(
|
|
629
|
+
Scheduler,
|
|
630
|
+
Effect.gen(function* () {
|
|
631
|
+
const scheduler = yield* make(options);
|
|
632
|
+
const pollInterval = yield* duration(options.pollInterval ?? "1 minute", "pollInterval");
|
|
633
|
+
yield* Effect.forkScoped(
|
|
634
|
+
Effect.forever(
|
|
635
|
+
scheduler.runOnce.pipe(
|
|
636
|
+
Effect.catchCause(
|
|
637
|
+
(cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.logWarning("A trigger scheduler tick failed", cause)
|
|
638
|
+
),
|
|
639
|
+
Effect.andThen(Effect.sleep(pollInterval))
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
return scheduler;
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
const layerNoop = Layer.succeed(Scheduler)(makeNoop());
|
|
647
|
+
//# sourceMappingURL=Scheduler.js.map
|