@shivaedev/pr-watch 0.1.2 → 0.3.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +20 -5
  3. package/dist/GitHub.d.ts.map +1 -1
  4. package/dist/GitHub.js +5 -19
  5. package/dist/GitHub.js.map +1 -1
  6. package/dist/GitHubHttp.d.ts +5 -6
  7. package/dist/GitHubHttp.d.ts.map +1 -1
  8. package/dist/GitHubHttp.js +34 -29
  9. package/dist/GitHubHttp.js.map +1 -1
  10. package/dist/Watcher.d.ts +1 -1
  11. package/dist/Watcher.d.ts.map +1 -1
  12. package/dist/Watcher.js +4 -4
  13. package/dist/Watcher.js.map +1 -1
  14. package/dist/arguments.d.ts +4 -4
  15. package/dist/arguments.d.ts.map +1 -1
  16. package/dist/arguments.js +19 -3
  17. package/dist/arguments.js.map +1 -1
  18. package/dist/cli.js +13 -12
  19. package/dist/cli.js.map +1 -1
  20. package/dist/confirmQueue.d.ts +3 -2
  21. package/dist/confirmQueue.d.ts.map +1 -1
  22. package/dist/confirmQueue.js +2 -2
  23. package/dist/confirmQueue.js.map +1 -1
  24. package/dist/events.d.ts +4 -2
  25. package/dist/events.d.ts.map +1 -1
  26. package/dist/events.js +5 -2
  27. package/dist/events.js.map +1 -1
  28. package/dist/health.d.ts +6 -0
  29. package/dist/health.d.ts.map +1 -0
  30. package/dist/health.js +19 -0
  31. package/dist/health.js.map +1 -0
  32. package/dist/model.d.ts +3 -1
  33. package/dist/model.d.ts.map +1 -1
  34. package/dist/model.js +1 -0
  35. package/dist/model.js.map +1 -1
  36. package/dist/program.d.ts +3 -2
  37. package/dist/program.d.ts.map +1 -1
  38. package/dist/program.js +42 -23
  39. package/dist/program.js.map +1 -1
  40. package/dist/quota.d.ts +3 -9
  41. package/dist/quota.d.ts.map +1 -1
  42. package/dist/quota.js +10 -30
  43. package/dist/quota.js.map +1 -1
  44. package/package.json +5 -4
  45. package/src/GitHub.ts +5 -19
  46. package/src/GitHubHttp.ts +47 -31
  47. package/src/Watcher.ts +5 -5
  48. package/src/arguments.ts +20 -3
  49. package/src/cli.ts +19 -16
  50. package/src/confirmQueue.ts +2 -2
  51. package/src/events.ts +11 -6
  52. package/src/health.ts +26 -0
  53. package/src/model.ts +3 -1
  54. package/src/program.ts +56 -26
  55. package/src/quota.ts +16 -37
  56. package/dist/httpResponse.d.ts +0 -15
  57. package/dist/httpResponse.d.ts.map +0 -1
  58. package/dist/httpResponse.js +0 -31
  59. package/dist/httpResponse.js.map +0 -1
  60. package/src/httpResponse.ts +0 -37
package/dist/events.d.ts CHANGED
@@ -1,3 +1,5 @@
1
- import type { Snapshot } from "./model.ts";
2
- export declare function events(before: Snapshot | undefined, after: Snapshot): readonly string[];
1
+ import type { Snapshot } from "#model.ts";
2
+ export declare const eventKinds: readonly ["merged", "closed", "conflict", "queued", "dequeued", "auto-merge-off", "checks-failed", "stuck"];
3
+ export type EventKind = (typeof eventKinds)[number];
4
+ export declare function events(before: Snapshot | undefined, after: Snapshot, selected?: readonly EventKind[]): readonly string[];
3
5
  //# sourceMappingURL=events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAgB3C,wBAAgB,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,EAAE,KAAK,EAAE,QAAQ,GAAG,SAAS,MAAM,EAAE,CAgBvF"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,eAAO,MAAM,UAAU,YAAI,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AACtI,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAgBpD,wBAAgB,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,MAAM,EAAE,CAkBxH"}
package/dist/events.js CHANGED
@@ -1,3 +1,4 @@
1
+ export const eventKinds = ["merged", "closed", "conflict", "queued", "dequeued", "auto-merge-off", "checks-failed", "stuck"];
1
2
  function stuck(pr) {
2
3
  return pr.state === "open" && pr.autoMerge && (pr.dirty || pr.failed);
3
4
  }
@@ -12,7 +13,7 @@ function openEvents(before, after) {
12
13
  ];
13
14
  return transitions.flatMap(([changed, event]) => (changed ? [event] : []));
14
15
  }
15
- export function events(before, after) {
16
+ export function events(before, after, selected) {
16
17
  if (before === undefined && !stuck(after)) {
17
18
  return [];
18
19
  }
@@ -27,6 +28,8 @@ export function events(before, after) {
27
28
  if (after.state === "open") {
28
29
  found.push(...openEvents(baseline, after));
29
30
  }
30
- return found.map((event) => JSON.stringify({ branch: after.branch, event, number: after.number, title: after.title }));
31
+ return found
32
+ .filter((event) => selected === undefined || selected.includes(event))
33
+ .map((event) => JSON.stringify({ branch: after.branch, event, number: after.number, title: after.title }));
31
34
  }
32
35
  //# sourceMappingURL=events.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,SAAS,KAAK,CAAC,EAAY;IAC1B,OAAO,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,EAAE,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC;AACD,SAAS,UAAU,CAAC,MAAgB,EAAE,KAAe;IACpD,MAAM,WAAW,GAA4C;QAC5D,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC;QAC1C,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC;QAC1C,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC;QAC5F,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC;QACjD,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5E,CAAC;AACD,MAAM,UAAU,MAAM,CAAC,MAA4B,EAAE,KAAe;IACnE,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACvF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5E,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACxH,CAAC"}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AAGtI,SAAS,KAAK,CAAC,EAAY;IAC1B,OAAO,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,EAAE,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC;AACD,SAAS,UAAU,CAAC,MAAgB,EAAE,KAAe;IACpD,MAAM,WAAW,GAA+C;QAC/D,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC;QAC1C,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC;QAC1C,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC;QAC5F,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC;QACjD,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5E,CAAC;AACD,MAAM,UAAU,MAAM,CAAC,MAA4B,EAAE,KAAe,EAAE,QAA+B;IACpG,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACvF,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5E,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SACrE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAC7G,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { Effect } from "effect";
2
+ import type { WatchError } from "#model.ts";
3
+ export type Emit = (line: string) => Effect.Effect<void, WatchError>;
4
+ export declare const health: (emit: Emit, event: "cooldown" | "error" | "heartbeat" | "stopped", fields?: Record<string, unknown> | undefined) => Effect.Effect<void, WatchError, never>;
5
+ export declare const waitForPoll: (emit: Emit, interval: number, delay: number) => Effect.Effect<void, WatchError, never>;
6
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AACrE,eAAO,MAAM,MAAM,6JAOjB,CAAC;AAEH,eAAO,MAAM,WAAW,yFAYtB,CAAC"}
package/dist/health.js ADDED
@@ -0,0 +1,19 @@
1
+ import { Clock, Effect } from "effect";
2
+ export const health = Effect.fnUntraced(function* (emit, event, fields = {}) {
3
+ const at = new Date(yield* Clock.currentTimeMillis).toISOString();
4
+ yield* emit(JSON.stringify({ at, event, ...fields }));
5
+ });
6
+ export const waitForPoll = Effect.fnUntraced(function* (emit, interval, delay) {
7
+ const next = (yield* Clock.currentTimeMillis) + delay;
8
+ const nextPollAt = new Date(next).toISOString();
9
+ if (delay > interval) {
10
+ yield* health(emit, "cooldown", { delayMs: delay, message: "GitHub quota pacing", nextPollAt });
11
+ }
12
+ let remaining = delay;
13
+ while (remaining > 0) {
14
+ yield* health(emit, "heartbeat", { nextPollAt });
15
+ yield* Effect.sleep(Math.min(interval, remaining));
16
+ remaining = next - (yield* Clock.currentTimeMillis);
17
+ }
18
+ });
19
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIvC,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAChD,IAAU,EACV,KAAqD,EACrD,MAAM,GAA4B,EAAE;IAEpC,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,WAAW,EAAE,CAAC;IAClE,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,IAAU,EAAE,QAAgB,EAAE,KAAa;IAClG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,KAAK,CAAC;IACtD,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IAChD,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;QACtB,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,OAAO,SAAS,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QACjD,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;QACnD,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrD,CAAC;AACF,CAAC,CAAC,CAAC"}
package/dist/model.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Schema } from "effect";
2
+ import type { EventKind } from "#events.ts";
2
3
  declare const WatchError_base: Schema.Class<WatchError, Schema.TaggedStruct<"WatchError", {
3
4
  readonly message: Schema.String;
5
+ readonly retryAt: Schema.optionalKey<Schema.Number>;
4
6
  }>, import("effect/Cause").YieldableError>;
5
7
  export declare class WatchError extends WatchError_base {
6
8
  }
@@ -35,12 +37,12 @@ export declare const State: Schema.Struct<{
35
37
  export type State = typeof State.Type;
36
38
  export interface Options {
37
39
  readonly apiUrl: string;
40
+ readonly events?: readonly EventKind[];
38
41
  readonly fleet: boolean;
39
42
  readonly interval: number;
40
43
  readonly numbers: readonly number[];
41
44
  readonly repository: string;
42
45
  readonly stateFile: string;
43
- readonly token: string;
44
46
  }
45
47
  export {};
46
48
  //# sourceMappingURL=model.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;AAChC,qBAAa,UAAW,SAAQ,eAE9B;CAAG;AACL,eAAO,MAAM,QAAQ;;;;;;;;;;EAUnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAC;AAC5C,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;EAKhB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC;AACtC,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;;;;;AAC5C,qBAAa,UAAW,SAAQ,eAG9B;CAAG;AACL,eAAO,MAAM,QAAQ;;;;;;;;;;EAUnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAC;AAC5C,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;EAKhB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC;AACtC,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B"}
package/dist/model.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Schema } from "effect";
2
2
  export class WatchError extends Schema.TaggedError()("WatchError", {
3
3
  message: Schema.String,
4
+ retryAt: Schema.optionalKey(Schema.Number),
4
5
  }) {
5
6
  }
6
7
  export const Snapshot = Schema.Struct({
package/dist/model.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,MAAM,OAAO,UAAW,SAAQ,MAAM,CAAC,WAAW,EAAc,CAAC,YAAY,EAAE;IAC9E,OAAO,EAAE,MAAM,CAAC,MAAM;CACtB,CAAC;CAAG;AACL,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC,OAAO;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC,OAAO;IACrB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC,MAAM;IACzB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;CAC1B,CAAC,CAAC"}
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,OAAO,UAAW,SAAQ,MAAM,CAAC,WAAW,EAAc,CAAC,YAAY,EAAE;IAC9E,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;CAC1C,CAAC;CAAG;AACL,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC,OAAO;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC,OAAO;IACrB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC,MAAM;IACzB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;CAC1B,CAAC,CAAC"}
package/dist/program.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Effect } from "effect";
2
- import type { WatchError } from "./model.ts";
3
- export declare const program: (args: readonly string[], token: string, emit: (line: string) => Effect.Effect<void, WatchError>, diagnostic: (message: string) => Effect.Effect<void, WatchError>) => Effect.Effect<undefined, WatchError, "@shivaedev/pr-watch/Watcher">;
2
+ import { type Emit } from "#health.ts";
3
+ import type { WatchError } from "#model.ts";
4
+ export declare const program: (args: readonly string[], emit: Emit, diagnostic: (message: string) => Effect.Effect<void, WatchError>) => Effect.Effect<void, WatchError, "@shivaedev/pr-watch/Watcher">;
4
5
  //# sourceMappingURL=program.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,OAAO,wDAGN,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,wBACjC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,wEAyB/D,CAAC"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,KAAK,IAAI,EAAuB,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAG5C,eAAO,MAAM,OAAO,8DAGG,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,mEAsD/D,CAAC"}
package/dist/program.js CHANGED
@@ -1,30 +1,49 @@
1
- import { Effect } from "effect";
2
- import { argumentsFor, help } from "./arguments.js";
3
- import { Watcher } from "./Watcher.js";
4
- export const program = Effect.fn("PrWatch.program")(function* (args, token, emit, diagnostic) {
5
- const parsed = yield* argumentsFor(args, token);
6
- if (parsed.help) {
7
- yield* emit(help);
8
- return;
9
- }
10
- const watcher = yield* Watcher;
11
- let state = yield* watcher.load(parsed.options);
12
- while (true) {
13
- const result = yield* Effect.result(watcher.poll(parsed.options, state, emit, diagnostic));
14
- if (result._tag === "Success") {
15
- state = result.success;
1
+ import { Effect, Exit } from "effect";
2
+ import { argumentsFor, help } from "#arguments.ts";
3
+ import { health, waitForPoll } from "#health.ts";
4
+ import { Watcher } from "#Watcher.ts";
5
+ export const program = Effect.fn("PrWatch.program")(function* (args, emit, diagnostic) {
6
+ let reported;
7
+ let watching = true;
8
+ const run = Effect.gen(function* () {
9
+ const parsed = yield* argumentsFor(args);
10
+ if (parsed.help) {
11
+ watching = false;
12
+ yield* emit(help);
13
+ return;
14
+ }
15
+ const watcher = yield* Watcher;
16
+ let state = yield* watcher.load(parsed.options);
17
+ function report(message) {
18
+ return health(emit, "error", { message }).pipe(Effect.tap(Effect.sync(() => {
19
+ reported = message;
20
+ })), Effect.andThen(diagnostic(message)));
16
21
  }
17
- else {
22
+ while (true) {
23
+ reported = undefined;
24
+ state = yield* watcher.poll(parsed.options, state, emit, report);
18
25
  if (parsed.once) {
19
- return yield* Effect.fail(result.failure);
26
+ return;
20
27
  }
21
- yield* diagnostic(result.failure.message);
22
- state = yield* watcher.load(parsed.options);
28
+ yield* waitForPoll(emit, parsed.options.interval, yield* watcher.delay(parsed.options.interval));
23
29
  }
24
- if (parsed.once) {
25
- return;
30
+ });
31
+ yield* run.pipe(Effect.tapError((error) => Effect.gen(function* () {
32
+ if (reported !== error.message) {
33
+ yield* health(emit, "error", { message: error.message });
34
+ }
35
+ if (error.retryAt !== undefined) {
36
+ yield* health(emit, "cooldown", { message: error.message, nextPollAt: new Date(error.retryAt).toISOString() });
37
+ }
38
+ })), Effect.onExit((exit) => {
39
+ if (!watching) {
40
+ return Effect.void;
41
+ }
42
+ let reason = "completed";
43
+ if (exit._tag === "Failure") {
44
+ reason = Exit.hasInterrupts(exit) ? "interrupted" : "failed";
26
45
  }
27
- yield* Effect.sleep(yield* watcher.delay(parsed.options.interval));
28
- }
46
+ return health(emit, "stopped", { reason }).pipe(Effect.orDie);
47
+ }));
29
48
  });
30
49
  //# sourceMappingURL=program.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,QAAQ,CAAC,EAC5D,IAAuB,EACvB,KAAa,EACb,IAAuD,EACvD,UAAgE;IAEhE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO;IACR,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC;IAC/B,IAAI,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAChD,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;QAC3F,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC;QACxB,CAAC;aAAM,CAAC;YACP,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBACjB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;YACD,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAC1C,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO;QACR,CAAC;QACD,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,CAAC;AACF,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"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"}
package/dist/quota.d.ts CHANGED
@@ -1,10 +1,4 @@
1
- import type { HttpClientResponse } from "effect/http";
2
- export interface Budget {
3
- blockedUntil: number;
4
- limit: number;
5
- remaining: number;
6
- reset: number;
7
- }
8
- export declare function observeBudget(budget: Budget, response: Pick<HttpClientResponse.HttpClientResponse, "headers" | "status">, now: number): void;
9
- export declare function pollDelay(budget: Budget, interval: number, requests: number, now: number): number;
1
+ import type { RateLimitObservation } from "@shivaedev/effect-github/rateLimits.ts";
2
+ export declare function quotaRetryAt(observations: readonly RateLimitObservation[], retryAt?: number): number;
3
+ export declare function pollDelay(observations: readonly RateLimitObservation[], interval: number, requests: number, now: number, retryAt?: number): number;
10
4
  //# sourceMappingURL=quota.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,MAAM;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,EAAE,SAAS,GAAG,QAAQ,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAY5I;AACD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKjG"}
1
+ {"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAEnF,wBAAgB,YAAY,CAAC,YAAY,EAAE,SAAS,oBAAoB,EAAE,EAAE,OAAO,SAAI,GAAG,MAAM,CAE/F;AAED,wBAAgB,SAAS,CAAC,YAAY,EAAE,SAAS,oBAAoB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,MAAM,CAW7I"}
package/dist/quota.js CHANGED
@@ -1,34 +1,14 @@
1
- function numeric(headers, name) {
2
- const value = headers[name];
3
- return value !== undefined && Number.isFinite(Number(value)) ? Number(value) : undefined;
1
+ import { DateTime } from "effect";
2
+ export function quotaRetryAt(observations, retryAt = 0) {
3
+ return Math.max(retryAt, ...observations.filter((budget) => budget.remaining === 0).map((budget) => DateTime.toEpochMillis(budget.reset)));
4
4
  }
5
- export function observeBudget(budget, response, now) {
6
- budget.remaining = numeric(response.headers, "x-ratelimit-remaining") ?? budget.remaining;
7
- budget.limit = numeric(response.headers, "x-ratelimit-limit") ?? budget.limit;
8
- budget.reset = (numeric(response.headers, "x-ratelimit-reset") ?? budget.reset / 1000) * 1000;
9
- if (response.status === 429 || response.status === 403) {
10
- const retry = response.headers["retry-after"] ?? null;
11
- const until = retryUntil(retry, now);
12
- budget.blockedUntil = Math.max(budget.blockedUntil, Number.isFinite(until) ? until : now + 60_000);
13
- }
14
- if (budget.remaining === 0) {
15
- budget.blockedUntil = Math.max(budget.blockedUntil, budget.reset);
16
- }
17
- }
18
- export function pollDelay(budget, interval, requests, now) {
19
- const ratio = budget.remaining / Math.max(1, budget.limit);
20
- const multiplier = slowdown(ratio);
21
- const paced = budget.remaining > 0 && ratio < 0.5 ? Math.ceil((Math.max(0, budget.reset - now) * requests) / budget.remaining) : 0;
22
- return Math.max(interval * multiplier, paced, budget.blockedUntil - now);
23
- }
24
- function retryUntil(retry, now) {
25
- if (retry === null) {
26
- return now + 60_000;
27
- }
28
- if (Number.isFinite(Number(retry))) {
29
- return now + Number(retry) * 1000;
30
- }
31
- return Date.parse(retry);
5
+ export function pollDelay(observations, interval, requests, now, retryAt = 0) {
6
+ return Math.max(interval, quotaRetryAt(observations, retryAt) - now, ...observations.map((budget) => {
7
+ const ratio = budget.remaining / Math.max(1, budget.limit);
8
+ const reset = DateTime.toEpochMillis(budget.reset);
9
+ const paced = budget.remaining > 0 && ratio < 0.5 ? Math.ceil((Math.max(0, reset - now) * requests) / budget.remaining) : 0;
10
+ return Math.max(interval * slowdown(ratio), paced);
11
+ }));
32
12
  }
33
13
  function slowdown(ratio) {
34
14
  if (ratio < 0.1) {
package/dist/quota.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"quota.js","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"quota.js","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGlC,MAAM,UAAU,YAAY,CAAC,YAA6C,EAAE,OAAO,GAAG,CAAC;IACtF,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC5I,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,YAA6C,EAAE,QAAgB,EAAE,QAAgB,EAAE,GAAW,EAAE,OAAO,GAAG,CAAC;IACpI,OAAO,IAAI,CAAC,GAAG,CACd,QAAQ,EACR,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,GAAG,EACzC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,GAAG,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5H,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;IACpD,CAAC,CAAC,CACF,CAAC;AACH,CAAC;AACD,SAAS,QAAQ,CAAC,KAAa;IAC9B,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACX,CAAC;IACD,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,CAAC,CAAC;IACV,CAAC;IACD,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,CAAC,CAAC;IACV,CAAC;IACD,OAAO,CAAC,CAAC;AACV,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/pr-watch",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "One REST watcher for pull request transitions across a repository",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/pr-watch#readme",
6
6
  "bugs": {
@@ -43,15 +43,16 @@
43
43
  "README.md"
44
44
  ],
45
45
  "dependencies": {
46
+ "@shivaedev/effect-github": "0.3.0",
46
47
  "@shivaedev/effect-service": "0.4.1"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@effect/platform-node": "4.0.5",
50
51
  "@effect/platform-node-shared": "4.0.5",
51
52
  "@effect/vitest": "4.0.5",
52
- "@shivaedev/effect-test": "0.7.2",
53
- "@shivaedev/quality": "0.34.0",
54
- "@shivaedev/test-story": "0.18.2",
53
+ "@shivaedev/effect-test": "0.8.1",
54
+ "@shivaedev/quality": "0.39.0",
55
+ "@shivaedev/test-story": "0.19.0",
55
56
  "@types/node": "24.19.2",
56
57
  "effect": "4.0.5",
57
58
  "typescript": "7.0.2",
package/src/GitHub.ts CHANGED
@@ -28,21 +28,7 @@ export const GitHub = defineService({
28
28
  return Schema.decodeUnknownEffect(schema)(body).pipe(Effect.mapError(() => new WatchError({ message: "Invalid GitHub response" })));
29
29
  }
30
30
  function pages(options: Options, path: string) {
31
- return Effect.gen(function* () {
32
- const bodies: unknown[] = [];
33
- const visited = new Set<string>();
34
- let url: string | undefined = `${options.apiUrl}/repos/${options.repository}${path}`;
35
- while (url !== undefined) {
36
- if (visited.has(url)) {
37
- return yield* Effect.fail(new WatchError({ message: "Repeated pagination URL" }));
38
- }
39
- visited.add(url);
40
- const response: Effect.Success<ReturnType<typeof http.get>> = yield* http.get(url, options.token);
41
- bodies.push(response.body);
42
- url = response.next;
43
- }
44
- return bodies;
45
- });
31
+ return http.list(`${options.apiUrl}/repos/${options.repository}${path}`);
46
32
  }
47
33
  return {
48
34
  confirmQueue: (options: Options, snapshot: Snapshot) => confirmQueue(http, options, snapshot),
@@ -66,8 +52,8 @@ export const GitHub = defineService({
66
52
  }),
67
53
  snapshot: (options: Options, number: number, queued: ReadonlySet<number>) =>
68
54
  Effect.gen(function* () {
69
- const response = yield* http.get(`${options.apiUrl}/repos/${options.repository}/pulls/${number}`, options.token);
70
- const pr = yield* decode(Pull, response.body);
55
+ const response = yield* http.get(`${options.apiUrl}/repos/${options.repository}/pulls/${number}`);
56
+ const pr = yield* decode(Pull, response);
71
57
  if (pr.number !== number) {
72
58
  return yield* Effect.fail(new WatchError({ message: "GitHub returned the wrong PR" }));
73
59
  }
@@ -78,8 +64,8 @@ export const GitHub = defineService({
78
64
  if (pr.state === "open") {
79
65
  const bodies = yield* pages(options, `/commits/${pr.head.sha}/check-runs?per_page=100&filter=latest`);
80
66
  const checks = yield* Effect.forEach(bodies, (body) => decode(Checks, body));
81
- const statusResponse = yield* http.get(`${options.apiUrl}/repos/${options.repository}/commits/${pr.head.sha}/status`, options.token);
82
- const status = yield* decode(Status, statusResponse.body);
67
+ const statusResponse = yield* http.get(`${options.apiUrl}/repos/${options.repository}/commits/${pr.head.sha}/status`);
68
+ const status = yield* decode(Status, statusResponse);
83
69
  failed =
84
70
  checks.some((page) => page.checkRuns.some((check) => check.status === "completed" && failures.has(check.conclusion ?? "")))
85
71
  || ["failure", "error"].includes(status.state);
package/src/GitHubHttp.ts CHANGED
@@ -1,55 +1,71 @@
1
- import { Clock, Effect } from "effect";
2
- import { HttpClient } from "effect/http";
1
+ import { Clock, DateTime, Effect, Schema, Stream } from "effect";
2
+ import { GitHubRateLimited } from "@shivaedev/effect-github/errors.ts";
3
+ import { GitHub } from "@shivaedev/effect-github/GitHub.ts";
3
4
  import { defineService } from "@shivaedev/effect-service/defineService.ts";
4
- import { type CachedResponse, decodeResponse, fetchResponse } from "./httpResponse.ts";
5
- import { WatchError } from "./model.ts";
6
- import { type Budget, observeBudget, pollDelay } from "./quota.ts";
5
+ import { WatchError } from "#model.ts";
6
+ import { pollDelay, quotaRetryAt } from "#quota.ts";
7
+
7
8
  export const GitHubHttp = defineService({
8
9
  id: "@shivaedev/pr-watch/GitHubHttp",
9
10
  initialize: Effect.gen(function* () {
10
- return {
11
- budget: { blockedUntil: 0, limit: 5000, remaining: 5000, reset: 0 } satisfies Budget,
12
- cache: new Map<string, CachedResponse>(),
13
- client: yield* HttpClient.HttpClient,
14
- requests: 0,
15
- };
11
+ return { github: yield* GitHub, requests: 0, retryAt: 0 };
16
12
  }),
17
13
  methods: (state) => {
18
- function request(url: string, token: string, body?: unknown) {
14
+ function request<T, E, R>(operation: Effect.Effect<T, E, R>) {
19
15
  return Effect.gen(function* () {
20
16
  const now = yield* Clock.currentTimeMillis;
21
- if (state.budget.blockedUntil > now) {
22
- return yield* Effect.fail(new WatchError({ message: "GitHub quota cooldown" }));
23
- }
24
- const cached = body === undefined ? state.cache.get(url) : undefined;
25
- state.requests += 1;
26
- const response = yield* fetchResponse(state.client, url, token, cached, body);
27
- observeBudget(state.budget, response, yield* Clock.currentTimeMillis);
28
- if (response.status === 304 && cached !== undefined) {
29
- return cached;
17
+ const observations = yield* state.github.observations();
18
+ if (quotaRetryAt(observations, state.retryAt) > now) {
19
+ return yield* new WatchError({ message: "GitHub quota cooldown", retryAt: quotaRetryAt(observations, state.retryAt) });
30
20
  }
31
- const result = yield* decodeResponse(url, response);
32
- if (body === undefined) {
33
- state.cache.set(url, result);
34
- }
35
- return result;
21
+ return yield* operation;
36
22
  }).pipe(
37
23
  Effect.scoped,
38
24
  Effect.timeout("30 seconds"),
39
- Effect.mapError((cause) => (cause instanceof WatchError ? cause : new WatchError({ message: cause.message }))),
25
+ Effect.tapError((cause) =>
26
+ Effect.sync(() => {
27
+ if (cause instanceof GitHubRateLimited) {
28
+ state.retryAt = DateTime.toEpochMillis(cause.retryAt);
29
+ }
30
+ }),
31
+ ),
32
+ Effect.mapError((cause) => {
33
+ if (cause instanceof WatchError) {
34
+ return cause;
35
+ }
36
+ if (cause instanceof GitHubRateLimited) {
37
+ return new WatchError({ message: "GitHub quota cooldown", retryAt: DateTime.toEpochMillis(cause.retryAt) });
38
+ }
39
+ return new WatchError({ message: "GitHub request failed" });
40
+ }),
40
41
  );
41
42
  }
43
+ function counted<T, E, R>(operation: Effect.Effect<T, E, R>) {
44
+ return Effect.suspend(() => {
45
+ state.requests += 1;
46
+ return operation;
47
+ });
48
+ }
42
49
  return {
43
50
  delay: (interval: number) =>
44
51
  Effect.gen(function* () {
45
52
  const now = yield* Clock.currentTimeMillis;
46
- const delay = pollDelay(state.budget, interval, Math.max(1, state.requests), now);
53
+ const observations = yield* state.github.observations();
54
+ const delay = pollDelay(observations, interval, Math.max(1, state.requests), now, state.retryAt);
47
55
  state.requests = 0;
48
56
  return delay;
49
57
  }),
50
- get: (url: string, token: string) => request(url, token),
51
- post: (url: string, token: string, body: unknown) => request(url, token, body),
58
+ get: (url: string) => request(counted(state.github.read(url)).pipe(Effect.flatMap((response) => response.decode(Schema.Unknown)))),
59
+ list: (url: string) =>
60
+ request(
61
+ Stream.unwrap(state.github.list(url)).pipe(
62
+ Stream.mapEffect((response) => counted(response.decode(Schema.Unknown))),
63
+ Stream.runCollect,
64
+ ),
65
+ ),
66
+ post: (url: string, body: unknown) =>
67
+ request(counted(state.github.write("POST", url, body)).pipe(Effect.flatMap((response) => response.decode(Schema.Unknown)))),
52
68
  };
53
69
  },
54
- requires: [HttpClient.HttpClient],
70
+ requires: [GitHub],
55
71
  });
package/src/Watcher.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Effect } from "effect";
2
2
  import { defineService } from "@shivaedev/effect-service/defineService.ts";
3
- import { events } from "./events.ts";
4
- import { GitHub } from "./GitHub.ts";
5
- import type { Options, Snapshot, State, WatchError } from "./model.ts";
6
- import { StateFile } from "./StateFile.ts";
3
+ import { events } from "#events.ts";
4
+ import { GitHub } from "#GitHub.ts";
5
+ import type { Options, Snapshot, State, WatchError } from "#model.ts";
6
+ import { StateFile } from "#StateFile.ts";
7
7
 
8
8
  function activeNumbers(options: Options, state: State) {
9
9
  return [...new Set([...options.numbers, ...(state.openNumbers ?? []), ...Object.keys(state.prs).map(Number)])].filter((number) => {
@@ -25,7 +25,7 @@ export const Watcher = defineService({
25
25
  before === undefined && snapshot.state === "closed" && state.openNumbers?.includes(snapshot.number)
26
26
  ? { ...snapshot, merged: false, state: "open" as const }
27
27
  : before;
28
- for (const line of events(baseline, snapshot)) {
28
+ for (const line of events(baseline, snapshot, options.events)) {
29
29
  yield* emit(line);
30
30
  }
31
31
  const next = {
package/src/arguments.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { Effect } from "effect";
3
- import { type Options, WatchError } from "./model.ts";
3
+ import { type EventKind, eventKinds } from "#events.ts";
4
+ import { type Options, WatchError } from "#model.ts";
4
5
  export const help = `pr-watch --repo OWNER/REPO [--fleet | PR_NUMBER ...]
5
6
  --state PATH Snapshot file (default .pr-watch/OWNER-REPO.json)
6
7
  --interval SECONDS Poll interval (default 60; slows as quota falls)
8
+ --events KINDS Comma-separated PR event kinds (health lines always print)
7
9
  --once Poll once and exit
8
10
  --api-url URL GitHub REST base URL (default https://api.github.com)
9
11
  --help Show this help
10
12
  Authentication: GH_TOKEN or GITHUB_TOKEN`;
11
- export function argumentsFor(args: readonly string[], token: string) {
13
+ export function argumentsFor(args: readonly string[]) {
12
14
  return Effect.try({
13
15
  catch: (cause) => new WatchError({ message: cause instanceof Error ? cause.message : "Invalid arguments" }),
14
16
  try: () => {
@@ -17,6 +19,7 @@ export function argumentsFor(args: readonly string[], token: string) {
17
19
  args: [...args],
18
20
  options: {
19
21
  "api-url": { type: "string" },
22
+ events: { type: "string" },
20
23
  fleet: { type: "boolean" },
21
24
  help: { type: "boolean" },
22
25
  interval: { type: "string" },
@@ -32,6 +35,7 @@ export function argumentsFor(args: readonly string[], token: string) {
32
35
  if (repository.match(/^[\w.-]+\/[\w.-]+$/u) === null) {
33
36
  throw new Error("--repo OWNER/REPO is required");
34
37
  }
38
+ const selected = eventsFor(values.events);
35
39
  const numbers = numbersFor(positionals, values.fleet ?? false);
36
40
  const interval = intervalFor(values.interval);
37
41
  const apiUrl = apiUrlFor(values["api-url"]);
@@ -40,12 +44,12 @@ export function argumentsFor(args: readonly string[], token: string) {
40
44
  once: values.once ?? false,
41
45
  options: {
42
46
  apiUrl,
47
+ ...(selected === undefined ? {} : { events: selected }),
43
48
  fleet: values.fleet ?? false,
44
49
  interval,
45
50
  numbers,
46
51
  repository,
47
52
  stateFile: values.state ?? `.pr-watch/${repository.replace("/", "-")}.json`,
48
- token,
49
53
  } satisfies Options,
50
54
  };
51
55
  },
@@ -81,3 +85,16 @@ function apiUrlFor(value: string | undefined): string {
81
85
  }
82
86
  return apiUrl;
83
87
  }
88
+
89
+ function eventsFor(value: string | undefined): readonly EventKind[] | undefined {
90
+ if (value === undefined) {
91
+ return undefined;
92
+ }
93
+ return value.split(",").map((name) => {
94
+ const kind = eventKinds.find((valid) => valid === name);
95
+ if (kind === undefined) {
96
+ throw new Error(`Unknown event kind ${JSON.stringify(name)}. Valid kinds: ${eventKinds.join(", ")}`);
97
+ }
98
+ return kind;
99
+ });
100
+ }