@smthrs/triggers 1.0.0-rc.8 → 1.0.0-rc.9
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/dist/cjs/internal/DueOccurrences.d.ts.map +1 -1
- package/dist/cjs/internal/DueOccurrences.js +10 -29
- package/dist/cjs/internal/DueOccurrences.js.map +2 -2
- package/dist/esm/internal/DueOccurrences.d.ts.map +1 -1
- package/dist/esm/internal/DueOccurrences.js +30 -24
- package/dist/esm/internal/DueOccurrences.js.map +1 -1
- package/docs/api.md +9 -3
- package/docs/concepts/policies.md +5 -0
- package/package.json +4 -4
- package/src/internal/DueOccurrences.ts +30 -36
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DueOccurrences.d.ts","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAoBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAClB,SAAS,UAAU,EACnB,KAAK,MAAM,EACX,UAAU,MAAM,GAAG,SAAS,KAC3B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"DueOccurrences.d.ts","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAoBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAClB,SAAS,UAAU,EACnB,KAAK,MAAM,EACX,UAAU,MAAM,GAAG,SAAS,KAC3B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAqC9B,CAAA"}
|
|
@@ -44,37 +44,18 @@ const withinBound = (triggerId, owed) => Effect.catch(owed, (error) => error.cod
|
|
|
44
44
|
const compute = (trigger, now, observed) => Effect.gen(function* () {
|
|
45
45
|
const cron = yield* Cron.parse(trigger.cron, trigger.timezone);
|
|
46
46
|
const current = (yield* Cron.previousAtOrBefore(cron, new Date(now))).getTime();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const owed = yield* withinBound(
|
|
51
|
-
trigger.id,
|
|
52
|
-
CatchUp.occurrences(
|
|
53
|
-
trigger.catchUp,
|
|
54
|
-
trigger.maxCatchUp,
|
|
55
|
-
new Date(trigger.lastFiredAt),
|
|
56
|
-
new Date(now),
|
|
57
|
-
cron
|
|
58
|
-
)
|
|
59
|
-
);
|
|
60
|
-
const occurrences = owed.map((occurrence) => occurrence.getTime());
|
|
61
|
-
if (occurrences.at(-1) !== current) occurrences.push(current);
|
|
62
|
-
return { occurrences, watermark: current };
|
|
47
|
+
const since = observed ?? trigger.lastFiredAt;
|
|
48
|
+
if (since === void 0 || current <= since) {
|
|
49
|
+
return { occurrences: [], watermark: Math.max(current, observed ?? current) };
|
|
63
50
|
}
|
|
64
|
-
|
|
65
|
-
const
|
|
51
|
+
const lone = trigger.maxCatchUp === 0 && (yield* Cron.previousAtOrBefore(cron, new Date(current - 1))).getTime() <= since;
|
|
52
|
+
const bound = lone ? 1 : trigger.maxCatchUp;
|
|
53
|
+
const owed = yield* withinBound(
|
|
66
54
|
trigger.id,
|
|
67
|
-
CatchUp.occurrences(
|
|
68
|
-
trigger.catchUp,
|
|
69
|
-
trigger.maxCatchUp,
|
|
70
|
-
new Date(observed),
|
|
71
|
-
new Date(current - 1),
|
|
72
|
-
cron
|
|
73
|
-
)
|
|
55
|
+
CatchUp.occurrences(trigger.catchUp, bound, new Date(since), new Date(now), cron)
|
|
74
56
|
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
57
|
+
const occurrences = owed.map((occurrence) => occurrence.getTime());
|
|
58
|
+
if (occurrences.at(-1) !== current) occurrences.push(current);
|
|
59
|
+
return { occurrences, watermark: current };
|
|
79
60
|
});
|
|
80
61
|
//# sourceMappingURL=DueOccurrences.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/internal/DueOccurrences.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Which occurrences one scheduler tick owes a trigger.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport * as CatchUp from \"../CatchUp.ts\"\nimport * as Cron from \"../Cron.ts\"\nimport type { TriggerError } from \"../TriggerError.ts\"\nimport type { Registered } from \"../TriggerStore.ts\"\n\n/**\n * The occurrences to claim, oldest first, and the watermark that stands once\n * every one of them has been dispatched.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface Due {\n readonly occurrences: ReadonlyArray<number>\n readonly watermark: number\n}\n\n// A bound the declaration cannot honour is a statement about how much history\n// to replay, not a reason to stop scheduling: the backlog beyond the bound is\n// abandoned, loudly, and the current occurrence still fires.\nconst withinBound = (\n triggerId: string,\n owed: Effect.Effect<ReadonlyArray<Date>, TriggerError>\n): Effect.Effect<ReadonlyArray<Date>, TriggerError> =>\n Effect.catch(owed, (error) =>\n error.code === \"catch_up_bound_exceeded\"\n ? Effect.as(\n Effect.annotateLogs(\n Effect.logWarning(\"A trigger abandoned catch-up work beyond its bound\", error),\n { triggerId }\n ),\n [] as ReadonlyArray<Date>\n )\n : Effect.fail(error))\n\n/**\n * Computes what a trigger owes at `now`, given the watermark this process has\n * already dispatched through, or `undefined` on first sight of the trigger.\n *\n * The answer depends on nothing but its arguments. The watermark is the\n * caller's to keep: it only moves forward, and only past occurrences the\n * caller finished dispatching.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const compute = (\n trigger: Registered,\n now: number,\n observed: number | undefined\n): Effect.Effect<Due, TriggerError> =>\n Effect.gen(function*() {\n const cron = yield* Cron.parse(trigger.cron, trigger.timezone)\n const current = (yield* Cron.previousAtOrBefore(cron, new Date(now))).getTime()\n
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,aAAwB;AACxB,cAAyB;AACzB,WAAsB;AAmBtB,MAAM,cAAc,CAClB,WACA,SAEA,OAAO,MAAM,MAAM,CAAC,UAClB,MAAM,SAAS,4BACX,OAAO;AAAA,EACP,OAAO;AAAA,IACL,OAAO,WAAW,sDAAsD,KAAK;AAAA,IAC7E,EAAE,UAAU;AAAA,EACd;AAAA,EACA,CAAC;AACH,IACE,OAAO,KAAK,KAAK,CAAC;AAanB,MAAM,UAAU,CACrB,SACA,KACA,aAEA,OAAO,IAAI,aAAY;AACrB,QAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,MAAM,QAAQ,QAAQ;AAC7D,QAAM,WAAW,OAAO,KAAK,mBAAmB,MAAM,IAAI,KAAK,GAAG,CAAC,GAAG,QAAQ;
|
|
4
|
+
"sourcesContent": ["/**\n * Which occurrences one scheduler tick owes a trigger.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport * as CatchUp from \"../CatchUp.ts\"\nimport * as Cron from \"../Cron.ts\"\nimport type { TriggerError } from \"../TriggerError.ts\"\nimport type { Registered } from \"../TriggerStore.ts\"\n\n/**\n * The occurrences to claim, oldest first, and the watermark that stands once\n * every one of them has been dispatched.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface Due {\n readonly occurrences: ReadonlyArray<number>\n readonly watermark: number\n}\n\n// A bound the declaration cannot honour is a statement about how much history\n// to replay, not a reason to stop scheduling: the backlog beyond the bound is\n// abandoned, loudly, and the current occurrence still fires.\nconst withinBound = (\n triggerId: string,\n owed: Effect.Effect<ReadonlyArray<Date>, TriggerError>\n): Effect.Effect<ReadonlyArray<Date>, TriggerError> =>\n Effect.catch(owed, (error) =>\n error.code === \"catch_up_bound_exceeded\"\n ? Effect.as(\n Effect.annotateLogs(\n Effect.logWarning(\"A trigger abandoned catch-up work beyond its bound\", error),\n { triggerId }\n ),\n [] as ReadonlyArray<Date>\n )\n : Effect.fail(error))\n\n/**\n * Computes what a trigger owes at `now`, given the watermark this process has\n * already dispatched through, or `undefined` on first sight of the trigger.\n *\n * The answer depends on nothing but its arguments. The watermark is the\n * caller's to keep: it only moves forward, and only past occurrences the\n * caller finished dispatching.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const compute = (\n trigger: Registered,\n now: number,\n observed: number | undefined\n): Effect.Effect<Due, TriggerError> =>\n Effect.gen(function*() {\n const cron = yield* Cron.parse(trigger.cron, trigger.timezone)\n const current = (yield* Cron.previousAtOrBefore(cron, new Date(now))).getTime()\n // First sight of this trigger in this process starts from the last fire\n // the store recorded. A trigger that has never fired starts from here\n // rather than from whatever occurrence last passed: registering a weekly\n // trigger on a Sunday evening owes nothing for the Monday six days gone,\n // which is what `catchUp` says.\n const since = observed ?? trigger.lastFiredAt\n if (since === undefined || current <= since) {\n return { occurrences: [], watermark: Math.max(current, observed ?? current) }\n }\n // A restart and a stall in this process missed the same boundaries, so\n // both ask the policy about the same interval, the current boundary\n // included. The stalled poll used to leave the current boundary out, so\n // `one` fired two runs and a bound of 2 fired three where a restart fired\n // one.\n //\n // When only the boundary that just passed is due, nothing was missed: a\n // bound of zero has nothing to refuse, so it is read as one. Every other\n // bound is still validated and searched exactly as stated.\n // Only a zero bound needs the boundary before this one, so no other tick\n // pays for that search.\n const lone = trigger.maxCatchUp === 0 &&\n (yield* Cron.previousAtOrBefore(cron, new Date(current - 1))).getTime() <= since\n const bound = lone ? 1 : trigger.maxCatchUp\n const owed = yield* withinBound(\n trigger.id,\n CatchUp.occurrences(trigger.catchUp, bound, new Date(since), new Date(now), cron)\n )\n // The current boundary is owed whatever the policy says about the\n // backlog. Under `none`, or once the bound abandoned the backlog, a\n // restart used to skip it.\n const occurrences = owed.map((occurrence) => occurrence.getTime())\n if (occurrences.at(-1) !== current) occurrences.push(current)\n return { occurrences, watermark: current }\n })\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,aAAwB;AACxB,cAAyB;AACzB,WAAsB;AAmBtB,MAAM,cAAc,CAClB,WACA,SAEA,OAAO,MAAM,MAAM,CAAC,UAClB,MAAM,SAAS,4BACX,OAAO;AAAA,EACP,OAAO;AAAA,IACL,OAAO,WAAW,sDAAsD,KAAK;AAAA,IAC7E,EAAE,UAAU;AAAA,EACd;AAAA,EACA,CAAC;AACH,IACE,OAAO,KAAK,KAAK,CAAC;AAanB,MAAM,UAAU,CACrB,SACA,KACA,aAEA,OAAO,IAAI,aAAY;AACrB,QAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,MAAM,QAAQ,QAAQ;AAC7D,QAAM,WAAW,OAAO,KAAK,mBAAmB,MAAM,IAAI,KAAK,GAAG,CAAC,GAAG,QAAQ;AAM9E,QAAM,QAAQ,YAAY,QAAQ;AAClC,MAAI,UAAU,UAAa,WAAW,OAAO;AAC3C,WAAO,EAAE,aAAa,CAAC,GAAG,WAAW,KAAK,IAAI,SAAS,YAAY,OAAO,EAAE;AAAA,EAC9E;AAYA,QAAM,OAAO,QAAQ,eAAe,MACjC,OAAO,KAAK,mBAAmB,MAAM,IAAI,KAAK,UAAU,CAAC,CAAC,GAAG,QAAQ,KAAK;AAC7E,QAAM,QAAQ,OAAO,IAAI,QAAQ;AACjC,QAAM,OAAO,OAAO;AAAA,IAClB,QAAQ;AAAA,IACR,QAAQ,YAAY,QAAQ,SAAS,OAAO,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK,GAAG,GAAG,IAAI;AAAA,EAClF;AAIA,QAAM,cAAc,KAAK,IAAI,CAAC,eAAe,WAAW,QAAQ,CAAC;AACjE,MAAI,YAAY,GAAG,EAAE,MAAM,QAAS,aAAY,KAAK,OAAO;AAC5D,SAAO,EAAE,aAAa,WAAW,QAAQ;AAC3C,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DueOccurrences.d.ts","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAoBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAClB,SAAS,UAAU,EACnB,KAAK,MAAM,EACX,UAAU,MAAM,GAAG,SAAS,KAC3B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"DueOccurrences.d.ts","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAoBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAClB,SAAS,UAAU,EACnB,KAAK,MAAM,EACX,UAAU,MAAM,GAAG,SAAS,KAC3B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAqC9B,CAAA"}
|
|
@@ -26,30 +26,36 @@ const withinBound = (triggerId, owed) => Effect.catch(owed, (error) => error.cod
|
|
|
26
26
|
export const compute = (trigger, now, observed) => Effect.gen(function* () {
|
|
27
27
|
const cron = yield* Cron.parse(trigger.cron, trigger.timezone);
|
|
28
28
|
const current = (yield* Cron.previousAtOrBefore(cron, new Date(now))).getTime();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return { occurrences: [], watermark: current };
|
|
38
|
-
const owed = yield* withinBound(trigger.id, CatchUp.occurrences(trigger.catchUp, trigger.maxCatchUp, new Date(trigger.lastFiredAt), new Date(now), cron));
|
|
39
|
-
// The current boundary is owed whatever the policy says about the
|
|
40
|
-
// backlog, exactly as on every later poll. Under `none`, or once the
|
|
41
|
-
// bound abandoned the backlog, a restart used to skip it.
|
|
42
|
-
const occurrences = owed.map((occurrence) => occurrence.getTime());
|
|
43
|
-
if (occurrences.at(-1) !== current)
|
|
44
|
-
occurrences.push(current);
|
|
45
|
-
return { occurrences, watermark: current };
|
|
29
|
+
// First sight of this trigger in this process starts from the last fire
|
|
30
|
+
// the store recorded. A trigger that has never fired starts from here
|
|
31
|
+
// rather than from whatever occurrence last passed: registering a weekly
|
|
32
|
+
// trigger on a Sunday evening owes nothing for the Monday six days gone,
|
|
33
|
+
// which is what `catchUp` says.
|
|
34
|
+
const since = observed ?? trigger.lastFiredAt;
|
|
35
|
+
if (since === undefined || current <= since) {
|
|
36
|
+
return { occurrences: [], watermark: Math.max(current, observed ?? current) };
|
|
46
37
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
// A restart and a stall in this process missed the same boundaries, so
|
|
39
|
+
// both ask the policy about the same interval, the current boundary
|
|
40
|
+
// included. The stalled poll used to leave the current boundary out, so
|
|
41
|
+
// `one` fired two runs and a bound of 2 fired three where a restart fired
|
|
42
|
+
// one.
|
|
43
|
+
//
|
|
44
|
+
// When only the boundary that just passed is due, nothing was missed: a
|
|
45
|
+
// bound of zero has nothing to refuse, so it is read as one. Every other
|
|
46
|
+
// bound is still validated and searched exactly as stated.
|
|
47
|
+
// Only a zero bound needs the boundary before this one, so no other tick
|
|
48
|
+
// pays for that search.
|
|
49
|
+
const lone = trigger.maxCatchUp === 0 &&
|
|
50
|
+
(yield* Cron.previousAtOrBefore(cron, new Date(current - 1))).getTime() <= since;
|
|
51
|
+
const bound = lone ? 1 : trigger.maxCatchUp;
|
|
52
|
+
const owed = yield* withinBound(trigger.id, CatchUp.occurrences(trigger.catchUp, bound, new Date(since), new Date(now), cron));
|
|
53
|
+
// The current boundary is owed whatever the policy says about the
|
|
54
|
+
// backlog. Under `none`, or once the bound abandoned the backlog, a
|
|
55
|
+
// restart used to skip it.
|
|
56
|
+
const occurrences = owed.map((occurrence) => occurrence.getTime());
|
|
57
|
+
if (occurrences.at(-1) !== current)
|
|
58
|
+
occurrences.push(current);
|
|
59
|
+
return { occurrences, watermark: current };
|
|
54
60
|
});
|
|
55
61
|
//# sourceMappingURL=DueOccurrences.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DueOccurrences.js","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,OAAO,MAAM,eAAe,CAAA;AACxC,OAAO,KAAK,IAAI,MAAM,YAAY,CAAA;AAgBlC,8EAA8E;AAC9E,8EAA8E;AAC9E,6DAA6D;AAC7D,MAAM,WAAW,GAAG,CAClB,SAAiB,EACjB,IAAsD,EACJ,EAAE,CACpD,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,CAC3B,KAAK,CAAC,IAAI,KAAK,yBAAyB;IACtC,CAAC,CAAC,MAAM,CAAC,EAAE,CACT,MAAM,CAAC,YAAY,CACjB,MAAM,CAAC,UAAU,CAAC,oDAAoD,EAAE,KAAK,CAAC,EAC9E,EAAE,SAAS,EAAE,CACd,EACD,EAAyB,CAC1B;IACD,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;AAE3B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CACrB,OAAmB,EACnB,GAAW,EACX,QAA4B,EACM,EAAE,CACpC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;IAC/E,
|
|
1
|
+
{"version":3,"file":"DueOccurrences.js","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which occurrences one scheduler tick owes a trigger.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport * as CatchUp from \"../CatchUp.ts\"\nimport * as Cron from \"../Cron.ts\"\nimport type { TriggerError } from \"../TriggerError.ts\"\nimport type { Registered } from \"../TriggerStore.ts\"\n\n/**\n * The occurrences to claim, oldest first, and the watermark that stands once\n * every one of them has been dispatched.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface Due {\n readonly occurrences: ReadonlyArray<number>\n readonly watermark: number\n}\n\n// A bound the declaration cannot honour is a statement about how much history\n// to replay, not a reason to stop scheduling: the backlog beyond the bound is\n// abandoned, loudly, and the current occurrence still fires.\nconst withinBound = (\n triggerId: string,\n owed: Effect.Effect<ReadonlyArray<Date>, TriggerError>\n): Effect.Effect<ReadonlyArray<Date>, TriggerError> =>\n Effect.catch(owed, (error) =>\n error.code === \"catch_up_bound_exceeded\"\n ? Effect.as(\n Effect.annotateLogs(\n Effect.logWarning(\"A trigger abandoned catch-up work beyond its bound\", error),\n { triggerId }\n ),\n [] as ReadonlyArray<Date>\n )\n : Effect.fail(error))\n\n/**\n * Computes what a trigger owes at `now`, given the watermark this process has\n * already dispatched through, or `undefined` on first sight of the trigger.\n *\n * The answer depends on nothing but its arguments. The watermark is the\n * caller's to keep: it only moves forward, and only past occurrences the\n * caller finished dispatching.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const compute = (\n trigger: Registered,\n now: number,\n observed: number | undefined\n): Effect.Effect<Due, TriggerError> =>\n Effect.gen(function*() {\n const cron = yield* Cron.parse(trigger.cron, trigger.timezone)\n const current = (yield* Cron.previousAtOrBefore(cron, new Date(now))).getTime()\n // First sight of this trigger in this process starts from the last fire\n // the store recorded. A trigger that has never fired starts from here\n // rather than from whatever occurrence last passed: registering a weekly\n // trigger on a Sunday evening owes nothing for the Monday six days gone,\n // which is what `catchUp` says.\n const since = observed ?? trigger.lastFiredAt\n if (since === undefined || current <= since) {\n return { occurrences: [], watermark: Math.max(current, observed ?? current) }\n }\n // A restart and a stall in this process missed the same boundaries, so\n // both ask the policy about the same interval, the current boundary\n // included. The stalled poll used to leave the current boundary out, so\n // `one` fired two runs and a bound of 2 fired three where a restart fired\n // one.\n //\n // When only the boundary that just passed is due, nothing was missed: a\n // bound of zero has nothing to refuse, so it is read as one. Every other\n // bound is still validated and searched exactly as stated.\n // Only a zero bound needs the boundary before this one, so no other tick\n // pays for that search.\n const lone = trigger.maxCatchUp === 0 &&\n (yield* Cron.previousAtOrBefore(cron, new Date(current - 1))).getTime() <= since\n const bound = lone ? 1 : trigger.maxCatchUp\n const owed = yield* withinBound(\n trigger.id,\n CatchUp.occurrences(trigger.catchUp, bound, new Date(since), new Date(now), cron)\n )\n // The current boundary is owed whatever the policy says about the\n // backlog. Under `none`, or once the bound abandoned the backlog, a\n // restart used to skip it.\n const occurrences = owed.map((occurrence) => occurrence.getTime())\n if (occurrences.at(-1) !== current) occurrences.push(current)\n return { occurrences, watermark: current }\n })\n"]}
|
package/docs/api.md
CHANGED
|
@@ -316,6 +316,11 @@ backlog when catch-up exceeds its bound. It still dispatches the current
|
|
|
316
316
|
occurrence subject to overlap, on the first poll after a restart and on every
|
|
317
317
|
later poll.
|
|
318
318
|
|
|
319
|
+
The bound counts every boundary since the last dispatch, the one that just
|
|
320
|
+
passed included, and a restart and a stalled scheduler count the same
|
|
321
|
+
boundaries. A tick where only that one boundary passed fires it under any
|
|
322
|
+
bound.
|
|
323
|
+
|
|
319
324
|
## TriggerStore
|
|
320
325
|
|
|
321
326
|
The durable state contract: registration, listing, the claim protocol, and
|
|
@@ -1144,9 +1149,10 @@ included. See [Overlap and catch-up](./concepts/policies.md).
|
|
|
1144
1149
|
## Webhook verification and input ownership
|
|
1145
1150
|
|
|
1146
1151
|
The signature verifier looks up `SignatureConfig.header` in `RawInbound.headers`
|
|
1147
|
-
first as lowercase
|
|
1148
|
-
|
|
1149
|
-
|
|
1152
|
+
first as lowercase, then exactly as written, then under any other casing, so a
|
|
1153
|
+
header name matches whatever capitalisation the request carries. It encodes the
|
|
1154
|
+
supplied header value as UTF-8 with `TextEncoder` and compares those bytes with
|
|
1155
|
+
the bytes returned by `SignatureConfig.expected`. An absent or empty header is refused
|
|
1150
1156
|
before `expected` runs, and an `expected` that answers with zero bytes is
|
|
1151
1157
|
refused after it. Constant-time equality agrees on two empty byte strings, so
|
|
1152
1158
|
without those guards a request carrying no signature at all would authenticate
|
|
@@ -118,6 +118,11 @@ backlog when catch-up exceeds its bound. It still dispatches the current
|
|
|
118
118
|
occurrence subject to overlap, on the first poll after a restart and on every
|
|
119
119
|
later poll.
|
|
120
120
|
|
|
121
|
+
The bound counts every boundary since the last dispatch, the one that just
|
|
122
|
+
passed included, and a restart and a stalled scheduler count the same
|
|
123
|
+
boundaries. A tick where only that one boundary passed fires it under any
|
|
124
|
+
bound.
|
|
125
|
+
|
|
121
126
|
## Choosing
|
|
122
127
|
|
|
123
128
|
The task-shaped version of this page, with the question to ask for each policy,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/triggers",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.9",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "agent"
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"tag": "next"
|
|
207
207
|
},
|
|
208
208
|
"dependencies": {
|
|
209
|
-
"@smthrs/control": "1.0.0-rc.
|
|
210
|
-
"@smthrs/database": "1.0.0-rc.
|
|
209
|
+
"@smthrs/control": "1.0.0-rc.9",
|
|
210
|
+
"@smthrs/database": "1.0.0-rc.9"
|
|
211
211
|
},
|
|
212
212
|
"peerDependencies": {
|
|
213
213
|
"effect": "^4.0.4"
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
"vitest": "5.0.0"
|
|
233
233
|
},
|
|
234
234
|
"scripts": {
|
|
235
|
-
"lint": "eslint src --max-warnings=0 && dprint check",
|
|
235
|
+
"lint": "eslint src examples --max-warnings=0 && dprint check",
|
|
236
236
|
"format": "dprint fmt",
|
|
237
237
|
"build": "node scripts/build.mjs",
|
|
238
238
|
"check": "tsc -b tsconfig.json && tsc -p tsconfig.test.json --noEmit",
|
|
@@ -59,43 +59,37 @@ export const compute = (
|
|
|
59
59
|
Effect.gen(function*() {
|
|
60
60
|
const cron = yield* Cron.parse(trigger.cron, trigger.timezone)
|
|
61
61
|
const current = (yield* Cron.previousAtOrBefore(cron, new Date(now))).getTime()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
trigger.id,
|
|
71
|
-
CatchUp.occurrences(
|
|
72
|
-
trigger.catchUp,
|
|
73
|
-
trigger.maxCatchUp,
|
|
74
|
-
new Date(trigger.lastFiredAt),
|
|
75
|
-
new Date(now),
|
|
76
|
-
cron
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
// The current boundary is owed whatever the policy says about the
|
|
80
|
-
// backlog, exactly as on every later poll. Under `none`, or once the
|
|
81
|
-
// bound abandoned the backlog, a restart used to skip it.
|
|
82
|
-
const occurrences = owed.map((occurrence) => occurrence.getTime())
|
|
83
|
-
if (occurrences.at(-1) !== current) occurrences.push(current)
|
|
84
|
-
return { occurrences, watermark: current }
|
|
62
|
+
// First sight of this trigger in this process starts from the last fire
|
|
63
|
+
// the store recorded. A trigger that has never fired starts from here
|
|
64
|
+
// rather than from whatever occurrence last passed: registering a weekly
|
|
65
|
+
// trigger on a Sunday evening owes nothing for the Monday six days gone,
|
|
66
|
+
// which is what `catchUp` says.
|
|
67
|
+
const since = observed ?? trigger.lastFiredAt
|
|
68
|
+
if (since === undefined || current <= since) {
|
|
69
|
+
return { occurrences: [], watermark: Math.max(current, observed ?? current) }
|
|
85
70
|
}
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
// A restart and a stall in this process missed the same boundaries, so
|
|
72
|
+
// both ask the policy about the same interval, the current boundary
|
|
73
|
+
// included. The stalled poll used to leave the current boundary out, so
|
|
74
|
+
// `one` fired two runs and a bound of 2 fired three where a restart fired
|
|
75
|
+
// one.
|
|
76
|
+
//
|
|
77
|
+
// When only the boundary that just passed is due, nothing was missed: a
|
|
78
|
+
// bound of zero has nothing to refuse, so it is read as one. Every other
|
|
79
|
+
// bound is still validated and searched exactly as stated.
|
|
80
|
+
// Only a zero bound needs the boundary before this one, so no other tick
|
|
81
|
+
// pays for that search.
|
|
82
|
+
const lone = trigger.maxCatchUp === 0 &&
|
|
83
|
+
(yield* Cron.previousAtOrBefore(cron, new Date(current - 1))).getTime() <= since
|
|
84
|
+
const bound = lone ? 1 : trigger.maxCatchUp
|
|
85
|
+
const owed = yield* withinBound(
|
|
88
86
|
trigger.id,
|
|
89
|
-
CatchUp.occurrences(
|
|
90
|
-
trigger.catchUp,
|
|
91
|
-
trigger.maxCatchUp,
|
|
92
|
-
new Date(observed),
|
|
93
|
-
new Date(current - 1),
|
|
94
|
-
cron
|
|
95
|
-
)
|
|
87
|
+
CatchUp.occurrences(trigger.catchUp, bound, new Date(since), new Date(now), cron)
|
|
96
88
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
// The current boundary is owed whatever the policy says about the
|
|
90
|
+
// backlog. Under `none`, or once the bound abandoned the backlog, a
|
|
91
|
+
// restart used to skip it.
|
|
92
|
+
const occurrences = owed.map((occurrence) => occurrence.getTime())
|
|
93
|
+
if (occurrences.at(-1) !== current) occurrences.push(current)
|
|
94
|
+
return { occurrences, watermark: current }
|
|
101
95
|
})
|