@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.
@@ -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,CA2C9B,CAAA"}
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
- if (observed === void 0) {
48
- if (trigger.lastFiredAt === void 0) return { occurrences: [], watermark: current };
49
- if (current <= trigger.lastFiredAt) return { occurrences: [], watermark: current };
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
- if (current <= observed) return { occurrences: [], watermark: observed };
65
- const backlog = yield* withinBound(
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
- return {
76
- occurrences: [...backlog.map((occurrence) => occurrence.getTime()), current],
77
- watermark: current
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 if (observed === undefined) {\n // First sight of this trigger in this process. A trigger that has never\n // fired starts from here rather than from whatever occurrence last\n // passed: registering a weekly trigger on a Sunday evening owes nothing\n // for the Monday six days gone, which is what `catchUp` says.\n if (trigger.lastFiredAt === undefined) return { occurrences: [], watermark: current }\n if (current <= trigger.lastFiredAt) return { occurrences: [], watermark: current }\n const owed = yield* withinBound(\n trigger.id,\n CatchUp.occurrences(\n trigger.catchUp,\n trigger.maxCatchUp,\n new Date(trigger.lastFiredAt),\n new Date(now),\n cron\n )\n )\n // The current boundary is owed whatever the policy says about the\n // backlog, exactly as on every later poll. Under `none`, or once the\n // bound abandoned the backlog, a 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 if (current <= observed) return { occurrences: [], watermark: observed }\n const backlog = yield* withinBound(\n trigger.id,\n CatchUp.occurrences(\n trigger.catchUp,\n trigger.maxCatchUp,\n new Date(observed),\n new Date(current - 1),\n cron\n )\n )\n return {\n occurrences: [...backlog.map((occurrence) => occurrence.getTime()), current],\n watermark: current\n }\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;AAC9E,MAAI,aAAa,QAAW;AAK1B,QAAI,QAAQ,gBAAgB,OAAW,QAAO,EAAE,aAAa,CAAC,GAAG,WAAW,QAAQ;AACpF,QAAI,WAAW,QAAQ,YAAa,QAAO,EAAE,aAAa,CAAC,GAAG,WAAW,QAAQ;AACjF,UAAM,OAAO,OAAO;AAAA,MAClB,QAAQ;AAAA,MACR,QAAQ;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,IAAI,KAAK,QAAQ,WAAW;AAAA,QAC5B,IAAI,KAAK,GAAG;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAIA,UAAM,cAAc,KAAK,IAAI,CAAC,eAAe,WAAW,QAAQ,CAAC;AACjE,QAAI,YAAY,GAAG,EAAE,MAAM,QAAS,aAAY,KAAK,OAAO;AAC5D,WAAO,EAAE,aAAa,WAAW,QAAQ;AAAA,EAC3C;AACA,MAAI,WAAW,SAAU,QAAO,EAAE,aAAa,CAAC,GAAG,WAAW,SAAS;AACvE,QAAM,UAAU,OAAO;AAAA,IACrB,QAAQ;AAAA,IACR,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,IAAI,KAAK,QAAQ;AAAA,MACjB,IAAI,KAAK,UAAU,CAAC;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,aAAa,CAAC,GAAG,QAAQ,IAAI,CAAC,eAAe,WAAW,QAAQ,CAAC,GAAG,OAAO;AAAA,IAC3E,WAAW;AAAA,EACb;AACF,CAAC;",
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,CA2C9B,CAAA"}
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
- if (observed === undefined) {
30
- // First sight of this trigger in this process. A trigger that has never
31
- // fired starts from here rather than from whatever occurrence last
32
- // passed: registering a weekly trigger on a Sunday evening owes nothing
33
- // for the Monday six days gone, which is what `catchUp` says.
34
- if (trigger.lastFiredAt === undefined)
35
- return { occurrences: [], watermark: current };
36
- if (current <= trigger.lastFiredAt)
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
- if (current <= observed)
48
- return { occurrences: [], watermark: observed };
49
- const backlog = yield* withinBound(trigger.id, CatchUp.occurrences(trigger.catchUp, trigger.maxCatchUp, new Date(observed), new Date(current - 1), cron));
50
- return {
51
- occurrences: [...backlog.map((occurrence) => occurrence.getTime()), current],
52
- watermark: current
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":"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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 if (observed === undefined) {\n // First sight of this trigger in this process. A trigger that has never\n // fired starts from here rather than from whatever occurrence last\n // passed: registering a weekly trigger on a Sunday evening owes nothing\n // for the Monday six days gone, which is what `catchUp` says.\n if (trigger.lastFiredAt === undefined) return { occurrences: [], watermark: current }\n if (current <= trigger.lastFiredAt) return { occurrences: [], watermark: current }\n const owed = yield* withinBound(\n trigger.id,\n CatchUp.occurrences(\n trigger.catchUp,\n trigger.maxCatchUp,\n new Date(trigger.lastFiredAt),\n new Date(now),\n cron\n )\n )\n // The current boundary is owed whatever the policy says about the\n // backlog, exactly as on every later poll. Under `none`, or once the\n // bound abandoned the backlog, a 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 if (current <= observed) return { occurrences: [], watermark: observed }\n const backlog = yield* withinBound(\n trigger.id,\n CatchUp.occurrences(\n trigger.catchUp,\n trigger.maxCatchUp,\n new Date(observed),\n new Date(current - 1),\n cron\n )\n )\n return {\n occurrences: [...backlog.map((occurrence) => occurrence.getTime()), current],\n watermark: current\n }\n })\n"]}
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 and then exactly as written. It encodes the supplied header
1148
- value as UTF-8 with `TextEncoder` and compares those bytes with the bytes
1149
- returned by `SignatureConfig.expected`. An absent or empty header is refused
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.8",
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.8",
210
- "@smthrs/database": "1.0.0-rc.8"
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
- if (observed === undefined) {
63
- // First sight of this trigger in this process. A trigger that has never
64
- // fired starts from here rather than from whatever occurrence last
65
- // passed: registering a weekly trigger on a Sunday evening owes nothing
66
- // for the Monday six days gone, which is what `catchUp` says.
67
- if (trigger.lastFiredAt === undefined) return { occurrences: [], watermark: current }
68
- if (current <= trigger.lastFiredAt) return { occurrences: [], watermark: current }
69
- const owed = yield* withinBound(
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
- if (current <= observed) return { occurrences: [], watermark: observed }
87
- const backlog = yield* withinBound(
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
- return {
98
- occurrences: [...backlog.map((occurrence) => occurrence.getTime()), current],
99
- watermark: current
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
  })