@smthrs/triggers 0.0.0-stage → 1.0.0-rc.4
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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one claim decision both trigger stores apply.
|
|
3
|
+
*
|
|
4
|
+
* A claim is a pure function of the rows it reads: the trigger declaration,
|
|
5
|
+
* the fire row at the occurrence being claimed, and the fire row the active
|
|
6
|
+
* reservation holds. Each store gathers that snapshot in its own storage,
|
|
7
|
+
* calls {@link decide}, and applies the writes it answers. Writing the fences,
|
|
8
|
+
* the expired-reservation reclaim, and the supersede predecessor substitution
|
|
9
|
+
* once is what keeps `SqlTriggerStore` and the published in-memory store from
|
|
10
|
+
* drifting apart.
|
|
11
|
+
*
|
|
12
|
+
* @since 1.0.0-rc.0
|
|
13
|
+
*/
|
|
14
|
+
import type { Overlap as Policy } from "./Trigger.ts";
|
|
15
|
+
import { TriggerError } from "./TriggerError.ts";
|
|
16
|
+
import { type Claim, type ClaimFire, type Outcome } from "./TriggerStore.ts";
|
|
17
|
+
/**
|
|
18
|
+
* A fire row as the decision reads it. `outcome` is `null` for a claimed but
|
|
19
|
+
* unsettled occurrence, matching the nullable column.
|
|
20
|
+
*
|
|
21
|
+
* @category models
|
|
22
|
+
* @since 1.0.0-rc.0
|
|
23
|
+
*/
|
|
24
|
+
export interface Fire {
|
|
25
|
+
readonly outcome: Outcome | null;
|
|
26
|
+
readonly runId?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Everything the decision reads, gathered by the store before it runs.
|
|
30
|
+
*
|
|
31
|
+
* `existingFire` is absent when this claim is what created the row.
|
|
32
|
+
* `activeFire` is the row at `reservationOccurrence(activeRunId)`, read after
|
|
33
|
+
* that insert, and is needed only while `activeRunId` is a reservation.
|
|
34
|
+
*
|
|
35
|
+
* @category models
|
|
36
|
+
* @since 1.0.0-rc.0
|
|
37
|
+
*/
|
|
38
|
+
export interface Snapshot {
|
|
39
|
+
readonly revision: number;
|
|
40
|
+
readonly enabled: boolean;
|
|
41
|
+
readonly overlap: Policy;
|
|
42
|
+
readonly activeRunId?: string | undefined;
|
|
43
|
+
readonly activeClaimedAt?: number | undefined;
|
|
44
|
+
readonly pending?: number | undefined;
|
|
45
|
+
readonly existingFire?: Fire | undefined;
|
|
46
|
+
readonly activeFire?: Fire | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One durable edit a claim owes, in the order the store must apply them.
|
|
50
|
+
*
|
|
51
|
+
* `SetOutcome` with `whileOpen` may only settle a fire that is still `null` or
|
|
52
|
+
* `buffered`; the SQL store spells that as a `WHERE` clause.
|
|
53
|
+
* `ReleaseReservation` may only land while `expected` still owns the trigger.
|
|
54
|
+
*
|
|
55
|
+
* @category models
|
|
56
|
+
* @since 1.0.0-rc.0
|
|
57
|
+
*/
|
|
58
|
+
export type Write = {
|
|
59
|
+
readonly _tag: "SetOutcome";
|
|
60
|
+
readonly occurrence: number;
|
|
61
|
+
readonly outcome: Outcome;
|
|
62
|
+
readonly whileOpen: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
readonly _tag: "SetRunId";
|
|
65
|
+
readonly occurrence: number;
|
|
66
|
+
readonly runId: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly _tag: "ReleaseReservation";
|
|
69
|
+
readonly expected: string;
|
|
70
|
+
readonly activeRunId?: string | undefined;
|
|
71
|
+
readonly pending?: number | undefined;
|
|
72
|
+
} | {
|
|
73
|
+
readonly _tag: "AdvanceCursor";
|
|
74
|
+
readonly occurrence: number;
|
|
75
|
+
} | {
|
|
76
|
+
readonly _tag: "SetPending";
|
|
77
|
+
readonly occurrence: number;
|
|
78
|
+
} | {
|
|
79
|
+
readonly _tag: "Reserve";
|
|
80
|
+
readonly reservationId: string;
|
|
81
|
+
readonly claimedAt: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* What a claim decided: a refusal the store fails with, or a claim outcome and
|
|
85
|
+
* the writes that make it durable. An unclaimed occurrence answers
|
|
86
|
+
* `{ claimed: false }` with no writes.
|
|
87
|
+
*
|
|
88
|
+
* @category models
|
|
89
|
+
* @since 1.0.0-rc.0
|
|
90
|
+
*/
|
|
91
|
+
export type Decision = {
|
|
92
|
+
readonly _tag: "Refused";
|
|
93
|
+
readonly error: TriggerError;
|
|
94
|
+
} | {
|
|
95
|
+
readonly _tag: "Decided";
|
|
96
|
+
readonly claim: Claim;
|
|
97
|
+
readonly writes: ReadonlyArray<Write>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The claim request, the rows it reads, the reservation id the store minted
|
|
101
|
+
* for it, and the store clock reading it was claimed at.
|
|
102
|
+
*
|
|
103
|
+
* @category models
|
|
104
|
+
* @since 1.0.0-rc.0
|
|
105
|
+
*/
|
|
106
|
+
export interface Input {
|
|
107
|
+
readonly fire: ClaimFire;
|
|
108
|
+
readonly snapshot: Snapshot;
|
|
109
|
+
readonly reservationId: string;
|
|
110
|
+
readonly claimedAt: number;
|
|
111
|
+
}
|
|
112
|
+
type AdmittedDecision = Extract<Decision, {
|
|
113
|
+
readonly _tag: "Decided";
|
|
114
|
+
}>;
|
|
115
|
+
/**
|
|
116
|
+
* What a reader finds behind a trigger's active owner: nothing, a live owner,
|
|
117
|
+
* or an expired reservation to release.
|
|
118
|
+
*
|
|
119
|
+
* `expected` is the reservation the release must still find in place.
|
|
120
|
+
* `recovered` is the launched predecessor the release hands back, and
|
|
121
|
+
* `pending` is the occurrence it re-arms; both are absent when the reservation
|
|
122
|
+
* covered no unsettled fire.
|
|
123
|
+
*
|
|
124
|
+
* @category models
|
|
125
|
+
* @since 1.0.0-rc.0
|
|
126
|
+
*/
|
|
127
|
+
export type Lease = {
|
|
128
|
+
readonly _tag: "Idle";
|
|
129
|
+
} | {
|
|
130
|
+
readonly _tag: "Held";
|
|
131
|
+
readonly runId: string;
|
|
132
|
+
} | {
|
|
133
|
+
readonly _tag: "Expired";
|
|
134
|
+
readonly expected: string;
|
|
135
|
+
readonly recovered?: string | undefined;
|
|
136
|
+
readonly pending?: number | undefined;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Applies the reservation lease to a trigger's active owner.
|
|
140
|
+
*
|
|
141
|
+
* `unfinished` is the fire row at `reservationOccurrence(activeRunId)` while
|
|
142
|
+
* that row is still `null` or `buffered`, and is absent otherwise. A process
|
|
143
|
+
* that died after claiming an occurrence but before launching it left the
|
|
144
|
+
* reservation; the expired lease releases it and re-arms both ordinary and
|
|
145
|
+
* buffered work.
|
|
146
|
+
*
|
|
147
|
+
* @category decision
|
|
148
|
+
* @since 1.0.0-rc.0
|
|
149
|
+
*/
|
|
150
|
+
export declare const lease: (input: {
|
|
151
|
+
readonly activeRunId?: string | undefined;
|
|
152
|
+
readonly activeClaimedAt?: number | undefined;
|
|
153
|
+
readonly pending?: number | undefined;
|
|
154
|
+
readonly now: number;
|
|
155
|
+
readonly unfinished?: Fire | undefined;
|
|
156
|
+
}) => Lease;
|
|
157
|
+
/**
|
|
158
|
+
* The refusals a claim owes before it reads any fire row, in the order the
|
|
159
|
+
* stores apply them. A store checks these first so a refused claim leaves the
|
|
160
|
+
* ledger untouched; {@link decide} re-applies them so a store that only calls
|
|
161
|
+
* it is still fenced.
|
|
162
|
+
*
|
|
163
|
+
* @category decision
|
|
164
|
+
* @since 1.0.0-rc.0
|
|
165
|
+
*/
|
|
166
|
+
export declare const refuse: (snapshot: Pick<Snapshot, "enabled" | "revision">, fire: ClaimFire) => TriggerError | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* Applies the claim protocol to a snapshot.
|
|
169
|
+
*
|
|
170
|
+
* @category decision
|
|
171
|
+
* @since 1.0.0-rc.0
|
|
172
|
+
*/
|
|
173
|
+
export declare const decide: (input: Input) => Decision;
|
|
174
|
+
/**
|
|
175
|
+
* Decides over a snapshot already admitted by {@link refuse} in the same
|
|
176
|
+
* transaction. This avoids checking the same immutable revision twice.
|
|
177
|
+
* @category decision
|
|
178
|
+
* @since 1.0.0-rc.0
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
export declare const decideFenced: ({ claimedAt, fire, reservationId, snapshot }: Input) => AdmittedDecision;
|
|
182
|
+
export {};
|
|
183
|
+
//# sourceMappingURL=ClaimDecision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaimDecision.d.ts","sourceRoot":"","sources":["../../src/ClaimDecision.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
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 ClaimDecision_exports = {};
|
|
30
|
+
__export(ClaimDecision_exports, {
|
|
31
|
+
decide: () => decide,
|
|
32
|
+
decideFenced: () => decideFenced,
|
|
33
|
+
lease: () => lease,
|
|
34
|
+
refuse: () => refuse
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(ClaimDecision_exports);
|
|
37
|
+
var Overlap = __toESM(require("./Overlap.js"), 1);
|
|
38
|
+
var import_TriggerError = require("./TriggerError.js");
|
|
39
|
+
var import_TriggerStore = require("./TriggerStore.js");
|
|
40
|
+
const decided = (claim, writes) => ({ _tag: "Decided", claim, writes });
|
|
41
|
+
const lease = (input) => {
|
|
42
|
+
const { activeClaimedAt, activeRunId, now, pending, unfinished } = input;
|
|
43
|
+
if (activeRunId === void 0) return { _tag: "Idle" };
|
|
44
|
+
if (!(0, import_TriggerStore.isReservation)(activeRunId) || activeClaimedAt !== void 0 && activeClaimedAt > now - import_TriggerStore.reservationLeaseMs) {
|
|
45
|
+
return { _tag: "Held", runId: activeRunId };
|
|
46
|
+
}
|
|
47
|
+
const occurrence = (0, import_TriggerStore.reservationOccurrence)(activeRunId);
|
|
48
|
+
if (occurrence === void 0 || unfinished === void 0) return { _tag: "Expired", expected: activeRunId };
|
|
49
|
+
const predecessor = unfinished.runId;
|
|
50
|
+
return {
|
|
51
|
+
_tag: "Expired",
|
|
52
|
+
expected: activeRunId,
|
|
53
|
+
pending: Overlap.pendingAfter({ running: false, pending, due: occurrence }),
|
|
54
|
+
...predecessor !== void 0 && !(0, import_TriggerStore.isReservation)(predecessor) ? { recovered: predecessor } : {}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const refuse = (snapshot, fire) => {
|
|
58
|
+
if (snapshot.revision !== fire.expectedRevision) {
|
|
59
|
+
return new import_TriggerError.TriggerError({
|
|
60
|
+
code: "revision_mismatch",
|
|
61
|
+
message: `trigger ${fire.triggerId} is at revision ${snapshot.revision}, not the claimed ${fire.expectedRevision}`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (!snapshot.enabled) {
|
|
65
|
+
return new import_TriggerError.TriggerError({ code: "trigger_disabled", message: `trigger ${fire.triggerId} is disabled` });
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
};
|
|
69
|
+
const decide = (input) => {
|
|
70
|
+
const refusal = refuse(input.snapshot, input.fire);
|
|
71
|
+
return refusal === void 0 ? decideFenced(input) : { _tag: "Refused", error: refusal };
|
|
72
|
+
};
|
|
73
|
+
const decideFenced = ({ claimedAt, fire, reservationId, snapshot }) => {
|
|
74
|
+
let activeRunId = snapshot.activeRunId;
|
|
75
|
+
let pending = snapshot.pending;
|
|
76
|
+
const expiredReservation = activeRunId !== void 0 && (0, import_TriggerStore.isReservation)(activeRunId) && (snapshot.activeClaimedAt === void 0 || snapshot.activeClaimedAt <= claimedAt - import_TriggerStore.reservationLeaseMs) ? activeRunId : void 0;
|
|
77
|
+
const existing = snapshot.existingFire;
|
|
78
|
+
if (existing !== void 0) {
|
|
79
|
+
const resumableBuffer = fire.resumeBuffered === true && existing.outcome === "buffered";
|
|
80
|
+
const resumableReservation = existing.outcome === null && (activeRunId === void 0 || (0, import_TriggerStore.reservationOccurrence)(activeRunId) === fire.occurrence && expiredReservation !== void 0);
|
|
81
|
+
const resumableSupersede = fire.resumeBuffered === true && existing.outcome === null && snapshot.overlap === "supersede" && activeRunId !== void 0 && existing.runId === activeRunId;
|
|
82
|
+
if (!resumableBuffer && !resumableReservation && !resumableSupersede) return decided({ claimed: false }, []);
|
|
83
|
+
}
|
|
84
|
+
const writes = [];
|
|
85
|
+
if (expiredReservation !== void 0) {
|
|
86
|
+
const expiredOccurrence = (0, import_TriggerStore.reservationOccurrence)(expiredReservation);
|
|
87
|
+
const expiredFire = expiredOccurrence === void 0 ? void 0 : snapshot.activeFire;
|
|
88
|
+
if (expiredOccurrence !== void 0 && expiredFire !== void 0 && (expiredFire.outcome === null || expiredFire.outcome === "buffered")) {
|
|
89
|
+
if (snapshot.overlap === "supersede") {
|
|
90
|
+
const predecessor = expiredFire.runId;
|
|
91
|
+
activeRunId = predecessor !== void 0 && !(0, import_TriggerStore.isReservation)(predecessor) ? predecessor : void 0;
|
|
92
|
+
if (expiredOccurrence !== fire.occurrence) {
|
|
93
|
+
writes.push({ _tag: "SetOutcome", occurrence: expiredOccurrence, outcome: "superseded", whileOpen: false });
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
activeRunId = void 0;
|
|
97
|
+
if (expiredOccurrence !== fire.occurrence) {
|
|
98
|
+
pending = Overlap.pendingAfter({ running: false, pending, due: expiredOccurrence });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
activeRunId = void 0;
|
|
103
|
+
}
|
|
104
|
+
writes.push({ _tag: "ReleaseReservation", expected: expiredReservation, activeRunId, pending });
|
|
105
|
+
}
|
|
106
|
+
const state = { running: activeRunId !== void 0, pending, due: fire.occurrence };
|
|
107
|
+
const action = Overlap.decide(snapshot.overlap, state);
|
|
108
|
+
if (action === "skip" || action === "buffer") {
|
|
109
|
+
writes.push({
|
|
110
|
+
_tag: "SetOutcome",
|
|
111
|
+
occurrence: fire.occurrence,
|
|
112
|
+
outcome: action === "skip" ? "skipped" : "buffered",
|
|
113
|
+
whileOpen: false
|
|
114
|
+
});
|
|
115
|
+
writes.push({ _tag: "AdvanceCursor", occurrence: fire.occurrence });
|
|
116
|
+
if (action === "buffer") writes.push({ _tag: "SetPending", occurrence: Overlap.pendingAfter(state) });
|
|
117
|
+
return decided({ claimed: true, action }, writes);
|
|
118
|
+
}
|
|
119
|
+
let supersededRunId = activeRunId;
|
|
120
|
+
if (action === "supersede" && activeRunId !== void 0) {
|
|
121
|
+
if ((0, import_TriggerStore.isReservation)(activeRunId)) {
|
|
122
|
+
const activeOccurrence = (0, import_TriggerStore.reservationOccurrence)(activeRunId);
|
|
123
|
+
if (activeOccurrence !== void 0) {
|
|
124
|
+
const predecessor = snapshot.activeFire?.runId;
|
|
125
|
+
if (predecessor !== void 0 && !(0, import_TriggerStore.isReservation)(predecessor)) supersededRunId = predecessor;
|
|
126
|
+
writes.push({ _tag: "SetOutcome", occurrence: activeOccurrence, outcome: "superseded", whileOpen: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (supersededRunId !== void 0 && !(0, import_TriggerStore.isReservation)(supersededRunId)) {
|
|
130
|
+
writes.push({ _tag: "SetRunId", occurrence: fire.occurrence, runId: supersededRunId });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
writes.push({ _tag: "Reserve", reservationId, claimedAt });
|
|
134
|
+
return decided(
|
|
135
|
+
{
|
|
136
|
+
claimed: true,
|
|
137
|
+
action,
|
|
138
|
+
reservationId,
|
|
139
|
+
...supersededRunId === void 0 ? {} : { activeRunId: supersededRunId }
|
|
140
|
+
},
|
|
141
|
+
writes
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=ClaimDecision.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ClaimDecision.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The one claim decision both trigger stores apply.\n *\n * A claim is a pure function of the rows it reads: the trigger declaration,\n * the fire row at the occurrence being claimed, and the fire row the active\n * reservation holds. Each store gathers that snapshot in its own storage,\n * calls {@link decide}, and applies the writes it answers. Writing the fences,\n * the expired-reservation reclaim, and the supersede predecessor substitution\n * once is what keeps `SqlTriggerStore` and the published in-memory store from\n * drifting apart.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Overlap from \"./Overlap.ts\"\nimport type { Overlap as Policy } from \"./Trigger.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\nimport {\n type Claim,\n type ClaimFire,\n isReservation,\n type Outcome,\n reservationLeaseMs,\n reservationOccurrence\n} from \"./TriggerStore.ts\"\n\n/**\n * A fire row as the decision reads it. `outcome` is `null` for a claimed but\n * unsettled occurrence, matching the nullable column.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Fire {\n readonly outcome: Outcome | null\n readonly runId?: string | undefined\n}\n\n/**\n * Everything the decision reads, gathered by the store before it runs.\n *\n * `existingFire` is absent when this claim is what created the row.\n * `activeFire` is the row at `reservationOccurrence(activeRunId)`, read after\n * that insert, and is needed only while `activeRunId` is a reservation.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Snapshot {\n readonly revision: number\n readonly enabled: boolean\n readonly overlap: Policy\n readonly activeRunId?: string | undefined\n readonly activeClaimedAt?: number | undefined\n readonly pending?: number | undefined\n readonly existingFire?: Fire | undefined\n readonly activeFire?: Fire | undefined\n}\n\n/**\n * One durable edit a claim owes, in the order the store must apply them.\n *\n * `SetOutcome` with `whileOpen` may only settle a fire that is still `null` or\n * `buffered`; the SQL store spells that as a `WHERE` clause.\n * `ReleaseReservation` may only land while `expected` still owns the trigger.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Write =\n | {\n readonly _tag: \"SetOutcome\"\n readonly occurrence: number\n readonly outcome: Outcome\n readonly whileOpen: boolean\n }\n | { readonly _tag: \"SetRunId\"; readonly occurrence: number; readonly runId: string }\n | {\n readonly _tag: \"ReleaseReservation\"\n readonly expected: string\n readonly activeRunId?: string | undefined\n readonly pending?: number | undefined\n }\n | { readonly _tag: \"AdvanceCursor\"; readonly occurrence: number }\n | { readonly _tag: \"SetPending\"; readonly occurrence: number }\n | { readonly _tag: \"Reserve\"; readonly reservationId: string; readonly claimedAt: number }\n\n/**\n * What a claim decided: a refusal the store fails with, or a claim outcome and\n * the writes that make it durable. An unclaimed occurrence answers\n * `{ claimed: false }` with no writes.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Decision =\n | { readonly _tag: \"Refused\"; readonly error: TriggerError }\n | { readonly _tag: \"Decided\"; readonly claim: Claim; readonly writes: ReadonlyArray<Write> }\n\n/**\n * The claim request, the rows it reads, the reservation id the store minted\n * for it, and the store clock reading it was claimed at.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Input {\n readonly fire: ClaimFire\n readonly snapshot: Snapshot\n readonly reservationId: string\n readonly claimedAt: number\n}\n\ntype AdmittedDecision = Extract<Decision, { readonly _tag: \"Decided\" }>\n\nconst decided = (claim: Claim, writes: ReadonlyArray<Write>): AdmittedDecision => ({ _tag: \"Decided\", claim, writes })\n\n/**\n * What a reader finds behind a trigger's active owner: nothing, a live owner,\n * or an expired reservation to release.\n *\n * `expected` is the reservation the release must still find in place.\n * `recovered` is the launched predecessor the release hands back, and\n * `pending` is the occurrence it re-arms; both are absent when the reservation\n * covered no unsettled fire.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Lease =\n | { readonly _tag: \"Idle\" }\n | { readonly _tag: \"Held\"; readonly runId: string }\n | {\n readonly _tag: \"Expired\"\n readonly expected: string\n readonly recovered?: string | undefined\n readonly pending?: number | undefined\n }\n\n/**\n * Applies the reservation lease to a trigger's active owner.\n *\n * `unfinished` is the fire row at `reservationOccurrence(activeRunId)` while\n * that row is still `null` or `buffered`, and is absent otherwise. A process\n * that died after claiming an occurrence but before launching it left the\n * reservation; the expired lease releases it and re-arms both ordinary and\n * buffered work.\n *\n * @category decision\n * @since 1.0.0-rc.0\n */\nexport const lease = (input: {\n readonly activeRunId?: string | undefined\n readonly activeClaimedAt?: number | undefined\n readonly pending?: number | undefined\n readonly now: number\n readonly unfinished?: Fire | undefined\n}): Lease => {\n const { activeClaimedAt, activeRunId, now, pending, unfinished } = input\n if (activeRunId === undefined) return { _tag: \"Idle\" }\n if (!isReservation(activeRunId) || (activeClaimedAt !== undefined && activeClaimedAt > now - reservationLeaseMs)) {\n return { _tag: \"Held\", runId: activeRunId }\n }\n const occurrence = reservationOccurrence(activeRunId)\n if (occurrence === undefined || unfinished === undefined) return { _tag: \"Expired\", expected: activeRunId }\n const predecessor = unfinished.runId\n return {\n _tag: \"Expired\",\n expected: activeRunId,\n pending: Overlap.pendingAfter({ running: false, pending, due: occurrence }),\n ...(predecessor !== undefined && !isReservation(predecessor) ? { recovered: predecessor } : {})\n }\n}\n\n/**\n * The refusals a claim owes before it reads any fire row, in the order the\n * stores apply them. A store checks these first so a refused claim leaves the\n * ledger untouched; {@link decide} re-applies them so a store that only calls\n * it is still fenced.\n *\n * @category decision\n * @since 1.0.0-rc.0\n */\nexport const refuse = (\n snapshot: Pick<Snapshot, \"enabled\" | \"revision\">,\n fire: ClaimFire\n): TriggerError | undefined => {\n if (snapshot.revision !== fire.expectedRevision) {\n return new TriggerError({\n code: \"revision_mismatch\",\n message: `trigger ${fire.triggerId} is at revision ${snapshot.revision}, not the claimed ${fire.expectedRevision}`\n })\n }\n if (!snapshot.enabled) {\n return new TriggerError({ code: \"trigger_disabled\", message: `trigger ${fire.triggerId} is disabled` })\n }\n return undefined\n}\n\n/**\n * Applies the claim protocol to a snapshot.\n *\n * @category decision\n * @since 1.0.0-rc.0\n */\nexport const decide = (input: Input): Decision => {\n const refusal = refuse(input.snapshot, input.fire)\n return refusal === undefined ? decideFenced(input) : { _tag: \"Refused\", error: refusal }\n}\n\n/**\n * Decides over a snapshot already admitted by {@link refuse} in the same\n * transaction. This avoids checking the same immutable revision twice.\n * @category decision\n * @since 1.0.0-rc.0\n * @private\n */\nexport const decideFenced = ({ claimedAt, fire, reservationId, snapshot }: Input): AdmittedDecision => {\n let activeRunId = snapshot.activeRunId\n let pending = snapshot.pending\n // A reservation with no claim timestamp predates the lease column. Nothing\n // writes that shape now, so treating it as expired is the only way such a\n // row is ever reclaimed.\n const expiredReservation = activeRunId !== undefined && isReservation(activeRunId) &&\n (snapshot.activeClaimedAt === undefined || snapshot.activeClaimedAt <= claimedAt - reservationLeaseMs)\n ? activeRunId\n : undefined\n const existing = snapshot.existingFire\n if (existing !== undefined) {\n const resumableBuffer = fire.resumeBuffered === true && existing.outcome === \"buffered\"\n const resumableReservation = existing.outcome === null &&\n (activeRunId === undefined ||\n (reservationOccurrence(activeRunId) === fire.occurrence && expiredReservation !== undefined))\n const resumableSupersede = fire.resumeBuffered === true && existing.outcome === null &&\n snapshot.overlap === \"supersede\" && activeRunId !== undefined && existing.runId === activeRunId\n if (!resumableBuffer && !resumableReservation && !resumableSupersede) return decided({ claimed: false }, [])\n }\n const writes: Array<Write> = []\n if (expiredReservation !== undefined) {\n const expiredOccurrence = reservationOccurrence(expiredReservation)\n const expiredFire = expiredOccurrence === undefined ? undefined : snapshot.activeFire\n if (\n expiredOccurrence !== undefined && expiredFire !== undefined &&\n (expiredFire.outcome === null || expiredFire.outcome === \"buffered\")\n ) {\n if (snapshot.overlap === \"supersede\") {\n const predecessor = expiredFire.runId\n activeRunId = predecessor !== undefined && !isReservation(predecessor) ? predecessor : undefined\n if (expiredOccurrence !== fire.occurrence) {\n writes.push({ _tag: \"SetOutcome\", occurrence: expiredOccurrence, outcome: \"superseded\", whileOpen: false })\n }\n } else {\n activeRunId = undefined\n if (expiredOccurrence !== fire.occurrence) {\n pending = Overlap.pendingAfter({ running: false, pending, due: expiredOccurrence })\n }\n }\n } else {\n activeRunId = undefined\n }\n writes.push({ _tag: \"ReleaseReservation\", expected: expiredReservation, activeRunId, pending })\n }\n const state: Overlap.State = { running: activeRunId !== undefined, pending, due: fire.occurrence }\n const action = Overlap.decide(snapshot.overlap, state)\n // A skip or buffer is complete inside the claim transaction. A fire or\n // supersede is only reserved here; its cursor advances when `recordResult`\n // makes the launched run durable.\n if (action === \"skip\" || action === \"buffer\") {\n writes.push({\n _tag: \"SetOutcome\",\n occurrence: fire.occurrence,\n outcome: action === \"skip\" ? \"skipped\" : \"buffered\",\n whileOpen: false\n })\n writes.push({ _tag: \"AdvanceCursor\", occurrence: fire.occurrence })\n if (action === \"buffer\") writes.push({ _tag: \"SetPending\", occurrence: Overlap.pendingAfter(state) })\n return decided({ claimed: true, action }, writes)\n }\n let supersededRunId = activeRunId\n if (action === \"supersede\" && activeRunId !== undefined) {\n if (isReservation(activeRunId)) {\n const activeOccurrence = reservationOccurrence(activeRunId)\n if (activeOccurrence !== undefined) {\n const predecessor = snapshot.activeFire?.runId\n if (predecessor !== undefined && !isReservation(predecessor)) supersededRunId = predecessor\n writes.push({ _tag: \"SetOutcome\", occurrence: activeOccurrence, outcome: \"superseded\", whileOpen: true })\n }\n }\n if (supersededRunId !== undefined && !isReservation(supersededRunId)) {\n writes.push({ _tag: \"SetRunId\", occurrence: fire.occurrence, runId: supersededRunId })\n }\n }\n writes.push({ _tag: \"Reserve\", reservationId, claimedAt })\n return decided(\n {\n claimed: true,\n action,\n reservationId,\n ...(supersededRunId === undefined ? {} : { activeRunId: supersededRunId })\n },\n writes\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed wrappers around Effect Cron.
|
|
3
|
+
*
|
|
4
|
+
* Effect's field parser only range-checks each field, so an expression like
|
|
5
|
+
* `0 0 30 2 *` parses and then never matches a calendar date. Its occurrence
|
|
6
|
+
* search answers that by exhausting its bound and throwing, which inside
|
|
7
|
+
* `Effect.gen` is a defect rather than a typed failure: the fiber that owns the
|
|
8
|
+
* search dies instead of reporting. Every search here is wrapped so the
|
|
9
|
+
* exhausted bound arrives as `unsatisfiable_cron`, and {@link parse} probes for
|
|
10
|
+
* one occurrence so an unsatisfiable expression is refused where it is
|
|
11
|
+
* declared rather than at the tick that would have fired it.
|
|
12
|
+
*
|
|
13
|
+
* A satisfiable expression carries the opposite hazard: `* * * * *` across a
|
|
14
|
+
* year is 525,600 occurrences, and materializing them costs seconds and
|
|
15
|
+
* hundreds of megabytes. {@link occurrencesBetween} therefore always searches
|
|
16
|
+
* under a finite cap and reports when an unstated limit would exceed
|
|
17
|
+
* {@link maxOccurrences}.
|
|
18
|
+
*
|
|
19
|
+
* A timezone makes the expression a wall-clock schedule. A wall time that
|
|
20
|
+
* spring forward skips fires at the instant it would have had on the old
|
|
21
|
+
* offset, so a daily 02:30 fires at 03:30 daylight time. A wall time that fall
|
|
22
|
+
* back repeats fires once, at its first instant, unless the hour field is every
|
|
23
|
+
* hour, in which case both passes fire. Occurrences are whole seconds in UTC,
|
|
24
|
+
* whatever the host's zone.
|
|
25
|
+
*
|
|
26
|
+
* @since 0.1.0
|
|
27
|
+
*/
|
|
28
|
+
import * as EffectCron from "effect/Cron";
|
|
29
|
+
import * as Effect from "effect/Effect";
|
|
30
|
+
import { TriggerError } from "./TriggerError.ts";
|
|
31
|
+
/**
|
|
32
|
+
* A parsed cron expression, kept beside the text it came from so the
|
|
33
|
+
* declaration can be round-tripped.
|
|
34
|
+
*
|
|
35
|
+
* @category models
|
|
36
|
+
* @since 0.1.0
|
|
37
|
+
*/
|
|
38
|
+
export interface Cron {
|
|
39
|
+
readonly expression: string;
|
|
40
|
+
readonly timezone?: string | undefined;
|
|
41
|
+
readonly value: EffectCron.Cron;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The first occurrence strictly after `from`.
|
|
45
|
+
*
|
|
46
|
+
* @category getters
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
export declare const next: (cron: Cron, from: Date) => Effect.Effect<Date, TriggerError>;
|
|
50
|
+
/**
|
|
51
|
+
* Returns the latest occurrence at or before `at`.
|
|
52
|
+
*
|
|
53
|
+
* @category getters
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
*/
|
|
56
|
+
export declare const previousAtOrBefore: (cron: Cron, at: Date) => Effect.Effect<Date, TriggerError>;
|
|
57
|
+
/**
|
|
58
|
+
* The greatest number of occurrences one search returns when its caller states
|
|
59
|
+
* no limit of its own.
|
|
60
|
+
*
|
|
61
|
+
* @category constants
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
*/
|
|
64
|
+
export declare const maxOccurrences = 1000;
|
|
65
|
+
/**
|
|
66
|
+
* The occurrences in `(from, to]`, in order.
|
|
67
|
+
*
|
|
68
|
+
* A stated `limit` caps the result silently and must be a non-negative safe
|
|
69
|
+
* integer. With no stated limit, the search fails with
|
|
70
|
+
* `catch_up_bound_exceeded` when the interval holds more than
|
|
71
|
+
* {@link maxOccurrences}. A caller with its own bound can pass one more than
|
|
72
|
+
* that bound and compare the length, the way `CatchUp.occurrences` does.
|
|
73
|
+
*
|
|
74
|
+
* @category sequencing
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export declare const occurrencesBetween: (cron: Cron, from: Date, to: Date, limit?: number | undefined) => Effect.Effect<ReadonlyArray<Date>, TriggerError>;
|
|
78
|
+
/**
|
|
79
|
+
* Parses a cron expression in an optional timezone, reporting a malformed one
|
|
80
|
+
* as `invalid_cron` and one the calendar never satisfies as
|
|
81
|
+
* `unsatisfiable_cron`.
|
|
82
|
+
*
|
|
83
|
+
* The satisfiability probe searches forward from the current instant, which is
|
|
84
|
+
* the same search every tick performs, so an expression that survives parsing
|
|
85
|
+
* is one the scheduler can keep answering.
|
|
86
|
+
*
|
|
87
|
+
* @category constructors
|
|
88
|
+
* @since 0.1.0
|
|
89
|
+
*/
|
|
90
|
+
export declare const parse: (expression: string, timezone?: string) => Effect.Effect<Cron, TriggerError>;
|
|
91
|
+
//# sourceMappingURL=Cron.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cron.d.ts","sourceRoot":"","sources":["../../src/Cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,UAAU,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,CAAA;CAChC;AAYD;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,IAAI,EAAE,MAAM,IAAI,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CACwB,CAAA;AAEtG;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,IAAI,EAAE,IAAI,IAAI,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAKvF,CAAA;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,IAAI,EACV,MAAM,IAAI,EACV,IAAI,IAAI,EACR,QAAQ,MAAM,GAAG,SAAS,KACzB,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,YAAY,CAoCjD,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,KAAK,GAAI,YAAY,MAAM,EAAE,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAgB1F,CAAA"}
|
package/dist/cjs/Cron.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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 Cron_exports = {};
|
|
30
|
+
__export(Cron_exports, {
|
|
31
|
+
maxOccurrences: () => maxOccurrences,
|
|
32
|
+
next: () => next,
|
|
33
|
+
occurrencesBetween: () => occurrencesBetween,
|
|
34
|
+
parse: () => parse,
|
|
35
|
+
previousAtOrBefore: () => previousAtOrBefore
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(Cron_exports);
|
|
38
|
+
var Clock = __toESM(require("effect/Clock"), 1);
|
|
39
|
+
var EffectCron = __toESM(require("effect/Cron"), 1);
|
|
40
|
+
var Effect = __toESM(require("effect/Effect"), 1);
|
|
41
|
+
var Result = __toESM(require("effect/Result"), 1);
|
|
42
|
+
var WallClock = __toESM(require("./internal/WallClock.js"), 1);
|
|
43
|
+
var import_TriggerError = require("./TriggerError.js");
|
|
44
|
+
const exhausted = (cron, search, cause) => new import_TriggerError.TriggerError({
|
|
45
|
+
code: "unsatisfiable_cron",
|
|
46
|
+
message: `cron expression '${cron.expression}' has no ${search} occurrence`,
|
|
47
|
+
cause
|
|
48
|
+
});
|
|
49
|
+
const attempt = (cron, search, find) => Effect.try({ try: find, catch: (cause) => exhausted(cron, search, cause) });
|
|
50
|
+
const next = (cron, from) => attempt(cron, "next", () => new Date(WallClock.next(WallClock.compile(cron.value), from.getTime())));
|
|
51
|
+
const previousAtOrBefore = (cron, at) => attempt(
|
|
52
|
+
cron,
|
|
53
|
+
"previous",
|
|
54
|
+
() => new Date(WallClock.previousAtOrBefore(WallClock.compile(cron.value), at.getTime()))
|
|
55
|
+
);
|
|
56
|
+
const maxOccurrences = 1e3;
|
|
57
|
+
const occurrencesBetween = (cron, from, to, limit) => {
|
|
58
|
+
if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
59
|
+
return Effect.fail(
|
|
60
|
+
new import_TriggerError.TriggerError({
|
|
61
|
+
code: "invalid_options",
|
|
62
|
+
message: `occurrence limit must be a non-negative safe integer, received ${limit}`,
|
|
63
|
+
path: "limit"
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const searchLimit = limit ?? maxOccurrences + 1;
|
|
68
|
+
if (searchLimit === 0) return Effect.succeed([]);
|
|
69
|
+
const search = attempt(cron, "interval", () => {
|
|
70
|
+
const wall = WallClock.compile(cron.value);
|
|
71
|
+
const occurrences = [];
|
|
72
|
+
let cursor = from.getTime();
|
|
73
|
+
while (occurrences.length < searchLimit) {
|
|
74
|
+
cursor = WallClock.next(wall, cursor);
|
|
75
|
+
if (cursor > to.getTime()) break;
|
|
76
|
+
occurrences.push(new Date(cursor));
|
|
77
|
+
}
|
|
78
|
+
return occurrences;
|
|
79
|
+
});
|
|
80
|
+
if (limit !== void 0) return search;
|
|
81
|
+
return search.pipe(
|
|
82
|
+
Effect.flatMap(
|
|
83
|
+
(occurrences) => occurrences.length > maxOccurrences ? Effect.fail(
|
|
84
|
+
new import_TriggerError.TriggerError({
|
|
85
|
+
code: "catch_up_bound_exceeded",
|
|
86
|
+
message: `interval contains more than ${maxOccurrences} occurrences; maxOccurrences is ${maxOccurrences}`
|
|
87
|
+
})
|
|
88
|
+
) : Effect.succeed(occurrences)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
const parse = (expression, timezone) => Effect.gen(function* () {
|
|
93
|
+
const parsed = EffectCron.parse(expression, timezone);
|
|
94
|
+
if (Result.isFailure(parsed)) {
|
|
95
|
+
return yield* Effect.fail(
|
|
96
|
+
new import_TriggerError.TriggerError({ code: "invalid_cron", message: parsed.failure.message, cause: parsed.failure })
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const cron = {
|
|
100
|
+
expression,
|
|
101
|
+
...timezone === void 0 ? {} : { timezone },
|
|
102
|
+
value: parsed.success
|
|
103
|
+
};
|
|
104
|
+
const now = yield* Clock.currentTimeMillis;
|
|
105
|
+
yield* next(cron, new Date(now));
|
|
106
|
+
return cron;
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=Cron.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/Cron.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Typed wrappers around Effect Cron.\n *\n * Effect's field parser only range-checks each field, so an expression like\n * `0 0 30 2 *` parses and then never matches a calendar date. Its occurrence\n * search answers that by exhausting its bound and throwing, which inside\n * `Effect.gen` is a defect rather than a typed failure: the fiber that owns the\n * search dies instead of reporting. Every search here is wrapped so the\n * exhausted bound arrives as `unsatisfiable_cron`, and {@link parse} probes for\n * one occurrence so an unsatisfiable expression is refused where it is\n * declared rather than at the tick that would have fired it.\n *\n * A satisfiable expression carries the opposite hazard: `* * * * *` across a\n * year is 525,600 occurrences, and materializing them costs seconds and\n * hundreds of megabytes. {@link occurrencesBetween} therefore always searches\n * under a finite cap and reports when an unstated limit would exceed\n * {@link maxOccurrences}.\n *\n * A timezone makes the expression a wall-clock schedule. A wall time that\n * spring forward skips fires at the instant it would have had on the old\n * offset, so a daily 02:30 fires at 03:30 daylight time. A wall time that fall\n * back repeats fires once, at its first instant, unless the hour field is every\n * hour, in which case both passes fire. Occurrences are whole seconds in UTC,\n * whatever the host's zone.\n *\n * @since 0.1.0\n */\n\nimport * as Clock from \"effect/Clock\"\nimport * as EffectCron from \"effect/Cron\"\nimport * as Effect from \"effect/Effect\"\nimport * as Result from \"effect/Result\"\nimport * as WallClock from \"./internal/WallClock.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * A parsed cron expression, kept beside the text it came from so the\n * declaration can be round-tripped.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Cron {\n readonly expression: string\n readonly timezone?: string | undefined\n readonly value: EffectCron.Cron\n}\n\nconst exhausted = (cron: Cron, search: string, cause: unknown): TriggerError =>\n new TriggerError({\n code: \"unsatisfiable_cron\",\n message: `cron expression '${cron.expression}' has no ${search} occurrence`,\n cause\n })\n\nconst attempt = <A>(cron: Cron, search: string, find: () => A): Effect.Effect<A, TriggerError> =>\n Effect.try({ try: find, catch: (cause) => exhausted(cron, search, cause) })\n\n/**\n * The first occurrence strictly after `from`.\n *\n * @category getters\n * @since 0.1.0\n */\nexport const next = (cron: Cron, from: Date): Effect.Effect<Date, TriggerError> =>\n attempt(cron, \"next\", () => new Date(WallClock.next(WallClock.compile(cron.value), from.getTime())))\n\n/**\n * Returns the latest occurrence at or before `at`.\n *\n * @category getters\n * @since 0.1.0\n */\nexport const previousAtOrBefore = (cron: Cron, at: Date): Effect.Effect<Date, TriggerError> =>\n attempt(\n cron,\n \"previous\",\n () => new Date(WallClock.previousAtOrBefore(WallClock.compile(cron.value), at.getTime()))\n )\n\n/**\n * The greatest number of occurrences one search returns when its caller states\n * no limit of its own.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const maxOccurrences = 1000\n\n/**\n * The occurrences in `(from, to]`, in order.\n *\n * A stated `limit` caps the result silently and must be a non-negative safe\n * integer. With no stated limit, the search fails with\n * `catch_up_bound_exceeded` when the interval holds more than\n * {@link maxOccurrences}. A caller with its own bound can pass one more than\n * that bound and compare the length, the way `CatchUp.occurrences` does.\n *\n * @category sequencing\n * @since 0.1.0\n */\nexport const occurrencesBetween = (\n cron: Cron,\n from: Date,\n to: Date,\n limit?: number | undefined\n): Effect.Effect<ReadonlyArray<Date>, TriggerError> => {\n if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {\n return Effect.fail(\n new TriggerError({\n code: \"invalid_options\",\n message: `occurrence limit must be a non-negative safe integer, received ${limit}`,\n path: \"limit\"\n })\n )\n }\n const searchLimit = limit ?? maxOccurrences + 1\n if (searchLimit === 0) return Effect.succeed([])\n const search = attempt(cron, \"interval\", () => {\n const wall = WallClock.compile(cron.value)\n const occurrences: Array<Date> = []\n let cursor = from.getTime()\n while (occurrences.length < searchLimit) {\n cursor = WallClock.next(wall, cursor)\n if (cursor > to.getTime()) break\n occurrences.push(new Date(cursor))\n }\n return occurrences\n })\n if (limit !== undefined) return search\n return search.pipe(\n Effect.flatMap((occurrences) =>\n occurrences.length > maxOccurrences\n ? Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `interval contains more than ${maxOccurrences} occurrences; maxOccurrences is ${maxOccurrences}`\n })\n )\n : Effect.succeed(occurrences)\n )\n )\n}\n\n/**\n * Parses a cron expression in an optional timezone, reporting a malformed one\n * as `invalid_cron` and one the calendar never satisfies as\n * `unsatisfiable_cron`.\n *\n * The satisfiability probe searches forward from the current instant, which is\n * the same search every tick performs, so an expression that survives parsing\n * is one the scheduler can keep answering.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const parse = (expression: string, timezone?: string): Effect.Effect<Cron, TriggerError> =>\n Effect.gen(function*() {\n const parsed = EffectCron.parse(expression, timezone)\n if (Result.isFailure(parsed)) {\n return yield* Effect.fail(\n new TriggerError({ code: \"invalid_cron\", message: parsed.failure.message, cause: parsed.failure })\n )\n }\n const cron: Cron = {\n expression,\n ...(timezone === undefined ? {} : { timezone }),\n value: parsed.success\n }\n const now = yield* Clock.currentTimeMillis\n yield* next(cron, new Date(now))\n return cron\n })\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BA,YAAuB;AACvB,iBAA4B;AAC5B,aAAwB;AACxB,aAAwB;AACxB,gBAA2B;AAC3B,0BAA6B;AAe7B,MAAM,YAAY,CAAC,MAAY,QAAgB,UAC7C,IAAI,iCAAa;AAAA,EACf,MAAM;AAAA,EACN,SAAS,oBAAoB,KAAK,UAAU,YAAY,MAAM;AAAA,EAC9D;AACF,CAAC;AAEH,MAAM,UAAU,CAAI,MAAY,QAAgB,SAC9C,OAAO,IAAI,EAAE,KAAK,MAAM,OAAO,CAAC,UAAU,UAAU,MAAM,QAAQ,KAAK,EAAE,CAAC;AAQrE,MAAM,OAAO,CAAC,MAAY,SAC/B,QAAQ,MAAM,QAAQ,MAAM,IAAI,KAAK,UAAU,KAAK,UAAU,QAAQ,KAAK,KAAK,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC;AAQ9F,MAAM,qBAAqB,CAAC,MAAY,OAC7C;AAAA,EACE;AAAA,EACA;AAAA,EACA,MAAM,IAAI,KAAK,UAAU,mBAAmB,UAAU,QAAQ,KAAK,KAAK,GAAG,GAAG,QAAQ,CAAC,CAAC;AAC1F;AASK,MAAM,iBAAiB;AAcvB,MAAM,qBAAqB,CAChC,MACA,MACA,IACA,UACqD;AACrD,MAAI,UAAU,WAAc,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,IAAI;AACtE,WAAO,OAAO;AAAA,MACZ,IAAI,iCAAa;AAAA,QACf,MAAM;AAAA,QACN,SAAS,kEAAkE,KAAK;AAAA,QAChF,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AACA,QAAM,cAAc,SAAS,iBAAiB;AAC9C,MAAI,gBAAgB,EAAG,QAAO,OAAO,QAAQ,CAAC,CAAC;AAC/C,QAAM,SAAS,QAAQ,MAAM,YAAY,MAAM;AAC7C,UAAM,OAAO,UAAU,QAAQ,KAAK,KAAK;AACzC,UAAM,cAA2B,CAAC;AAClC,QAAI,SAAS,KAAK,QAAQ;AAC1B,WAAO,YAAY,SAAS,aAAa;AACvC,eAAS,UAAU,KAAK,MAAM,MAAM;AACpC,UAAI,SAAS,GAAG,QAAQ,EAAG;AAC3B,kBAAY,KAAK,IAAI,KAAK,MAAM,CAAC;AAAA,IACnC;AACA,WAAO;AAAA,EACT,CAAC;AACD,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO;AAAA,IACZ,OAAO;AAAA,MAAQ,CAAC,gBACd,YAAY,SAAS,iBACjB,OAAO;AAAA,QACP,IAAI,iCAAa;AAAA,UACf,MAAM;AAAA,UACN,SAAS,+BAA+B,cAAc,mCAAmC,cAAc;AAAA,QACzG,CAAC;AAAA,MACH,IACE,OAAO,QAAQ,WAAW;AAAA,IAChC;AAAA,EACF;AACF;AAcO,MAAM,QAAQ,CAAC,YAAoB,aACxC,OAAO,IAAI,aAAY;AACrB,QAAM,SAAS,WAAW,MAAM,YAAY,QAAQ;AACpD,MAAI,OAAO,UAAU,MAAM,GAAG;AAC5B,WAAO,OAAO,OAAO;AAAA,MACnB,IAAI,iCAAa,EAAE,MAAM,gBAAgB,SAAS,OAAO,QAAQ,SAAS,OAAO,OAAO,QAAQ,CAAC;AAAA,IACnG;AAAA,EACF;AACA,QAAM,OAAa;AAAA,IACjB;AAAA,IACA,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;AAAA,IAC7C,OAAO,OAAO;AAAA,EAChB;AACA,QAAM,MAAM,OAAO,MAAM;AACzB,SAAO,KAAK,MAAM,IAAI,KAAK,GAAG,CAAC;AAC/B,SAAO;AACT,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|