@groundnuty/macf-channel-server 0.2.47 → 0.2.49

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 (51) hide show
  1. package/dist/collision.d.ts +28 -3
  2. package/dist/collision.d.ts.map +1 -1
  3. package/dist/collision.js +78 -18
  4. package/dist/collision.js.map +1 -1
  5. package/dist/delivery/driver.d.ts +45 -0
  6. package/dist/delivery/driver.d.ts.map +1 -0
  7. package/dist/delivery/driver.js +34 -0
  8. package/dist/delivery/driver.js.map +1 -0
  9. package/dist/delivery/in-memory-store.d.ts +24 -0
  10. package/dist/delivery/in-memory-store.d.ts.map +1 -0
  11. package/dist/delivery/in-memory-store.js +60 -0
  12. package/dist/delivery/in-memory-store.js.map +1 -0
  13. package/dist/delivery/inbox-drain-driver.d.ts +150 -0
  14. package/dist/delivery/inbox-drain-driver.d.ts.map +1 -0
  15. package/dist/delivery/inbox-drain-driver.js +109 -0
  16. package/dist/delivery/inbox-drain-driver.js.map +1 -0
  17. package/dist/delivery/inbox-ticker.d.ts +45 -0
  18. package/dist/delivery/inbox-ticker.d.ts.map +1 -0
  19. package/dist/delivery/inbox-ticker.js +68 -0
  20. package/dist/delivery/inbox-ticker.js.map +1 -0
  21. package/dist/delivery/inbox.d.ts +57 -0
  22. package/dist/delivery/inbox.d.ts.map +1 -0
  23. package/dist/delivery/inbox.js +19 -0
  24. package/dist/delivery/inbox.js.map +1 -0
  25. package/dist/delivery/message-id.d.ts +10 -0
  26. package/dist/delivery/message-id.d.ts.map +1 -0
  27. package/dist/delivery/message-id.js +33 -0
  28. package/dist/delivery/message-id.js.map +1 -0
  29. package/dist/delivery/outbox-ticker.d.ts +50 -0
  30. package/dist/delivery/outbox-ticker.d.ts.map +1 -0
  31. package/dist/delivery/outbox-ticker.js +49 -0
  32. package/dist/delivery/outbox-ticker.js.map +1 -0
  33. package/dist/delivery/outbox.d.ts +105 -0
  34. package/dist/delivery/outbox.d.ts.map +1 -0
  35. package/dist/delivery/outbox.js +103 -0
  36. package/dist/delivery/outbox.js.map +1 -0
  37. package/dist/https.d.ts +24 -0
  38. package/dist/https.d.ts.map +1 -1
  39. package/dist/https.js +70 -4
  40. package/dist/https.js.map +1 -1
  41. package/dist/notify-peer.d.ts +70 -0
  42. package/dist/notify-peer.d.ts.map +1 -1
  43. package/dist/notify-peer.js +142 -29
  44. package/dist/notify-peer.js.map +1 -1
  45. package/dist/server.js +100 -2
  46. package/dist/server.js.map +1 -1
  47. package/dist/shutdown.d.ts +18 -0
  48. package/dist/shutdown.d.ts.map +1 -1
  49. package/dist/shutdown.js +18 -1
  50. package/dist/shutdown.js.map +1 -1
  51. package/package.json +2 -2
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Grace window before an undrained entry is considered a genuine orphan
3
+ * (constraint 1). Deliberately >= the ticker's own tick interval — the
4
+ * accept()→onNotify()→markProcessed() sequence on the ORIGINAL receive path
5
+ * completes in well under a second in practice, so an interval comfortably
6
+ * larger than one tick cycle can never mistake a mid-flight entry for an
7
+ * orphan.
8
+ */
9
+ export const DEFAULT_INBOX_DRAIN_GRACE_MS = 30_000;
10
+ /**
11
+ * Attempt cap (constraint 3). Small + finite — unlike the outbox's
12
+ * wall-clock-only retry budget (Decision 4 explicitly rejects an attempt
13
+ * count there, because restart gaps make attempt-count meaningless across
14
+ * process lifetimes), THIS driver's retries are only ever in-process (a
15
+ * fresh process gets a fresh, empty attempts `Map` + — today — a fresh,
16
+ * empty in-memory inbox), so a small attempt cap is the right complementary
17
+ * bound to the TTL below: it stops hammering a persistently-broken
18
+ * `onNotify` path quickly, without waiting a full day to give up.
19
+ */
20
+ export const DEFAULT_INBOX_MAX_ATTEMPTS = 5;
21
+ /**
22
+ * Wall-clock TTL (constraint 3), mirroring the outbox's `DEFAULT_TTL_MS`
23
+ * (`outbox.ts`) — same 24h budget, same rationale (an 8h-class outage fits
24
+ * comfortably; anything longer legitimately downgrades to the give-up path).
25
+ */
26
+ export const DEFAULT_INBOX_TTL_MS = 24 * 60 * 60 * 1000;
27
+ /**
28
+ * Drive one orphan-drain tick: for every undrained inbox entry, either skip
29
+ * it (too young — constraint 1; or already exhausted — constraint 3), give
30
+ * it up (TTL exceeded — constraint 3), or re-fire `onNotify` (success →
31
+ * `markProcessed`, constraint 2; failure → bump the attempt counter,
32
+ * constraint 3). Callable directly by tests with an explicit `now` + a
33
+ * fresh `attempts` Map, or wrapped in `inbox-ticker.ts`'s periodic timer.
34
+ */
35
+ export async function driveInboxOnce(deps) {
36
+ const { inbox, onNotify, attempts, logger } = deps;
37
+ const now = deps.now ?? Date.now;
38
+ const graceMs = deps.graceMs ?? DEFAULT_INBOX_DRAIN_GRACE_MS;
39
+ const maxAttempts = deps.maxAttempts ?? DEFAULT_INBOX_MAX_ATTEMPTS;
40
+ const ttlMs = deps.ttlMs ?? DEFAULT_INBOX_TTL_MS;
41
+ const t = now();
42
+ const undrained = await inbox.undrained();
43
+ let attempted = 0;
44
+ let drained = 0;
45
+ let failed = 0;
46
+ let gaveUp = 0;
47
+ for (const entry of undrained) {
48
+ const priorAttempts = attempts.get(entry.id) ?? 0;
49
+ // Constraint 3 (already exhausted): don't re-evaluate age at all once
50
+ // capped — permanently given-up for THIS driver; only the future
51
+ // surface-as-text path (constraint 4) is left to deal with it.
52
+ if (priorAttempts >= maxAttempts) {
53
+ gaveUp++;
54
+ continue;
55
+ }
56
+ const age = t - entry.receivedAt;
57
+ // Constraint 3 (TTL): checked before the grace window — an entry old
58
+ // enough to exceed the TTL is never "too young to be an orphan"; TTL
59
+ // (default 24h) is always far larger than the grace window (default
60
+ // 30s), so the two checks never actually compete in practice.
61
+ if (age > ttlMs) {
62
+ attempts.set(entry.id, maxAttempts); // pin capped so future ticks short-circuit above
63
+ gaveUp++;
64
+ logger?.warn('inbox_drain_gave_up', {
65
+ id: entry.id,
66
+ reason: 'ttl_exceeded',
67
+ age_ms: age,
68
+ ttl_ms: ttlMs,
69
+ });
70
+ continue;
71
+ }
72
+ // Constraint 1 (grace-before-drain): too young to be a genuine orphan —
73
+ // may still be mid-flight in the ORIGINAL accept()→onNotify()→
74
+ // markProcessed() sequence. Leave it for a later tick.
75
+ if (age < graceMs) {
76
+ continue;
77
+ }
78
+ attempted++;
79
+ try {
80
+ // See InboxDriveDeps.onNotify's doc comment for why this cast is sound.
81
+ await onNotify(entry.payload);
82
+ // Constraint 2: markProcessed ONLY after a confirmed onNotify success.
83
+ await inbox.markProcessed(entry.id);
84
+ attempts.delete(entry.id);
85
+ drained++;
86
+ }
87
+ catch (err) {
88
+ failed++;
89
+ const nextAttempts = priorAttempts + 1;
90
+ attempts.set(entry.id, nextAttempts);
91
+ logger?.warn('inbox_drain_refire_failed', {
92
+ id: entry.id,
93
+ attempt: nextAttempts,
94
+ max_attempts: maxAttempts,
95
+ error: err instanceof Error ? err.message : String(err),
96
+ });
97
+ if (nextAttempts >= maxAttempts) {
98
+ gaveUp++;
99
+ logger?.warn('inbox_drain_gave_up', {
100
+ id: entry.id,
101
+ reason: 'max_attempts_exceeded',
102
+ attempts: nextAttempts,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ return { attempted, drained, failed, gaveUp };
108
+ }
109
+ //# sourceMappingURL=inbox-drain-driver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox-drain-driver.js","sourceRoot":"","sources":["../../src/delivery/inbox-drain-driver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * DR-038 Decision 5 follow-on (groundnuty/macf#744) — the mechanism that
3
+ * DRIVES `inbox-drain-driver.ts`'s `driveInboxOnce()` periodically over the
4
+ * channel-server's lifetime, so the receiver's own in-process inbox orphans
5
+ * (an entry whose original `onNotify` threw between `inbox.accept()` and
6
+ * `inbox.markProcessed()`) get re-fired without waiting for a restart or the
7
+ * plugin's separate, short-lived startup-drain (which can't see this
8
+ * process's own in-memory orphans).
9
+ *
10
+ * Exact same lifecycle shape as `outbox-ticker.ts` (`start()` / `stop()` /
11
+ * `tickNow()`, unref()'d interval, tick body never throws past the timer) —
12
+ * intentionally, so the two DR-038 tickers read as one family in
13
+ * `server.ts` + `shutdown.ts`.
14
+ *
15
+ * `server.ts` calls `tickNow()` once at startup (mirroring the outbox
16
+ * ticker's own startup tick) BEFORE starting the recurring interval, so any
17
+ * orphan already undrained when this process comes up gets an immediate
18
+ * first re-fire attempt rather than waiting a full tick interval.
19
+ */
20
+ import type { Logger, NotifyPayload } from '@groundnuty/macf-core';
21
+ import type { Inbox } from './inbox.js';
22
+ /** Default retry-drive cadence. Same cadence as the outbox ticker. */
23
+ export declare const DEFAULT_INBOX_TICK_INTERVAL_MS = 30000;
24
+ export interface InboxTicker {
25
+ /** Start the periodic drive tick (no-op if already started). */
26
+ readonly start: () => void;
27
+ /** Clear the interval (idempotent; safe before start). */
28
+ readonly stop: () => void;
29
+ /** Fire one drive tick immediately, outside the interval schedule. */
30
+ readonly tickNow: () => Promise<void>;
31
+ }
32
+ export declare function createInboxTicker(opts: {
33
+ readonly inbox: Inbox;
34
+ /** The SAME `onNotify` callback passed to `createHttpsServer`. */
35
+ readonly onNotify: (payload: NotifyPayload) => Promise<void>;
36
+ readonly logger: Logger;
37
+ /** Cadence in ms; default 30s. */
38
+ readonly intervalMs?: number;
39
+ readonly graceMs?: number;
40
+ readonly maxAttempts?: number;
41
+ readonly ttlMs?: number;
42
+ /** Injectable epoch-ms clock (testing). Default `Date.now`. */
43
+ readonly now?: () => number;
44
+ }): InboxTicker;
45
+ //# sourceMappingURL=inbox-ticker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox-ticker.d.ts","sourceRoot":"","sources":["../../src/delivery/inbox-ticker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAQxC,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,QAAS,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B,GAAG,WAAW,CA+Dd"}
@@ -0,0 +1,68 @@
1
+ import { driveInboxOnce, DEFAULT_INBOX_DRAIN_GRACE_MS, DEFAULT_INBOX_MAX_ATTEMPTS, DEFAULT_INBOX_TTL_MS, } from './inbox-drain-driver.js';
2
+ /** Default retry-drive cadence. Same cadence as the outbox ticker. */
3
+ export const DEFAULT_INBOX_TICK_INTERVAL_MS = 30_000;
4
+ export function createInboxTicker(opts) {
5
+ const { inbox, onNotify, logger } = opts;
6
+ const intervalMs = opts.intervalMs ?? DEFAULT_INBOX_TICK_INTERVAL_MS;
7
+ const graceMs = opts.graceMs ?? DEFAULT_INBOX_DRAIN_GRACE_MS;
8
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_INBOX_MAX_ATTEMPTS;
9
+ const ttlMs = opts.ttlMs ?? DEFAULT_INBOX_TTL_MS;
10
+ const now = opts.now ?? Date.now;
11
+ // Process-lifetime attempt counter (DR-038 Decision 5 follow-on constraint
12
+ // 3) — never persisted, never touches the InboxStore/InboxEntry schema,
13
+ // reset to empty on every fresh process. This IS the "driver-locally"
14
+ // bookkeeping the design calls for: the ticker owns the one long-lived Map
15
+ // instance and hands it to `driveInboxOnce` unchanged on every tick so
16
+ // attempt counts actually accumulate across ticks.
17
+ const attempts = new Map();
18
+ let timer = null;
19
+ async function tickNow() {
20
+ try {
21
+ const summary = await driveInboxOnce({
22
+ inbox,
23
+ onNotify,
24
+ attempts,
25
+ logger,
26
+ now,
27
+ graceMs,
28
+ maxAttempts,
29
+ ttlMs,
30
+ });
31
+ if (summary.attempted > 0 || summary.gaveUp > 0) {
32
+ logger.info('inbox_drain_tick', {
33
+ attempted: summary.attempted,
34
+ drained: summary.drained,
35
+ failed: summary.failed,
36
+ gave_up: summary.gaveUp,
37
+ });
38
+ }
39
+ }
40
+ catch (err) {
41
+ // Best-effort: a driveInboxOnce failure (store I/O error, etc.) must
42
+ // never escape a timer callback and become an uncaughtException — same
43
+ // posture as outbox-ticker.ts's tick(). The next tick retries.
44
+ logger.warn('inbox_drain_tick_failed', {
45
+ error: err instanceof Error ? err.message : String(err),
46
+ });
47
+ }
48
+ }
49
+ return {
50
+ start() {
51
+ if (timer !== null)
52
+ return;
53
+ timer = setInterval(() => {
54
+ void tickNow();
55
+ }, intervalMs);
56
+ // Never let the drain-drive interval keep the process alive.
57
+ timer.unref();
58
+ },
59
+ stop() {
60
+ if (timer !== null) {
61
+ clearInterval(timer);
62
+ timer = null;
63
+ }
64
+ },
65
+ tickNow,
66
+ };
67
+ }
68
+ //# sourceMappingURL=inbox-ticker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox-ticker.js","sourceRoot":"","sources":["../../src/delivery/inbox-ticker.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,cAAc,EACd,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AAEjC,sEAAsE;AACtE,MAAM,CAAC,MAAM,8BAA8B,GAAG,MAAM,CAAC;AAWrD,MAAM,UAAU,iBAAiB,CAAC,IAYjC;IACC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,8BAA8B,CAAC;IACrE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,4BAA4B,CAAC;IAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,0BAA0B,CAAC;IACnE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,oBAAoB,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,2EAA2E;IAC3E,wEAAwE;IACxE,sEAAsE;IACtE,2EAA2E;IAC3E,uEAAuE;IACvE,mDAAmD;IACnD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,IAAI,KAAK,GAA0C,IAAI,CAAC;IAExD,KAAK,UAAU,OAAO;QACpB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC;gBACnC,KAAK;gBACL,QAAQ;gBACR,QAAQ;gBACR,MAAM;gBACN,GAAG;gBACH,OAAO;gBACP,WAAW;gBACX,KAAK;aACN,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,SAAS,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChD,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE;oBAC9B,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,OAAO,EAAE,OAAO,CAAC,MAAM;iBACxB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,uEAAuE;YACvE,+DAA+D;YAC/D,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE;gBACrC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK;YACH,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO;YAC3B,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;gBACvB,KAAK,OAAO,EAAE,CAAC;YACjB,CAAC,EAAE,UAAU,CAAC,CAAC;YACf,6DAA6D;YAC7D,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,CAAC;QACD,IAAI;YACF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrB,KAAK,GAAG,IAAI,CAAC;YACf,CAAC;QACH,CAAC;QACD,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * DR-038 Decisions 1, 3, 5 — the receiver-side effectively-once logic:
3
+ * persist-THEN-ack ordering (Decision 3), and the store-backed primitives
4
+ * the inbox drain (Decision 5) needs.
5
+ *
6
+ * Runtime-agnostic over an injected `InboxStore` (the pluggable driver
7
+ * interface, hoisted to `@groundnuty/macf-core`'s `delivery/store.ts` —
8
+ * macf#741). The drain TRIGGER itself — hooking `accept` into the HTTP
9
+ * `/notify` / `message/send` handler (on-receipt), and hooking `undrained`
10
+ * + `markProcessed` into the plugin's SessionStart `startup_check`
11
+ * (on-startup) — is a follow-on slice. This module provides the store-
12
+ * backed primitives both triggers call into, with the dedup-by-id property
13
+ * that makes the two triggers safe to run overlapping (Decision 5: "no
14
+ * periodic drain is needed if receipt + startup cover it").
15
+ */
16
+ import type { InboxStore, InboxEntry } from '@groundnuty/macf-core';
17
+ export interface InboxAcceptResult {
18
+ /**
19
+ * `true` when this `id` was genuinely new — now durably persisted for
20
+ * the first time. `false` when it was a redelivery: the existing
21
+ * persisted entry is left untouched (idempotent no-op per Decision 1's
22
+ * "idempotent receipt").
23
+ */
24
+ readonly wasNew: boolean;
25
+ }
26
+ export interface InboxDeps {
27
+ readonly store: InboxStore;
28
+ /** Injectable epoch-ms clock (testing). Default `Date.now`. */
29
+ readonly now?: () => number;
30
+ }
31
+ export interface Inbox {
32
+ /**
33
+ * Persist-THEN-ack (DR-038 Decision 3). The caller — the HTTP handler at
34
+ * the `/notify` or `message/send` boundary — MUST `await` this and
35
+ * return its HTTP 200 only AFTER it resolves. Returning 200 before this
36
+ * resolves would make the ACK mean "received" instead of "durably
37
+ * yours," reproducing the exact silent-fallback shape (success at the
38
+ * boundary, semantic durability absent — see `silent-fallback-hazards.md`)
39
+ * Decision 3 exists to avoid.
40
+ *
41
+ * A re-delivered `id` dedups here (`wasNew: false`) rather than
42
+ * re-persisting or re-processing — the caller still returns 200 per
43
+ * Decision 3 ("a lost-in-transit 200 is safe: the sender re-sends the
44
+ * same id → the receiver dedups... → re-returns 200. Idempotent by
45
+ * construction.").
46
+ */
47
+ readonly accept: (id: string, payload: unknown) => Promise<InboxAcceptResult>;
48
+ /**
49
+ * Persisted-but-not-processed entries — DR-038 Decision 5's drain input,
50
+ * read by both the on-receipt and on-startup triggers.
51
+ */
52
+ readonly undrained: () => Promise<InboxEntry[]>;
53
+ /** Mark an entry processed once the drain has handled it — won't be re-drained. */
54
+ readonly markProcessed: (id: string) => Promise<void>;
55
+ }
56
+ export declare function createInbox(deps: InboxDeps): Inbox;
57
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/delivery/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,KAAK;IACpB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAE9E;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAEhD,mFAAmF;IACnF,QAAQ,CAAC,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAkBlD"}
@@ -0,0 +1,19 @@
1
+ export function createInbox(deps) {
2
+ const { store } = deps;
3
+ const now = deps.now ?? Date.now;
4
+ return {
5
+ async accept(id, payload) {
6
+ const entry = {
7
+ id,
8
+ payload,
9
+ receivedAt: now(),
10
+ processed: false,
11
+ };
12
+ const wasNew = await store.persist(entry);
13
+ return { wasNew };
14
+ },
15
+ undrained: () => store.listUndrained(),
16
+ markProcessed: (id) => store.markProcessed(id),
17
+ };
18
+ }
19
+ //# sourceMappingURL=inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/delivery/inbox.ts"],"names":[],"mappings":"AA6DA,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IAEjC,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,OAAgB;YACvC,MAAM,KAAK,GAAe;gBACxB,EAAE;gBACF,OAAO;gBACP,UAAU,EAAE,GAAG,EAAE;gBACjB,SAAS,EAAE,KAAK;aACjB,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC1C,OAAO,EAAE,MAAM,EAAE,CAAC;QACpB,CAAC;QACD,SAAS,EAAE,GAA0B,EAAE,CAAC,KAAK,CAAC,aAAa,EAAE;QAC7D,aAAa,EAAE,CAAC,EAAU,EAAiB,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;KACtE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Mint a fresh message-id for a NEW outbox entry. Call this exactly ONCE per
3
+ * logical message, at enqueue time — never again for the same message on
4
+ * retry (the persisted id is what gets reused). `randomUUID()` is Node's
5
+ * CSPRNG-backed UUIDv4 generator (no weak-PRNG concern; matches the
6
+ * `no-weak-prng` invariant already enforced elsewhere in this package, e.g.
7
+ * `notify-peer.ts` / `https.ts`).
8
+ */
9
+ export declare function mintMessageId(): string;
10
+ //# sourceMappingURL=message-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-id.d.ts","sourceRoot":"","sources":["../../src/delivery/message-id.ts"],"names":[],"mappings":"AAsBA;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * DR-038 Decision 2 — the message-id contract for the effectively-once
3
+ * delivery layer.
4
+ *
5
+ * A UUIDv4, minted ONCE at outbox-enqueue, persisted with the entry BEFORE
6
+ * the first send, and reused VERBATIM on every retry — including across a
7
+ * sender restart (the id is stable because it's persisted, never
8
+ * regenerated). This is the whole receiver-side dedup key (Decision 1):
9
+ * `InboxStore.persist()` dedups by this id, which is what makes
10
+ * `message/send` safe to retry without the operation itself being
11
+ * idempotent.
12
+ *
13
+ * NOT a content-hash of the payload: two legitimately-identical messages
14
+ * (a peer sending "ping" twice, deliberately) would wrongly dedup-collide to
15
+ * one under a content-hash scheme. NOT a ULID: its only edge over UUIDv4 is
16
+ * a lexicographic time-ordering hint, but DR-038 explicitly scopes
17
+ * guaranteed ordering OUT of this delivery layer (see the DR's "Boundaries"
18
+ * section), and both `OutboxEntry` and `InboxEntry` already carry their own
19
+ * timestamp field for drain-ordering — so the id itself needn't encode time.
20
+ */
21
+ import { randomUUID } from 'node:crypto';
22
+ /**
23
+ * Mint a fresh message-id for a NEW outbox entry. Call this exactly ONCE per
24
+ * logical message, at enqueue time — never again for the same message on
25
+ * retry (the persisted id is what gets reused). `randomUUID()` is Node's
26
+ * CSPRNG-backed UUIDv4 generator (no weak-PRNG concern; matches the
27
+ * `no-weak-prng` invariant already enforced elsewhere in this package, e.g.
28
+ * `notify-peer.ts` / `https.ts`).
29
+ */
30
+ export function mintMessageId() {
31
+ return randomUUID();
32
+ }
33
+ //# sourceMappingURL=message-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-id.js","sourceRoot":"","sources":["../../src/delivery/message-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * DR-038 Decision 4 — the mechanism that DRIVES the persisted outbox's retry
3
+ * loop over the channel-server's lifetime.
4
+ *
5
+ * `outbox.ts`'s `driveOnce()` is a pure, callable-on-demand function; nothing
6
+ * in Slice A ticks it periodically. Two triggers combine to give the full
7
+ * restart-survival + eventual-retry contract:
8
+ *
9
+ * 1. `notify_peer`'s own enqueue path calls `driveOnce()` once immediately
10
+ * after enqueueing (see `notify-peer.ts`) — the reachable-peer common
11
+ * case is delivered inline, same UX as pre-DR-038 fire-and-forget.
12
+ * 2. THIS periodic, unref()'d ticker — mirrors `alive-ticker.ts` /
13
+ * `registry-heartbeat.ts`'s exact lifecycle shape (`start()` / `stop()`,
14
+ * never pins the event loop, tick body never throws past the timer) —
15
+ * is what drives backed-off RETRIES for an unreachable peer, and
16
+ * eventually TTL-expiry dead-lettering, WITHOUT requiring another
17
+ * `notify_peer` call to happen to trigger the next attempt. Without
18
+ * this, a message to a peer that's down for hours would sit in the
19
+ * outbox forever unless some other notify_peer call happened to touch
20
+ * the same store and incidentally drive it — that's not a retry
21
+ * schedule, that's luck.
22
+ *
23
+ * `server.ts` also calls `tickNow()` once at startup (DR-038 Decision 4:
24
+ * "resumed on sender startup") BEFORE starting the recurring interval, so any
25
+ * entry already overdue when the process comes up (a message enqueued by a
26
+ * prior process instance, surviving via a durable store driver once DR-008
27
+ * lands) gets its first post-restart attempt immediately rather than waiting
28
+ * a full tick interval.
29
+ */
30
+ import type { Logger } from '@groundnuty/macf-core';
31
+ import type { Outbox } from './outbox.js';
32
+ /** Default retry-drive cadence. Deliberately coarser than the alive-ticker's
33
+ * 60s — this is a backstop for the backoff schedule (1s..5min), not a
34
+ * liveness signal; a shorter interval would mostly find nothing due. */
35
+ export declare const DEFAULT_OUTBOX_TICK_INTERVAL_MS = 30000;
36
+ export interface OutboxTicker {
37
+ /** Start the periodic drive tick (no-op if already started). */
38
+ readonly start: () => void;
39
+ /** Clear the interval (idempotent; safe before start). */
40
+ readonly stop: () => void;
41
+ /** Fire one drive tick immediately, outside the interval schedule. */
42
+ readonly tickNow: () => Promise<void>;
43
+ }
44
+ export declare function createOutboxTicker(opts: {
45
+ readonly outbox: Outbox;
46
+ readonly logger: Logger;
47
+ /** Cadence in ms; default 30s. */
48
+ readonly intervalMs?: number;
49
+ }): OutboxTicker;
50
+ //# sourceMappingURL=outbox-ticker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox-ticker.d.ts","sourceRoot":"","sources":["../../src/delivery/outbox-ticker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;wEAEwE;AACxE,eAAO,MAAM,+BAA+B,QAAS,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,YAAY,CA2Cf"}
@@ -0,0 +1,49 @@
1
+ /** Default retry-drive cadence. Deliberately coarser than the alive-ticker's
2
+ * 60s — this is a backstop for the backoff schedule (1s..5min), not a
3
+ * liveness signal; a shorter interval would mostly find nothing due. */
4
+ export const DEFAULT_OUTBOX_TICK_INTERVAL_MS = 30_000;
5
+ export function createOutboxTicker(opts) {
6
+ const { outbox, logger } = opts;
7
+ const intervalMs = opts.intervalMs ?? DEFAULT_OUTBOX_TICK_INTERVAL_MS;
8
+ let timer = null;
9
+ async function tickNow() {
10
+ try {
11
+ const summary = await outbox.driveOnce();
12
+ if (summary.attempted > 0 || summary.deadLettered > 0) {
13
+ logger.info('outbox_drive_tick', {
14
+ attempted: summary.attempted,
15
+ acked: summary.acked,
16
+ failed: summary.failed,
17
+ dead_lettered: summary.deadLettered,
18
+ });
19
+ }
20
+ }
21
+ catch (err) {
22
+ // Best-effort: a driveOnce failure (store I/O error, etc.) must never
23
+ // escape a timer callback and become an uncaughtException — same
24
+ // posture as alive-ticker.ts's tick(). The next tick retries.
25
+ logger.warn('outbox_drive_tick_failed', {
26
+ error: err instanceof Error ? err.message : String(err),
27
+ });
28
+ }
29
+ }
30
+ return {
31
+ start() {
32
+ if (timer !== null)
33
+ return;
34
+ timer = setInterval(() => {
35
+ void tickNow();
36
+ }, intervalMs);
37
+ // Never let the retry-drive interval keep the process alive.
38
+ timer.unref();
39
+ },
40
+ stop() {
41
+ if (timer !== null) {
42
+ clearInterval(timer);
43
+ timer = null;
44
+ }
45
+ },
46
+ tickNow,
47
+ };
48
+ }
49
+ //# sourceMappingURL=outbox-ticker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox-ticker.js","sourceRoot":"","sources":["../../src/delivery/outbox-ticker.ts"],"names":[],"mappings":"AAgCA;;wEAEwE;AACxE,MAAM,CAAC,MAAM,+BAA+B,GAAG,MAAM,CAAC;AAWtD,MAAM,UAAU,kBAAkB,CAAC,IAKlC;IACC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,+BAA+B,CAAC;IACtE,IAAI,KAAK,GAA0C,IAAI,CAAC;IAExD,KAAK,UAAU,OAAO;QACpB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;YACzC,IAAI,OAAO,CAAC,SAAS,GAAG,CAAC,IAAI,OAAO,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;gBACtD,MAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE;oBAC/B,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,aAAa,EAAE,OAAO,CAAC,YAAY;iBACpC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,sEAAsE;YACtE,iEAAiE;YACjE,8DAA8D;YAC9D,MAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE;gBACtC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK;YACH,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO;YAC3B,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;gBACvB,KAAK,OAAO,EAAE,CAAC;YACjB,CAAC,EAAE,UAAU,CAAC,CAAC;YACf,6DAA6D;YAC7D,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,CAAC;QACD,IAAI;YACF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrB,KAAK,GAAG,IAAI,CAAC;YACf,CAAC;QACH,CAAC;QACD,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * DR-038 Decisions 1, 2, 4 — the sender-side effectively-once logic:
3
+ * persist-then-send, stable message-id reuse across retries, exponential-
4
+ * backoff retry bounded by a wall-clock TTL, and dead-letter escalation on
5
+ * TTL expiry.
6
+ *
7
+ * Runtime-agnostic over an injected `OutboxStore` (the pluggable driver
8
+ * interface, hoisted to `@groundnuty/macf-core`'s `delivery/store.ts` —
9
+ * macf#741), an injected `send` transport seam, and an injected clock — so
10
+ * the whole retry loop is deterministically testable without real time or
11
+ * real network I/O, and so a driver swap (in-memory here, VM disk-spool /
12
+ * K8s PVC-broker per DR-008) never touches this file.
13
+ *
14
+ * Retries are driven off the PERSISTED outbox, not an in-memory queue:
15
+ * `driveOnce` re-reads `store.listPending()` on every call. This is the
16
+ * crux of restart-survival (Decision 4) — a fresh `createOutbox()` instance
17
+ * over the SAME store (simulating a sender restart) sees every entry a
18
+ * prior process instance hadn't finished with, and its very first
19
+ * `driveOnce` picks up anything already past its `nextAttemptAt` — which is
20
+ * also what naturally implements the DR-038 "Open Questions" lean ("one
21
+ * immediate attempt on startup... then re-enter backoff") without any
22
+ * startup-specific code path: an overdue entry is simply due right away.
23
+ */
24
+ import type { Logger, OutboxStore, OutboxEntry } from '@groundnuty/macf-core';
25
+ /**
26
+ * Result of one send attempt. `ack: true` means durable-ACK per DR-038
27
+ * Decision 3 (an HTTP 200 the *receiver* only returns after its own
28
+ * persist-then-ACK). Anything else — `ack: false`, or the `send` promise
29
+ * rejecting — is treated uniformly as a delivery failure (timeout, 5xx,
30
+ * connection-refused: the dead-peer case) and the entry stays in the
31
+ * outbox for retry.
32
+ */
33
+ export interface OutboxSendResult {
34
+ readonly ack: boolean;
35
+ }
36
+ /**
37
+ * The injected transport seam. A real implementation dispatches the actual
38
+ * `message/send` (or legacy notify_peer POST) call; this module never
39
+ * touches the network directly — that keeps the effectively-once logic
40
+ * testable with a fake and reusable across transports.
41
+ */
42
+ export type OutboxSendFn = (target: string, id: string, payload: unknown) => Promise<OutboxSendResult>;
43
+ /** Default exponential-backoff base (DR-038 Decision 4: "~1s → … → 5min"). */
44
+ export declare const DEFAULT_BACKOFF_BASE_MS = 1000;
45
+ /** Default exponential-backoff cap (DR-038 Decision 4: "~1s → … → 5min"). */
46
+ export declare const DEFAULT_BACKOFF_CAP_MS: number;
47
+ /**
48
+ * Default wall-clock TTL bounding the retry budget. DR-038 Decision 4
49
+ * proposes 24h ("the 8h outage fits comfortably; a multi-day VM outage
50
+ * would dead-letter, correctly escalating to GitHub" — Open Questions).
51
+ */
52
+ export declare const DEFAULT_TTL_MS: number;
53
+ /**
54
+ * `baseMs * 2^attemptCount`, capped at `capMs`. `attemptCount` is the number
55
+ * of PRIOR failed attempts (0 on the first failure → backoff = `baseMs`).
56
+ * A wall-clock TTL bounds the retry budget (see `DEFAULT_TTL_MS`), NOT an
57
+ * attempt-count ceiling — an attempt count is meaningless across restart
58
+ * gaps of varying length (DR-038 Decision 4).
59
+ */
60
+ export declare function computeBackoffMs(attemptCount: number, baseMs?: number, capMs?: number): number;
61
+ export interface OutboxDeps {
62
+ readonly store: OutboxStore;
63
+ readonly send: OutboxSendFn;
64
+ /** Injectable epoch-ms clock (testing). Default `Date.now`. */
65
+ readonly now?: () => number;
66
+ /**
67
+ * Invoked once per TTL-expired entry, AFTER `store.deadLetter(id)` has
68
+ * already run. This is the decision-layer escalation hook DR-038
69
+ * Decision 4 requires ("a `gh issue` to the operator/reporter... a
70
+ * decision-layer action... NOT a store-driver method") — this slice does
71
+ * NOT implement the GitHub call itself; a later wiring slice supplies a
72
+ * real callback. A throw from this callback is caught + logged, never
73
+ * allowed to abort the rest of the drive tick.
74
+ */
75
+ readonly onDeadLetter?: (entry: OutboxEntry) => void | Promise<void>;
76
+ readonly logger?: Logger;
77
+ readonly backoffBaseMs?: number;
78
+ readonly backoffCapMs?: number;
79
+ readonly ttlMs?: number;
80
+ }
81
+ /** Per-tick counters, mainly for test assertions + operator diagnostics. */
82
+ export interface OutboxDriveSummary {
83
+ readonly attempted: number;
84
+ readonly acked: number;
85
+ readonly failed: number;
86
+ readonly deadLettered: number;
87
+ }
88
+ export interface Outbox {
89
+ /**
90
+ * Persist a NEW message BEFORE any send attempt (Decision 1: persist-
91
+ * then-send). Mints a fresh message-id (Decision 2) and returns it.
92
+ */
93
+ readonly enqueue: (target: string, payload: unknown) => Promise<string>;
94
+ /**
95
+ * Drive one retry tick: for every pending entry whose `nextAttemptAt <=
96
+ * now`, either dead-letter it (TTL expired — checked BEFORE attempting
97
+ * another send) or attempt a send (ack → `markAcked`; failure →
98
+ * `reschedule` with the next backoff). Callable directly by tests with an
99
+ * explicit `now`, or wrapped in an operator-driven loop with injected
100
+ * sleep between ticks.
101
+ */
102
+ readonly driveOnce: (now?: number) => Promise<OutboxDriveSummary>;
103
+ }
104
+ export declare function createOutbox(deps: OutboxDeps): Outbox;
105
+ //# sourceMappingURL=outbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.d.ts","sourceRoot":"","sources":["../../src/delivery/outbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAG9E;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/B,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAE7C,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAsB,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,MAAgC,EACxC,KAAK,GAAE,MAA+B,GACrC,MAAM,CAGR;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,MAAM;IACrB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxE;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACnE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAmFrD"}