stitchkit 0.99.0 → 0.100.1

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 (147) hide show
  1. package/CHANGELOG.md +124 -1
  2. package/dist/agent-runtime/contained-darwin.d.ts +0 -17
  3. package/dist/agent-runtime/contained-darwin.d.ts.map +1 -1
  4. package/dist/agent-runtime/contained-write.d.ts.map +1 -1
  5. package/dist/agent-runtime/invocation-attempt.d.ts +28 -0
  6. package/dist/agent-runtime/invocation-attempt.d.ts.map +1 -0
  7. package/dist/agent-runtime/invocation-failure.d.ts +6 -0
  8. package/dist/agent-runtime/invocation-failure.d.ts.map +1 -0
  9. package/dist/agent-runtime/invocation-payload.d.ts +20 -0
  10. package/dist/agent-runtime/invocation-payload.d.ts.map +1 -0
  11. package/dist/agent-runtime/invocation-read.d.ts +131 -0
  12. package/dist/agent-runtime/invocation-read.d.ts.map +1 -0
  13. package/dist/agent-runtime/invocation-schema.d.ts +378 -0
  14. package/dist/agent-runtime/invocation-schema.d.ts.map +1 -0
  15. package/dist/agent-runtime/invocations.d.ts +52 -0
  16. package/dist/agent-runtime/invocations.d.ts.map +1 -0
  17. package/dist/agent-runtime/projections.d.ts.map +1 -1
  18. package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
  19. package/dist/agent-runtime/run-execution-state.d.ts +4 -2
  20. package/dist/agent-runtime/run-execution-state.d.ts.map +1 -1
  21. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  22. package/dist/agent-runtime/run-failure.d.ts.map +1 -1
  23. package/dist/agent-runtime/run-model-attempts.d.ts.map +1 -1
  24. package/dist/agent-runtime/run-operation-lifecycle.d.ts +2 -2
  25. package/dist/agent-runtime/run-operation-lifecycle.d.ts.map +1 -1
  26. package/dist/agent-runtime/run-provider-ledger.d.ts +1 -0
  27. package/dist/agent-runtime/run-provider-ledger.d.ts.map +1 -1
  28. package/dist/agent-runtime/runtime.d.ts +11 -0
  29. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  30. package/dist/agent-runtime/schedule-dispatch.d.ts +9 -2
  31. package/dist/agent-runtime/schedule-dispatch.d.ts.map +1 -1
  32. package/dist/agent-runtime/schedule-records.d.ts +8 -2
  33. package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
  34. package/dist/agent-runtime/schedules.d.ts +5 -0
  35. package/dist/agent-runtime/schedules.d.ts.map +1 -1
  36. package/dist/agent-runtime/sqlite-events.d.ts.map +1 -1
  37. package/dist/agent-runtime/store-create.d.ts.map +1 -1
  38. package/dist/agent-runtime/store-driver-contract.d.ts +9 -0
  39. package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
  40. package/dist/agent-runtime/store-event-once.d.ts +37 -0
  41. package/dist/agent-runtime/store-event-once.d.ts.map +1 -0
  42. package/dist/agent-runtime/store-memory.d.ts.map +1 -1
  43. package/dist/agent-runtime/store.d.ts +11 -0
  44. package/dist/agent-runtime/store.d.ts.map +1 -1
  45. package/dist/application/change-subscription.d.ts +36 -0
  46. package/dist/application/change-subscription.d.ts.map +1 -0
  47. package/dist/application/diagnostic-journal-contract.d.ts +3 -6
  48. package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
  49. package/dist/application/notification-delivery.d.ts +55 -0
  50. package/dist/application/notification-delivery.d.ts.map +1 -0
  51. package/dist/application/notification-outbox-state.d.ts +41 -0
  52. package/dist/application/notification-outbox-state.d.ts.map +1 -0
  53. package/dist/application/notification-outbox.d.ts +7 -1
  54. package/dist/application/notification-outbox.d.ts.map +1 -1
  55. package/dist/application/watch-hub-core.d.ts +14 -84
  56. package/dist/application/watch-hub-core.d.ts.map +1 -1
  57. package/dist/application/watch-hub-source.d.ts +98 -0
  58. package/dist/application/watch-hub-source.d.ts.map +1 -0
  59. package/dist/application/watch-hub.d.ts +10 -2
  60. package/dist/application/watch-hub.d.ts.map +1 -1
  61. package/dist/browser/session-credentials.d.ts +26 -0
  62. package/dist/browser/session-credentials.d.ts.map +1 -0
  63. package/dist/browser/session.d.ts +33 -0
  64. package/dist/browser/session.d.ts.map +1 -0
  65. package/dist/durability/events.d.ts +6 -0
  66. package/dist/durability/events.d.ts.map +1 -1
  67. package/dist/entrypoints/agent-runtime/coding-tools.js +3 -2
  68. package/dist/entrypoints/agent-runtime/harness.js +5 -3
  69. package/dist/entrypoints/agent-runtime/sandbox.js +3 -2
  70. package/dist/entrypoints/agent-runtime/sqlite/bun.js +3 -3
  71. package/dist/entrypoints/agent-runtime/sqlite/node.js +3 -3
  72. package/dist/entrypoints/agent-runtime/testing.js +3 -3
  73. package/dist/entrypoints/agent-runtime.d.ts +3 -0
  74. package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
  75. package/dist/entrypoints/agent-runtime.js +65 -20
  76. package/dist/entrypoints/application/diagnostic-journal.js +2 -1
  77. package/dist/entrypoints/application.d.ts +3 -1
  78. package/dist/entrypoints/application.d.ts.map +1 -1
  79. package/dist/entrypoints/application.js +820 -294
  80. package/dist/entrypoints/files.js +9 -2
  81. package/dist/entrypoints/index.d.ts +2 -0
  82. package/dist/entrypoints/index.d.ts.map +1 -1
  83. package/dist/entrypoints/index.js +199 -0
  84. package/dist/entrypoints/live.js +78 -19
  85. package/dist/entrypoints/node.js +1 -1
  86. package/dist/entrypoints/react.d.ts +1 -1
  87. package/dist/entrypoints/react.d.ts.map +1 -1
  88. package/dist/entrypoints/react.js +59 -16
  89. package/dist/entrypoints/server.js +4 -4
  90. package/dist/{index-sp1tzwnb.js → index-10x2hbx0.js} +5 -2
  91. package/dist/{index-8p52kd5p.js → index-c6bxbsnc.js} +100 -14
  92. package/dist/{index-gxrt5h6v.js → index-cyktc4bp.js} +3 -0
  93. package/dist/{index-1m2thnq0.js → index-eaysmdtq.js} +11 -2
  94. package/dist/{index-jhx828hc.js → index-f4fx2tza.js} +948 -279
  95. package/dist/{index-px2gm2jp.js → index-fhwqwxe0.js} +17 -55
  96. package/dist/{index-e4hkgm5t.js → index-gw8bmyaj.js} +4 -2
  97. package/dist/{index-p3vsv1fb.js → index-hadmp193.js} +14 -5
  98. package/dist/{index-wt743f7f.js → index-k6gpggb9.js} +2 -1
  99. package/dist/index-q3pyb9n0.js +22 -0
  100. package/dist/{index-wsm5w4ee.js → index-vqr7f7v8.js} +51 -3
  101. package/dist/index-zjjyynm3.js +42 -0
  102. package/dist/internal/coalesced-task.d.ts +17 -0
  103. package/dist/internal/coalesced-task.d.ts.map +1 -0
  104. package/dist/internal/darwin-binding.d.ts +19 -0
  105. package/dist/internal/darwin-binding.d.ts.map +1 -0
  106. package/dist/internal/exclusive-lock.d.ts +20 -7
  107. package/dist/internal/exclusive-lock.d.ts.map +1 -1
  108. package/dist/internal/mutation-queue.d.ts +4 -0
  109. package/dist/internal/mutation-queue.d.ts.map +1 -0
  110. package/dist/internal/process-identity.d.ts +1 -1
  111. package/dist/internal/process-identity.d.ts.map +1 -1
  112. package/dist/internal/process-instance.d.ts +26 -0
  113. package/dist/internal/process-instance.d.ts.map +1 -0
  114. package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
  115. package/dist/live/watch-client-state.d.ts +71 -0
  116. package/dist/live/watch-client-state.d.ts.map +1 -0
  117. package/dist/live/watch-client.d.ts +5 -1
  118. package/dist/live/watch-client.d.ts.map +1 -1
  119. package/dist/live/watch-contract.d.ts +12 -0
  120. package/dist/live/watch-contract.d.ts.map +1 -1
  121. package/dist/react/cache-bridge.d.ts +10 -0
  122. package/dist/react/cache-bridge.d.ts.map +1 -1
  123. package/dist/server/realtime.d.ts.map +1 -1
  124. package/llms/agent-runtime.2.txt +102 -0
  125. package/llms/agent-runtime.3.txt +30 -1
  126. package/llms/agent-runtime.txt +8 -3
  127. package/llms/application.2.txt +84 -2
  128. package/llms/application.txt +32 -2
  129. package/llms/files.txt +1 -1
  130. package/llms/live.txt +8 -0
  131. package/llms/react.txt +8 -0
  132. package/llms/server.3.txt +57 -0
  133. package/llms/stitchkit.2.txt +41 -0
  134. package/llms/stitchkit.3.txt +26 -0
  135. package/llms/stitchkit.txt +3 -3
  136. package/llms/upgrading-0.37.0.txt +176 -0
  137. package/llms/{upgrading-0.37.0-0.58.0.txt → upgrading-0.44.0-0.60.0.txt} +229 -169
  138. package/llms/{upgrading-0.59.0-0.71.0.txt → upgrading-0.61.0-0.73.0.txt} +122 -229
  139. package/llms/{upgrading-0.72.0-0.99.0.txt → upgrading-0.74.0-0.100.0.txt} +80 -123
  140. package/llms/upgrading.txt +4 -3
  141. package/llms-full.txt +471 -11
  142. package/llms.txt +10 -9
  143. package/native/darwin-arm64.node +0 -0
  144. package/native/darwin-x64.node +0 -0
  145. package/package.json +1 -1
  146. package/dist/agent-runtime/run-mutation-queue.d.ts +0 -21
  147. package/dist/agent-runtime/run-mutation-queue.d.ts.map +0 -1
@@ -58,9 +58,6 @@ import {
58
58
  defineManagedResource,
59
59
  managedResourceDependencyId
60
60
  } from "../index-2k4yrqkc.js";
61
- import {
62
- withDeadline
63
- } from "../index-b9aefrqd.js";
64
61
  import"../index-3z73fh2c.js";
65
62
  import {
66
63
  createBoundedSinkManager
@@ -69,11 +66,14 @@ import {
69
66
  backoffDelay,
70
67
  createBackoff
71
68
  } from "../index-ngfhvfpd.js";
69
+ import {
70
+ withDeadline
71
+ } from "../index-b9aefrqd.js";
72
72
  import {
73
73
  deltaWins,
74
74
  diff,
75
75
  watchKeyString
76
- } from "../index-wt743f7f.js";
76
+ } from "../index-k6gpggb9.js";
77
77
  import"../index-1j8pm5t0.js";
78
78
  import {
79
79
  BoundedChannelPolicySchema,
@@ -90,7 +90,9 @@ import {
90
90
  import {
91
91
  argumentsDigest
92
92
  } from "../index-0b1ess5e.js";
93
- import"../index-2vtcmrj9.js";
93
+ import {
94
+ AppError
95
+ } from "../index-2vtcmrj9.js";
94
96
  import {
95
97
  serializeCanonicalJson
96
98
  } from "../index-k5hsfc3j.js";
@@ -483,12 +485,319 @@ function createBoundedAdmission(config) {
483
485
  getSnapshot
484
486
  };
485
487
  }
486
- // src/internal/decision.ts
488
+ // src/application/change-subscription.ts
487
489
  import { z as z2 } from "zod";
488
- var PolicyDecisionSchema = z2.discriminatedUnion("outcome", [
489
- z2.object({ outcome: z2.literal("allow") }).strict(),
490
- z2.object({ outcome: z2.literal("deny"), reason: z2.string().min(1) }).strict(),
491
- z2.object({ outcome: z2.literal("defer") }).strict()
490
+
491
+ // src/internal/coalesced-task.ts
492
+ function createCoalescedTask(config) {
493
+ const backoff = createBackoff(config.backoff);
494
+ let running = false;
495
+ let dirty = false;
496
+ let closed = false;
497
+ let retry;
498
+ const pauseRetry = () => {
499
+ clearTimeout(retry);
500
+ retry = undefined;
501
+ };
502
+ const pump = async () => {
503
+ if (running || closed || !config.active() || retry)
504
+ return;
505
+ running = true;
506
+ try {
507
+ while (dirty && !closed && config.active()) {
508
+ dirty = false;
509
+ try {
510
+ await config.run();
511
+ backoff.reset();
512
+ } catch (error) {
513
+ dirty = true;
514
+ if (!closed && config.active()) {
515
+ retry = setTimeout(() => {
516
+ retry = undefined;
517
+ pump();
518
+ }, backoff.next());
519
+ retry.unref?.();
520
+ config.onError(error);
521
+ }
522
+ return;
523
+ }
524
+ }
525
+ } finally {
526
+ running = false;
527
+ }
528
+ };
529
+ return {
530
+ get running() {
531
+ return running;
532
+ },
533
+ get pending() {
534
+ return dirty;
535
+ },
536
+ trigger() {
537
+ if (closed)
538
+ return;
539
+ dirty = true;
540
+ pump();
541
+ },
542
+ pauseRetry,
543
+ close() {
544
+ closed = true;
545
+ dirty = false;
546
+ pauseRetry();
547
+ }
548
+ };
549
+ }
550
+
551
+ // src/application/change-subscription.ts
552
+ class ChangeSubscription {
553
+ config;
554
+ keys;
555
+ interval;
556
+ timeout;
557
+ backoff;
558
+ tasks;
559
+ failedKeys = new Set;
560
+ lifetime;
561
+ attempt;
562
+ connection;
563
+ opening;
564
+ closing;
565
+ retry;
566
+ periodic;
567
+ context;
568
+ removeAbort;
569
+ constructor(config) {
570
+ this.config = config;
571
+ this.keys = z2.array(z2.string().min(1).max(256)).min(1).max(128).parse(config.keys);
572
+ if (new Set(this.keys).size !== this.keys.length)
573
+ throw new Error("Subscription keys must be unique");
574
+ this.interval = z2.number().int().min(10).max(3600000).parse(config.reconcileIntervalMs);
575
+ this.timeout = z2.number().int().min(1).max(3600000).parse(config.timeoutMs ?? 30000);
576
+ const policy = config.backoff ?? { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
577
+ this.backoff = createBackoff(policy);
578
+ this.tasks = new Map(this.keys.map((key) => [
579
+ key,
580
+ createCoalescedTask({
581
+ active: () => this.connection !== undefined && this.attempt?.signal.aborted === false,
582
+ run: () => this.read(key),
583
+ onError: (error) => {
584
+ this.failedKeys.add(key);
585
+ this.report(error);
586
+ },
587
+ backoff: policy
588
+ })
589
+ ]));
590
+ }
591
+ report(error) {
592
+ this.context?.reportHealth("degraded");
593
+ try {
594
+ this.config.onError(error);
595
+ } catch {}
596
+ }
597
+ async read(key) {
598
+ const connection = this.connection;
599
+ const attempt = this.attempt;
600
+ if (!connection || !attempt)
601
+ return;
602
+ const readAbort = new AbortController;
603
+ const signal = AbortSignal.any([attempt.signal, readAbort.signal]);
604
+ const work = this.config.reconcile(key, {
605
+ signal,
606
+ commit: (publish) => {
607
+ if (signal.aborted || this.connection !== connection)
608
+ return false;
609
+ publish();
610
+ return true;
611
+ }
612
+ });
613
+ const outcome = await withDeadline(work, this.timeout);
614
+ if (!outcome.settled) {
615
+ readAbort.abort();
616
+ this.report(new Error("Reconciliation deadline exceeded"));
617
+ await work;
618
+ throw new Error("Reconciliation deadline exceeded");
619
+ }
620
+ if (!signal.aborted && this.connection === connection) {
621
+ this.failedKeys.delete(key);
622
+ if (this.failedKeys.size === 0)
623
+ this.context?.reportHealth("healthy");
624
+ }
625
+ }
626
+ reconcile() {
627
+ for (const task of this.tasks.values())
628
+ task.trigger();
629
+ }
630
+ schedule() {
631
+ if (!this.lifetime || this.lifetime.signal.aborted || this.retry)
632
+ return;
633
+ this.retry = setTimeout(() => {
634
+ this.retry = undefined;
635
+ this.open();
636
+ }, this.backoff.next());
637
+ this.retry.unref?.();
638
+ }
639
+ async release(handle) {
640
+ const work = Promise.resolve().then(() => handle.close());
641
+ this.closing = work;
642
+ try {
643
+ const outcome = await withDeadline(work, this.timeout);
644
+ if (!outcome.settled) {
645
+ this.report(new Error("Subscription close deadline exceeded"));
646
+ await work;
647
+ }
648
+ } finally {
649
+ if (this.closing === work)
650
+ this.closing = undefined;
651
+ }
652
+ }
653
+ lost(handle, error) {
654
+ if (this.connection !== handle)
655
+ return;
656
+ this.connection = undefined;
657
+ this.attempt?.abort();
658
+ this.report(error);
659
+ this.release(handle).catch((cause) => this.report(cause)).then(() => this.schedule());
660
+ }
661
+ open() {
662
+ if (this.opening)
663
+ return this.opening;
664
+ const lifetime = this.lifetime;
665
+ if (!lifetime || lifetime.signal.aborted || this.closing)
666
+ return Promise.resolve();
667
+ this.opening = this.connect(lifetime).finally(() => {
668
+ this.opening = undefined;
669
+ });
670
+ return this.opening;
671
+ }
672
+ async connect(lifetime) {
673
+ const attempt = new AbortController;
674
+ this.attempt = attempt;
675
+ const onAbort = () => attempt.abort();
676
+ lifetime.signal.addEventListener("abort", onAbort, { once: true });
677
+ let handle;
678
+ let lost = false;
679
+ const work = (async () => {
680
+ handle = await this.config.connect(attempt.signal);
681
+ if (attempt.signal.aborted)
682
+ return;
683
+ const owned = handle;
684
+ await owned.subscribe({
685
+ signal: attempt.signal,
686
+ hint: (key) => {
687
+ if (attempt.signal.aborted || lost)
688
+ return;
689
+ const task = this.tasks.get(key);
690
+ if (task)
691
+ task.trigger();
692
+ else
693
+ this.report(new Error("Unknown subscription key"));
694
+ },
695
+ disconnected: (error) => {
696
+ if (attempt.signal.aborted || this.lifetime !== lifetime)
697
+ return;
698
+ lost = true;
699
+ if (this.connection === owned)
700
+ this.lost(owned, error);
701
+ else
702
+ this.report(error);
703
+ }
704
+ });
705
+ })();
706
+ try {
707
+ const outcome = await withDeadline(work, this.timeout);
708
+ if (!outcome.settled) {
709
+ attempt.abort();
710
+ this.report(new Error("Subscription connect deadline exceeded"));
711
+ await work;
712
+ }
713
+ if (attempt.signal.aborted || lifetime !== this.lifetime || lost) {
714
+ if (handle)
715
+ await this.release(handle);
716
+ this.schedule();
717
+ return;
718
+ }
719
+ this.connection = handle;
720
+ this.backoff.reset();
721
+ this.context?.reportHealth("healthy");
722
+ this.reconcile();
723
+ } catch (error) {
724
+ attempt.abort();
725
+ if (handle) {
726
+ try {
727
+ await this.release(handle);
728
+ } catch (cause) {
729
+ this.report(cause);
730
+ }
731
+ }
732
+ if (!lifetime.signal.aborted) {
733
+ this.report(error);
734
+ this.schedule();
735
+ }
736
+ } finally {
737
+ lifetime.signal.removeEventListener("abort", onAbort);
738
+ }
739
+ }
740
+ start(context) {
741
+ if (this.lifetime && !this.lifetime.signal.aborted)
742
+ throw new Error("Subscription already started");
743
+ if (this.opening || this.closing || [...this.tasks.values()].some((task) => task.running))
744
+ throw new Error("Previous subscription work has not settled");
745
+ this.context = context;
746
+ this.lifetime = new AbortController;
747
+ const abort = () => {
748
+ this.close();
749
+ };
750
+ context.signal.addEventListener("abort", abort, { once: true });
751
+ this.removeAbort = () => context.signal.removeEventListener("abort", abort);
752
+ if (context.signal.aborted) {
753
+ this.close();
754
+ return {};
755
+ }
756
+ this.periodic = setInterval(() => this.reconcile(), this.interval);
757
+ this.periodic.unref?.();
758
+ const ready = (async () => {
759
+ const outcome = await withDeadline(this.open(), this.timeout);
760
+ if (!outcome.settled || !this.connection) {
761
+ this.close().catch((error) => this.report(error));
762
+ throw new Error("Subscription did not become ready before its deadline");
763
+ }
764
+ })();
765
+ return { ready };
766
+ }
767
+ async close() {
768
+ this.lifetime?.abort();
769
+ this.attempt?.abort();
770
+ this.removeAbort?.();
771
+ clearTimeout(this.retry);
772
+ this.retry = undefined;
773
+ clearInterval(this.periodic);
774
+ this.periodic = undefined;
775
+ for (const task of this.tasks.values())
776
+ task.pauseRetry();
777
+ const handle = this.connection;
778
+ this.connection = undefined;
779
+ if (handle) {
780
+ const outcome = await withDeadline(this.release(handle), this.timeout);
781
+ if (!outcome.settled)
782
+ this.report(new Error("Subscription shutdown deadline exceeded"));
783
+ }
784
+ }
785
+ }
786
+ function changeSubscriptionResource(config) {
787
+ const subscription = new ChangeSubscription(config);
788
+ return defineManagedResource({
789
+ id: config.id,
790
+ start: (context) => subscription.start(context),
791
+ close: () => subscription.close(),
792
+ force: () => subscription.close()
793
+ });
794
+ }
795
+ // src/internal/decision.ts
796
+ import { z as z3 } from "zod";
797
+ var PolicyDecisionSchema = z3.discriminatedUnion("outcome", [
798
+ z3.object({ outcome: z3.literal("allow") }).strict(),
799
+ z3.object({ outcome: z3.literal("deny"), reason: z3.string().min(1) }).strict(),
800
+ z3.object({ outcome: z3.literal("defer") }).strict()
492
801
  ]);
493
802
 
494
803
  // src/application/decisions.ts
@@ -562,17 +871,17 @@ function createDecisionPipeline(policies, config) {
562
871
  };
563
872
  }
564
873
  // src/application/events.ts
565
- import { z as z3 } from "zod";
566
- var ApplicationLifecycleEventSchema = z3.object({
567
- type: z3.literal("application-state"),
874
+ import { z as z4 } from "zod";
875
+ var ApplicationLifecycleEventSchema = z4.object({
876
+ type: z4.literal("application-state"),
568
877
  applicationId: ApplicationIdSchema,
569
- epoch: z3.string().uuid(),
570
- revision: z3.number().int().nonnegative(),
878
+ epoch: z4.string().uuid(),
879
+ revision: z4.number().int().nonnegative(),
571
880
  lifecycle: ApplicationLifecycleSchema,
572
881
  health: ApplicationHealthSchema,
573
- ready: z3.boolean(),
574
- capturedAt: z3.string().datetime({ offset: true }),
575
- resources: z3.array(ManagedResourceSnapshotSchema).readonly()
882
+ ready: z4.boolean(),
883
+ capturedAt: z4.string().datetime({ offset: true }),
884
+ resources: z4.array(ManagedResourceSnapshotSchema).readonly()
576
885
  }).strict().readonly();
577
886
  function applicationLifecycleEvent(snapshot) {
578
887
  return ApplicationLifecycleEventSchema.parse({
@@ -603,13 +912,13 @@ function createApplicationEventSink(config) {
603
912
  };
604
913
  }
605
914
  // src/application/health.ts
606
- import { z as z4 } from "zod";
607
- var ApplicationHealthHandlerOptionsSchema = z4.object({
608
- kind: z4.enum(["liveness", "readiness"]),
609
- retryAfterSeconds: z4.number().int().nonnegative().default(5)
915
+ import { z as z5 } from "zod";
916
+ var ApplicationHealthHandlerOptionsSchema = z5.object({
917
+ kind: z5.enum(["liveness", "readiness"]),
918
+ retryAfterSeconds: z5.number().int().nonnegative().default(5)
610
919
  });
611
- var ApplicationOperationalHandlersOptionsSchema = z4.object({
612
- retryAfterSeconds: z4.number().int().nonnegative().default(5)
920
+ var ApplicationOperationalHandlersOptionsSchema = z5.object({
921
+ retryAfterSeconds: z5.number().int().nonnegative().default(5)
613
922
  });
614
923
  function createApplicationHealthHandler(application, options) {
615
924
  const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
@@ -804,43 +1113,33 @@ function sqliteKeyspaceBackend(declaration, config) {
804
1113
  }
805
1114
  };
806
1115
  }
807
- // src/application/notification-outbox.ts
808
- import { z as z5 } from "zod";
809
- function notificationOutboxStateSchema(payload) {
810
- return z5.object({
811
- schemaVersion: z5.literal(1),
812
- queue: z5.array(z5.object({
813
- key: z5.string().min(1).max(512),
814
- payload,
815
- createdAt: z5.string().datetime({ offset: true }),
816
- attempts: z5.number().int().nonnegative(),
817
- nextAttemptAt: z5.string().datetime({ offset: true }),
818
- leaseOwner: z5.string().min(1).max(128).nullable(),
819
- leaseId: z5.string().min(1).max(128).nullable(),
820
- leaseUntil: z5.string().datetime({ offset: true }).nullable()
821
- }).strict()),
822
- receipts: z5.array(z5.object({
823
- key: z5.string().min(1).max(512),
824
- completedAt: z5.string().datetime({ offset: true })
825
- }).strict())
826
- }).strict();
827
- }
828
- var emptyOutboxState = () => ({
829
- schemaVersion: 1,
830
- queue: [],
831
- receipts: []
832
- });
1116
+ // src/application/notification-delivery.ts
1117
+ import { z as z7 } from "zod";
1118
+
1119
+ // src/application/notification-outbox-state.ts
1120
+ import { z as z6 } from "zod";
833
1121
  var sizeOf = (value) => measureSize(value).responseBytes;
834
- var DEFAULT_OUTBOX_BACKOFF = { minDelayMs: 1000, maxDelayMs: 60000, jitter: 0 };
1122
+ var DEFAULT_OUTBOX_BACKOFF = {
1123
+ minDelayMs: 1000,
1124
+ maxDelayMs: 60000,
1125
+ jitter: 0
1126
+ };
835
1127
  function resolveOutboxLimits(config) {
836
- const pollIntervalMs = z5.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
837
- const leaseMs = z5.number().int().min(100).parse(config.leaseMs ?? 30000);
838
- const maxAttempts = z5.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
839
- const maxQueue = z5.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
840
- const maxStateBytes = z5.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
841
- const retainReceipts = z5.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
1128
+ const pollIntervalMs = z6.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
1129
+ const leaseMs = z6.number().int().min(100).parse(config.leaseMs ?? 30000);
1130
+ const maxAttempts = z6.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
1131
+ const maxQueue = z6.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
1132
+ const maxStateBytes = z6.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
1133
+ const retainReceipts = z6.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
842
1134
  return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
843
1135
  }
1136
+
1137
+ class OutboxStateLimitError extends Error {
1138
+ constructor(maxStateBytes) {
1139
+ super(`[stitchkit] notification outbox state limit (${maxStateBytes} bytes) exceeded`);
1140
+ this.name = "OutboxStateLimitError";
1141
+ }
1142
+ }
844
1143
  function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
845
1144
  if (parsed.queue.length > maxQueue) {
846
1145
  throw new Error(`[stitchkit] notification outbox queue limit (${maxQueue}) exceeded`);
@@ -851,9 +1150,8 @@ function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
851
1150
  receipts = receipts.slice(0, -1);
852
1151
  next = { ...parsed, receipts };
853
1152
  }
854
- if (sizeOf(next) > maxStateBytes) {
855
- throw new Error(`[stitchkit] notification outbox state limit (${maxStateBytes} bytes) exceeded`);
856
- }
1153
+ if (sizeOf(next) > maxStateBytes)
1154
+ throw new OutboxStateLimitError(maxStateBytes);
857
1155
  return next;
858
1156
  }
859
1157
  function dropReason(classification, attempts, maxAttempts) {
@@ -863,7 +1161,7 @@ function dropReason(classification, attempts, maxAttempts) {
863
1161
  return "terminal";
864
1162
  return attempts >= maxAttempts ? "attempt-limit" : null;
865
1163
  }
866
- var RetryDelaySchema = z5.number().nonnegative().max(24 * 60 * 60 * 1000);
1164
+ var RetryDelaySchema = z6.number().nonnegative().max(24 * 60 * 60 * 1000);
867
1165
  var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
868
1166
  function claimDue(queue, now, ownerId, leaseMs) {
869
1167
  const candidate = [...queue].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)).find((item) => Date.parse(item.nextAttemptAt) <= now.getTime() && (item.leaseUntil === null || Date.parse(item.leaseUntil) <= now.getTime()));
@@ -877,16 +1175,196 @@ function claimDue(queue, now, ownerId, leaseMs) {
877
1175
  leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
878
1176
  };
879
1177
  }
1178
+
1179
+ // src/application/notification-delivery.ts
1180
+ var NotificationDeliveryStateSchema = z7.object({
1181
+ version: z7.string().min(1).max(128),
1182
+ actions: z7.array(z7.string().min(1).max(128)).min(1).max(128),
1183
+ receipts: z7.array(z7.object({
1184
+ action: z7.string(),
1185
+ receipt: z7.json(),
1186
+ unrecorded: z7.enum(["not-json", "too-large"]).optional()
1187
+ }).strict()).max(128),
1188
+ projectedAt: z7.string().datetime({ offset: true }).optional()
1189
+ }).strict().refine((value) => new Set(value.actions).size === value.actions.length && new Set(value.receipts.map((receipt) => receipt.action)).size === value.receipts.length && value.receipts.every((receipt) => value.actions.includes(receipt.action)), "Invalid action plan or receipts");
1190
+
1191
+ class NotificationPlanVersionError extends Error {
1192
+ version;
1193
+ constructor(version) {
1194
+ super(`Notification action plan version "${version}" has no executor`);
1195
+ this.name = "NotificationPlanVersionError";
1196
+ this.version = version;
1197
+ }
1198
+ }
1199
+ function projectionFor(config, version) {
1200
+ if (version === config.version)
1201
+ return config.project;
1202
+ return config.retiredVersions && Object.hasOwn(config.retiredVersions, version) ? config.retiredVersions[version] : undefined;
1203
+ }
1204
+ function recordable(result) {
1205
+ const parsed = z7.json().safeParse(result ?? null);
1206
+ return parsed.success ? { receipt: parsed.data } : { receipt: null, unrecorded: "not-json" };
1207
+ }
1208
+ function notificationDeliveryPlan(config, payload) {
1209
+ return NotificationDeliveryStateSchema.parse({
1210
+ version: config.version,
1211
+ actions: [...config.actions(payload)],
1212
+ receipts: []
1213
+ });
1214
+ }
1215
+ async function deliverNotification(input) {
1216
+ const { item, config } = input;
1217
+ const plan = item.delivery;
1218
+ if (!plan)
1219
+ throw new Error("Notification has no action plan");
1220
+ const project = projectionFor(config, plan.version);
1221
+ if (!project)
1222
+ throw new NotificationPlanVersionError(plan.version);
1223
+ const owns = (current2) => current2.key === item.key && current2.leaseId === item.leaseId;
1224
+ const checkpoint = async (change) => input.store.update((state) => {
1225
+ if (!state)
1226
+ throw new Error("Outbox state disappeared");
1227
+ const live2 = state.queue.find(owns);
1228
+ if (!live2?.delivery)
1229
+ return { state, result: false };
1230
+ const delivery = NotificationDeliveryStateSchema.parse(change(live2.delivery));
1231
+ return {
1232
+ state: input.bounded({
1233
+ ...state,
1234
+ queue: state.queue.map((row) => row === live2 ? {
1235
+ ...row,
1236
+ delivery,
1237
+ leaseUntil: new Date(input.clock().getTime() + input.leaseMs).toISOString()
1238
+ } : row)
1239
+ }),
1240
+ result: true
1241
+ };
1242
+ });
1243
+ const notification = { key: item.key, payload: item.payload, attempt: item.attempts };
1244
+ for (const action of plan.actions) {
1245
+ if (!input.active())
1246
+ return false;
1247
+ if (plan.receipts.some((receipt) => receipt.action === action))
1248
+ continue;
1249
+ if (!await checkpoint((value) => value))
1250
+ return false;
1251
+ const result = await input.send({
1252
+ ...notification,
1253
+ action,
1254
+ idempotencyKey: JSON.stringify([item.key, plan.version, action])
1255
+ });
1256
+ const entry = recordable(result);
1257
+ const confirm = (receipt) => checkpoint((value) => ({ ...value, receipts: [...value.receipts, receipt] }));
1258
+ let confirmed;
1259
+ let dropped;
1260
+ try {
1261
+ confirmed = await confirm({ action, ...entry });
1262
+ } catch (error) {
1263
+ if (entry.unrecorded || !(error instanceof OutboxStateLimitError))
1264
+ throw error;
1265
+ dropped = error;
1266
+ confirmed = await confirm({ action, receipt: null, unrecorded: "too-large" });
1267
+ }
1268
+ if (dropped)
1269
+ await input.report(dropped);
1270
+ if (entry.unrecorded) {
1271
+ await input.report(new Error(`Notification action "${action}" returned a receipt that is not JSON`));
1272
+ }
1273
+ if (!confirmed)
1274
+ return false;
1275
+ }
1276
+ if (!input.active())
1277
+ return false;
1278
+ const current = await input.store.read();
1279
+ const live = current?.queue.find(owns);
1280
+ if (!live?.delivery)
1281
+ return false;
1282
+ if (live.delivery.projectedAt === undefined) {
1283
+ await project(notification, live.delivery.receipts);
1284
+ if (!await checkpoint((value) => ({ ...value, projectedAt: input.clock().toISOString() })))
1285
+ return false;
1286
+ }
1287
+ return true;
1288
+ }
1289
+ // src/application/notification-outbox.ts
1290
+ import { z as z8 } from "zod";
1291
+ function notificationOutboxStateSchema(payload) {
1292
+ return z8.object({
1293
+ schemaVersion: z8.literal(1),
1294
+ queue: z8.array(z8.object({
1295
+ key: z8.string().min(1).max(512),
1296
+ payload,
1297
+ delivery: NotificationDeliveryStateSchema.optional(),
1298
+ createdAt: z8.string().datetime({ offset: true }),
1299
+ attempts: z8.number().int().nonnegative(),
1300
+ nextAttemptAt: z8.string().datetime({ offset: true }),
1301
+ leaseOwner: z8.string().min(1).max(128).nullable(),
1302
+ leaseId: z8.string().min(1).max(128).nullable(),
1303
+ leaseUntil: z8.string().datetime({ offset: true }).nullable()
1304
+ }).strict()),
1305
+ receipts: z8.array(z8.object({
1306
+ key: z8.string().min(1).max(512),
1307
+ completedAt: z8.string().datetime({ offset: true }),
1308
+ delivery: NotificationDeliveryStateSchema.optional()
1309
+ }).strict())
1310
+ }).strict();
1311
+ }
1312
+ var emptyOutboxState = () => ({
1313
+ schemaVersion: 1,
1314
+ queue: [],
1315
+ receipts: []
1316
+ });
880
1317
  async function reportOutboxError(config, error) {
881
1318
  try {
882
1319
  await config.onError?.(error);
883
1320
  } catch {}
884
1321
  }
1322
+ function notificationRejection(config, parse, bounded, clock, backoffMs, pollIntervalMs) {
1323
+ const maxAttempts = config.maxAttempts ?? 100;
1324
+ return async (claimed, error) => {
1325
+ const unknownPlan = error instanceof NotificationPlanVersionError;
1326
+ if (unknownPlan)
1327
+ await reportOutboxError(config, error);
1328
+ const classification = unknownPlan ? { retryable: true } : await config.classify(error);
1329
+ return config.store.update((current) => {
1330
+ const state = parse(current);
1331
+ const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
1332
+ if (!live)
1333
+ return { state, result: null };
1334
+ const attempts = unknownPlan ? Math.max(0, live.attempts - 1) : live.attempts;
1335
+ const reason = dropReason(classification, attempts, maxAttempts);
1336
+ if (reason) {
1337
+ return {
1338
+ state: bounded({
1339
+ ...state,
1340
+ queue: state.queue.filter((item) => item.key !== live.key)
1341
+ }),
1342
+ result: { item: { ...live, attempts }, error, reason }
1343
+ };
1344
+ }
1345
+ const backoff = RetryDelaySchema.parse(backoffMs(Math.max(1, attempts)));
1346
+ const wait = unknownPlan ? Math.max(backoff, pollIntervalMs) : backoff;
1347
+ const retry = {
1348
+ ...live,
1349
+ attempts,
1350
+ nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
1351
+ ...NO_LEASE
1352
+ };
1353
+ return {
1354
+ state: bounded({
1355
+ ...state,
1356
+ queue: state.queue.map((item) => item.key === live.key ? retry : item)
1357
+ }),
1358
+ result: null
1359
+ };
1360
+ });
1361
+ };
1362
+ }
885
1363
  function createNotificationOutbox(config) {
886
1364
  const schema = notificationOutboxStateSchema(config.payloadSchema);
887
1365
  const clock = config.clock ?? (() => new Date);
888
1366
  const ownerId = config.ownerId ?? crypto.randomUUID();
889
- const { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts } = resolveOutboxLimits(config);
1367
+ const { pollIntervalMs, leaseMs, maxQueue, maxStateBytes, retainReceipts } = resolveOutboxLimits(config);
890
1368
  const backoffMs = config.backoffMs ?? ((attempt) => backoffDelay(DEFAULT_OUTBOX_BACKOFF, attempt));
891
1369
  let timer;
892
1370
  let running = false;
@@ -895,7 +1373,7 @@ function createNotificationOutbox(config) {
895
1373
  const bounded = (input) => boundedOutboxState(schema.parse(input), maxQueue, retainReceipts, maxStateBytes);
896
1374
  const parse = (state) => schema.parse(state ?? emptyOutboxState());
897
1375
  const enqueue = async (input) => {
898
- const key = z5.string().min(1).max(512).parse(input.key);
1376
+ const key = z8.string().min(1).max(512).parse(input.key);
899
1377
  const payload = config.payloadSchema.parse(input.payload);
900
1378
  const supersedes = new Set(input.supersedes ?? []);
901
1379
  return config.store.update((current) => {
@@ -904,7 +1382,16 @@ function createNotificationOutbox(config) {
904
1382
  return { state, result: false };
905
1383
  }
906
1384
  const now = clock().toISOString();
907
- const fresh = { key, payload, createdAt: now, attempts: 0, nextAttemptAt: now };
1385
+ const fresh = {
1386
+ key,
1387
+ payload,
1388
+ createdAt: now,
1389
+ attempts: 0,
1390
+ nextAttemptAt: now,
1391
+ ...config.delivery && {
1392
+ delivery: notificationDeliveryPlan(config.delivery, payload)
1393
+ }
1394
+ };
908
1395
  const queue = [
909
1396
  ...state.queue.filter((item) => !supersedes.has(item.key)),
910
1397
  { ...fresh, ...NO_LEASE }
@@ -932,11 +1419,15 @@ function createNotificationOutbox(config) {
932
1419
  const acknowledge = async (claimed) => {
933
1420
  await config.store.update((current) => {
934
1421
  const state = parse(current);
935
- const owns = state.queue.some((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
936
- if (!owns)
1422
+ const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
1423
+ if (!live)
937
1424
  return { state, result: undefined };
938
1425
  const receipts = [
939
- { key: claimed.key, completedAt: clock().toISOString() },
1426
+ {
1427
+ key: claimed.key,
1428
+ completedAt: clock().toISOString(),
1429
+ ...live.delivery && { delivery: live.delivery }
1430
+ },
940
1431
  ...state.receipts.filter((receipt) => receipt.key !== claimed.key)
941
1432
  ].slice(0, retainReceipts);
942
1433
  return {
@@ -949,39 +1440,7 @@ function createNotificationOutbox(config) {
949
1440
  };
950
1441
  });
951
1442
  };
952
- const reject = async (claimed, error) => {
953
- const classification = await config.classify(error);
954
- return config.store.update((current) => {
955
- const state = parse(current);
956
- const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
957
- if (!live)
958
- return { state, result: null };
959
- const attempts = live.attempts;
960
- const reason = dropReason(classification, attempts, maxAttempts);
961
- if (reason) {
962
- return {
963
- state: bounded({
964
- ...state,
965
- queue: state.queue.filter((item) => item.key !== live.key)
966
- }),
967
- result: { item: { ...live, attempts }, error, reason }
968
- };
969
- }
970
- const wait = RetryDelaySchema.parse(backoffMs(attempts));
971
- const retry = {
972
- ...live,
973
- nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
974
- ...NO_LEASE
975
- };
976
- return {
977
- state: bounded({
978
- ...state,
979
- queue: state.queue.map((item) => item.key === live.key ? retry : item)
980
- }),
981
- result: null
982
- };
983
- });
984
- };
1443
+ const reject = notificationRejection(config, parse, bounded, clock, backoffMs, pollIntervalMs);
985
1444
  const flushPass = async () => {
986
1445
  let delivered = 0;
987
1446
  while (!interrupted) {
@@ -989,11 +1448,29 @@ function createNotificationOutbox(config) {
989
1448
  if (!claimed)
990
1449
  return delivered;
991
1450
  try {
992
- await config.send({
993
- key: claimed.key,
994
- payload: claimed.payload,
995
- attempt: claimed.attempts
996
- });
1451
+ if (config.delivery) {
1452
+ const finished = await deliverNotification({
1453
+ item: claimed,
1454
+ store: config.store,
1455
+ config: config.delivery,
1456
+ send: config.send,
1457
+ clock,
1458
+ bounded,
1459
+ leaseMs,
1460
+ active: () => !interrupted,
1461
+ report: (error) => reportOutboxError(config, error)
1462
+ });
1463
+ if (!finished)
1464
+ return delivered;
1465
+ } else {
1466
+ if (claimed.delivery)
1467
+ throw new Error("An action-plan notification requires its delivery executor");
1468
+ await config.send({
1469
+ key: claimed.key,
1470
+ payload: claimed.payload,
1471
+ attempt: claimed.attempts
1472
+ });
1473
+ }
997
1474
  await acknowledge(claimed);
998
1475
  delivered += 1;
999
1476
  } catch (error) {
@@ -1072,11 +1549,11 @@ function notificationOutboxResource(outbox, config = {}) {
1072
1549
  });
1073
1550
  }
1074
1551
  // src/application/process-lifecycle.ts
1075
- import { z as z7 } from "zod";
1552
+ import { z as z10 } from "zod";
1076
1553
 
1077
1554
  // src/application/process-lifecycle-transitions.ts
1078
- import { z as z6 } from "zod";
1079
- var LifecycleTerminationSchema = z6.enum([
1555
+ import { z as z9 } from "zod";
1556
+ var LifecycleTerminationSchema = z9.enum([
1080
1557
  "active",
1081
1558
  "draining",
1082
1559
  "clean",
@@ -1085,22 +1562,22 @@ var LifecycleTerminationSchema = z6.enum([
1085
1562
  "hot-reload",
1086
1563
  "abnormal"
1087
1564
  ]);
1088
- var timestamp = z6.string().datetime({ offset: true });
1089
- var LifecycleRunSchema = z6.object({
1090
- runId: z6.string().min(1).max(128),
1091
- pid: z6.number().int().positive(),
1092
- version: z6.string().min(1).max(256),
1565
+ var timestamp = z9.string().datetime({ offset: true });
1566
+ var LifecycleRunSchema = z9.object({
1567
+ runId: z9.string().min(1).max(128),
1568
+ pid: z9.number().int().positive(),
1569
+ version: z9.string().min(1).max(256),
1093
1570
  startedAt: timestamp,
1094
1571
  readyAt: timestamp.nullable(),
1095
1572
  unavailableAt: timestamp.nullable().default(null),
1096
1573
  stoppedAt: timestamp.nullable(),
1097
1574
  termination: LifecycleTerminationSchema
1098
1575
  }).strict();
1099
- var LifecycleStateSchema = z6.object({
1100
- schemaVersion: z6.literal(1),
1101
- runs: z6.array(LifecycleRunSchema)
1576
+ var LifecycleStateSchema = z9.object({
1577
+ schemaVersion: z9.literal(1),
1578
+ runs: z9.array(LifecycleRunSchema)
1102
1579
  }).strict();
1103
- var PreviousExitSchema = z6.enum([
1580
+ var PreviousExitSchema = z9.enum([
1104
1581
  "first-boot",
1105
1582
  "hot-reload",
1106
1583
  "clean",
@@ -1127,7 +1604,7 @@ var UNKNOWN_VERSION = "unknown";
1127
1604
  function isVersionChange(previous, next) {
1128
1605
  return previous !== UNKNOWN_VERSION && next !== UNKNOWN_VERSION && previous !== next;
1129
1606
  }
1130
- var RetainSchema = z6.number().int().min(1).max(1000);
1607
+ var RetainSchema = z9.number().int().min(1).max(1000);
1131
1608
  function previousExitOf(previous, run, overlap) {
1132
1609
  if (!previous)
1133
1610
  return "first-boot";
@@ -1315,8 +1792,8 @@ function transitionProcessShutdown(source, input) {
1315
1792
  // src/application/process-lifecycle.ts
1316
1793
  function createProcessLifecycleLedger(config) {
1317
1794
  const clock = config.clock ?? (() => new Date);
1318
- const pid = z7.number().int().positive().parse(config.pid ?? process.pid);
1319
- const retain = z7.number().int().min(1).max(1000).parse(config.retain ?? 20);
1795
+ const pid = z10.number().int().positive().parse(config.pid ?? process.pid);
1796
+ const retain = z10.number().int().min(1).max(1000).parse(config.retain ?? 20);
1320
1797
  const configuredRunId = config.runId;
1321
1798
  const nextRunId = typeof configuredRunId === "function" ? configuredRunId : configuredRunId === undefined ? () => crypto.randomUUID() : () => configuredRunId;
1322
1799
  let runId = nextRunId();
@@ -1441,27 +1918,27 @@ function lifecycleLedgerResource(ledger, config) {
1441
1918
  });
1442
1919
  }
1443
1920
  // src/application/revision-signal.ts
1444
- import { z as z8 } from "zod";
1445
- var NonNegativeSafeIntegerSchema = z8.number().int().nonnegative().safe();
1446
- var PositiveSafeIntegerSchema2 = z8.number().int().positive().safe();
1447
- var TimerDelaySchema = z8.number().int().positive().max(2147483647);
1448
- var RevisionSignalStateSchema = z8.enum(["open", "closed"]);
1449
- var RevisionSignalWaitOutcomeSchema = z8.enum([
1921
+ import { z as z11 } from "zod";
1922
+ var NonNegativeSafeIntegerSchema = z11.number().int().nonnegative().safe();
1923
+ var PositiveSafeIntegerSchema2 = z11.number().int().positive().safe();
1924
+ var TimerDelaySchema = z11.number().int().positive().max(2147483647);
1925
+ var RevisionSignalStateSchema = z11.enum(["open", "closed"]);
1926
+ var RevisionSignalWaitOutcomeSchema = z11.enum([
1450
1927
  "changed",
1451
1928
  "timed-out",
1452
1929
  "aborted",
1453
1930
  "closed",
1454
1931
  "capacity"
1455
1932
  ]);
1456
- var RevisionSignalWaitResultSchema = z8.object({
1933
+ var RevisionSignalWaitResultSchema = z11.object({
1457
1934
  outcome: RevisionSignalWaitOutcomeSchema,
1458
1935
  revision: NonNegativeSafeIntegerSchema
1459
1936
  }).strict().readonly();
1460
- var RevisionSignalAdvanceResultSchema = z8.object({
1461
- outcome: z8.enum(["advanced", "closed"]),
1937
+ var RevisionSignalAdvanceResultSchema = z11.object({
1938
+ outcome: z11.enum(["advanced", "closed"]),
1462
1939
  revision: NonNegativeSafeIntegerSchema
1463
1940
  }).strict().readonly();
1464
- var RevisionSignalSnapshotSchema = z8.object({
1941
+ var RevisionSignalSnapshotSchema = z11.object({
1465
1942
  state: RevisionSignalStateSchema,
1466
1943
  revision: NonNegativeSafeIntegerSchema,
1467
1944
  pending: NonNegativeSafeIntegerSchema,
@@ -1665,6 +2142,78 @@ function managedServerResource(config) {
1665
2142
  }
1666
2143
  });
1667
2144
  }
2145
+ // src/application/watch-hub-source.ts
2146
+ function remember(source, deltaMemoryBytes, revision, value, fingerprint) {
2147
+ if (deltaMemoryBytes === 0)
2148
+ return;
2149
+ const bytes = (JSON.stringify(value) ?? "null").length;
2150
+ if (bytes > deltaMemoryBytes)
2151
+ return;
2152
+ source.history.set(revision, { value, fingerprint, bytes });
2153
+ source.historyBytes += bytes;
2154
+ for (const [oldest, entry] of source.history) {
2155
+ if (source.historyBytes <= deltaMemoryBytes)
2156
+ break;
2157
+ source.history.delete(oldest);
2158
+ source.historyBytes -= entry.bytes;
2159
+ }
2160
+ }
2161
+ function fullFrame(source) {
2162
+ const value = source.value;
2163
+ return {
2164
+ kind: "full",
2165
+ key: source.key,
2166
+ revision: source.revision,
2167
+ fingerprint: source.fingerprint ?? valueFingerprint(value),
2168
+ value
2169
+ };
2170
+ }
2171
+ function frameFor(source, subscriber) {
2172
+ const value = source.value;
2173
+ const full = fullFrame(source);
2174
+ const fingerprint = full.fingerprint;
2175
+ const base = source.baselines.get(subscriber);
2176
+ if (base === undefined)
2177
+ return full;
2178
+ if (base === source.revision) {
2179
+ return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
2180
+ }
2181
+ const held = source.history.get(base);
2182
+ if (!held)
2183
+ return full;
2184
+ const delta = diff(held.value, value);
2185
+ if (delta === undefined || !deltaWins(delta, value))
2186
+ return full;
2187
+ return {
2188
+ kind: "delta",
2189
+ key: source.key,
2190
+ revision: source.revision,
2191
+ fingerprint,
2192
+ base,
2193
+ delta
2194
+ };
2195
+ }
2196
+ function adoptBaseline(source, subscriber, have) {
2197
+ if (source.fingerprint === have.fingerprint) {
2198
+ source.baselines.set(subscriber, source.revision);
2199
+ return;
2200
+ }
2201
+ const named = source.history.get(have.revision);
2202
+ if (named?.fingerprint === have.fingerprint) {
2203
+ source.baselines.set(subscriber, have.revision);
2204
+ return;
2205
+ }
2206
+ for (const [revision, entry] of source.history) {
2207
+ if (entry.fingerprint !== have.fingerprint)
2208
+ continue;
2209
+ source.baselines.set(subscriber, revision);
2210
+ return;
2211
+ }
2212
+ }
2213
+ function valueFingerprint(value) {
2214
+ return argumentsDigest({ value });
2215
+ }
2216
+
1668
2217
  // src/application/watch-hub-core.ts
1669
2218
  var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
1670
2219
 
@@ -1678,9 +2227,23 @@ class WatchHubCore {
1678
2227
  same;
1679
2228
  reads = 0;
1680
2229
  closed = false;
2230
+ periodic;
1681
2231
  constructor(config) {
1682
2232
  this.config = config;
1683
2233
  this.maxWatches = config.maxWatchesPerSubscriber ?? 64;
2234
+ for (const bound of [config.maxSources ?? 1024, config.maxSubscribers ?? 1024]) {
2235
+ if (!Number.isInteger(bound) || bound < 1)
2236
+ throw new RangeError("Watch capacity must be positive");
2237
+ }
2238
+ if (config.reconcileIntervalMs !== undefined) {
2239
+ if (!Number.isInteger(config.reconcileIntervalMs) || config.reconcileIntervalMs < 10 || config.reconcileIntervalMs > 3600000)
2240
+ throw new RangeError("Invalid reconciliation interval");
2241
+ this.periodic = setInterval(() => {
2242
+ for (const source of this.sources.values())
2243
+ source.task.trigger();
2244
+ }, config.reconcileIntervalMs);
2245
+ this.periodic.unref?.();
2246
+ }
1684
2247
  this.holdMs = config.holdMs ?? 0;
1685
2248
  this.deltaMemoryBytes = config.deltaMemoryBytes ?? 262144;
1686
2249
  this.same = config.same ?? ((previous, next) => serializeCanonicalJson(previous) === serializeCanonicalJson(next));
@@ -1700,82 +2263,24 @@ class WatchHubCore {
1700
2263
  announceState(source, state) {
1701
2264
  source.state = state;
1702
2265
  for (const subscriber of source.subscribers) {
1703
- this.tell(source.key, () => subscriber.state(state));
1704
- }
1705
- }
1706
- remember(source, revision, value, fingerprint) {
1707
- if (this.deltaMemoryBytes === 0)
1708
- return;
1709
- const bytes = (JSON.stringify(value) ?? "null").length;
1710
- if (bytes > this.deltaMemoryBytes)
1711
- return;
1712
- source.history.set(revision, { value, fingerprint, bytes });
1713
- source.historyBytes += bytes;
1714
- for (const [oldest, entry] of source.history) {
1715
- if (source.historyBytes <= this.deltaMemoryBytes)
1716
- break;
1717
- source.history.delete(oldest);
1718
- source.historyBytes -= entry.bytes;
1719
- }
1720
- }
1721
- frameFor(source, subscriber) {
1722
- const value = source.value;
1723
- const fingerprint = source.fingerprint ?? valueFingerprint(value);
1724
- const full = {
1725
- kind: "full",
1726
- key: source.key,
1727
- revision: source.revision,
1728
- fingerprint,
1729
- value
1730
- };
1731
- const base = source.baselines.get(subscriber);
1732
- if (base === undefined)
1733
- return full;
1734
- if (base === source.revision) {
1735
- return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
1736
- }
1737
- const held = source.history.get(base);
1738
- if (!held)
1739
- return full;
1740
- const delta = diff(held.value, value);
1741
- if (delta === undefined || !deltaWins(delta, value))
1742
- return full;
1743
- return {
1744
- kind: "delta",
1745
- key: source.key,
1746
- revision: source.revision,
1747
- fingerprint,
1748
- base,
1749
- delta
1750
- };
1751
- }
1752
- adoptBaseline(source, subscriber, have) {
1753
- if (source.fingerprint === have.fingerprint) {
1754
- source.baselines.set(subscriber, source.revision);
1755
- return;
1756
- }
1757
- const named = source.history.get(have.revision);
1758
- if (named?.fingerprint === have.fingerprint) {
1759
- source.baselines.set(subscriber, have.revision);
1760
- return;
1761
- }
1762
- for (const [revision, entry] of source.history) {
1763
- if (entry.fingerprint !== have.fingerprint)
1764
- continue;
1765
- source.baselines.set(subscriber, revision);
1766
- return;
2266
+ for (const key of source.routes.get(subscriber)?.values() ?? []) {
2267
+ this.tell(key, () => subscriber.state({ ...state, key }));
2268
+ }
1767
2269
  }
1768
2270
  }
1769
2271
  deliver(source, subscriber) {
1770
- const frame = this.frameFor(source, subscriber);
1771
- const delivered = this.tell(source.key, () => subscriber.value(frame));
2272
+ const frame = frameFor(source, subscriber);
2273
+ let delivered = true;
2274
+ for (const key of source.routes.get(subscriber)?.values() ?? []) {
2275
+ delivered = this.tell(key, () => subscriber.value({ ...frame, key })) && delivered;
2276
+ }
1772
2277
  if (delivered)
1773
2278
  source.baselines.set(subscriber, source.revision);
1774
2279
  return delivered;
1775
2280
  }
1776
2281
  publish(source, value, fingerprint) {
1777
2282
  if (source.fingerprint !== undefined) {
1778
- this.remember(source, source.revision, source.value, source.fingerprint);
2283
+ remember(source, this.deltaMemoryBytes, source.revision, source.value, source.fingerprint);
1779
2284
  }
1780
2285
  source.revision += 1;
1781
2286
  source.value = value;
@@ -1783,58 +2288,34 @@ class WatchHubCore {
1783
2288
  for (const subscriber of source.subscribers)
1784
2289
  this.deliver(source, subscriber);
1785
2290
  }
1786
- async pump(source) {
1787
- if (source.reading || this.closed)
2291
+ async readSource(source) {
2292
+ this.reads += 1;
2293
+ const value = await this.config.read(source.operation, source.args, source.scope);
2294
+ if (this.closed || this.sources.get(source.id) !== source)
1788
2295
  return;
1789
- source.reading = true;
1790
- try {
1791
- while (source.dirty && !this.closed) {
1792
- source.dirty = false;
1793
- this.reads += 1;
1794
- try {
1795
- const value = await this.config.read(source.operation, source.args);
1796
- source.backoff.reset();
1797
- const signature = serializeCanonicalJson(value);
1798
- const unchanged = source.signature !== undefined && this.same(source.value, value);
1799
- source.signature = signature;
1800
- if (!unchanged)
1801
- this.publish(source, value, valueFingerprint(value));
1802
- if (source.state.phase !== "live") {
1803
- this.announceState(source, { key: source.key, phase: "live" });
1804
- }
1805
- } catch (error) {
1806
- const code = readErrorCode(error);
1807
- this.announceState(source, {
1808
- key: source.key,
1809
- phase: "unavailable",
1810
- reason: "source-error",
1811
- ...code !== undefined && { code },
1812
- message: error instanceof Error ? error.message : String(error)
1813
- });
1814
- this.config.logger?.warn?.("[stitchkit] watched read failed", {
1815
- key: watchKeyString(source.key),
1816
- error
1817
- });
1818
- this.scheduleRetry(source);
1819
- return;
1820
- }
1821
- }
1822
- } finally {
1823
- source.reading = false;
1824
- }
2296
+ const signature = serializeCanonicalJson(value);
2297
+ const unchanged = source.signature !== undefined && this.same(source.value, value);
2298
+ source.signature = signature;
2299
+ if (!unchanged)
2300
+ this.publish(source, value, valueFingerprint(value));
2301
+ if (source.state.phase !== "live")
2302
+ this.announceState(source, { key: source.key, phase: "live" });
1825
2303
  }
1826
- scheduleRetry(source) {
1827
- if (this.closed || source.subscribers.size === 0)
1828
- return;
1829
- clearTimeout(source.retry);
1830
- source.retry = setTimeout(() => {
1831
- source.dirty = true;
1832
- this.pump(source);
1833
- }, source.backoff.next());
1834
- source.retry.unref?.();
2304
+ readFailed(source, error) {
2305
+ this.announceState(source, {
2306
+ key: source.key,
2307
+ phase: "unavailable",
2308
+ reason: "source-error",
2309
+ ...AppError.is(error) ? { code: error.code, message: error.message } : { message: "The watched read failed" }
2310
+ });
2311
+ this.config.logger?.warn?.("[stitchkit] watched read failed", {
2312
+ key: watchKeyString(source.key),
2313
+ error
2314
+ });
1835
2315
  }
1836
- acquire(operation, key, args) {
1837
- const id = watchKeyString(key);
2316
+ acquire(operation, routed, args, scope) {
2317
+ const id = sourceId(scope, routed);
2318
+ const key = sharedKey(routed);
1838
2319
  const existing = this.sources.get(id);
1839
2320
  if (existing) {
1840
2321
  clearTimeout(existing.release);
@@ -1842,24 +2323,29 @@ class WatchHubCore {
1842
2323
  return existing;
1843
2324
  }
1844
2325
  const source = {
2326
+ id,
2327
+ scope,
1845
2328
  key,
1846
2329
  operation,
1847
2330
  args,
1848
2331
  subscribers: new Set,
2332
+ routes: new Map,
1849
2333
  unsubscribes: [],
1850
2334
  revision: 0,
1851
2335
  history: new Map,
1852
2336
  historyBytes: 0,
1853
2337
  baselines: new Map,
1854
- reading: false,
1855
- dirty: true,
1856
- state: { key, phase: "opening" },
1857
- backoff: createBackoff(this.config.backoff ?? DEFAULT_BACKOFF)
2338
+ task: createCoalescedTask({
2339
+ run: () => this.readSource(source),
2340
+ active: () => !this.closed && source.subscribers.size > 0,
2341
+ onError: (error) => this.readFailed(source, error),
2342
+ backoff: this.config.backoff ?? DEFAULT_BACKOFF
2343
+ }),
2344
+ state: { key, phase: "opening" }
1858
2345
  };
1859
2346
  for (const topic of this.config.invalidatedBy(operation, args)) {
1860
2347
  source.unsubscribes.push(this.config.subscribe(topic, () => {
1861
- source.dirty = true;
1862
- this.pump(source);
2348
+ source.task.trigger();
1863
2349
  }));
1864
2350
  }
1865
2351
  this.sources.set(id, source);
@@ -1868,17 +2354,17 @@ class WatchHubCore {
1868
2354
  release(source) {
1869
2355
  if (source.subscribers.size > 0)
1870
2356
  return;
1871
- clearTimeout(source.retry);
1872
- source.retry = undefined;
2357
+ source.task.pauseRetry();
1873
2358
  const drop = () => {
1874
- if (source.reading) {
2359
+ if (source.task.running) {
1875
2360
  source.release = setTimeout(drop, 10);
1876
2361
  source.release.unref?.();
1877
2362
  return;
1878
2363
  }
2364
+ source.task.close();
1879
2365
  for (const unsubscribe of source.unsubscribes)
1880
2366
  unsubscribe();
1881
- this.sources.delete(watchKeyString(source.key));
2367
+ this.sources.delete(source.id);
1882
2368
  };
1883
2369
  if (this.holdMs === 0) {
1884
2370
  drop();
@@ -1887,11 +2373,38 @@ class WatchHubCore {
1887
2373
  source.release = setTimeout(drop, this.holdMs);
1888
2374
  source.release.unref?.();
1889
2375
  }
1890
- attach(subscriber) {
1891
- const keys = new Set;
2376
+ unroute(source, subscriber, route) {
2377
+ const routes = source.routes.get(subscriber);
2378
+ routes?.delete(route);
2379
+ if (routes && routes.size > 0)
2380
+ return;
2381
+ source.routes.delete(subscriber);
2382
+ source.subscribers.delete(subscriber);
2383
+ source.baselines.delete(subscriber);
2384
+ this.release(source);
2385
+ }
2386
+ attach(subscriber, scope) {
2387
+ if (this.held.size >= (this.config.maxSubscribers ?? 1024)) {
2388
+ const refused = {
2389
+ open: () => ({ accepted: false, reason: "Watch subscriber capacity exceeded" }),
2390
+ close: () => {
2391
+ return;
2392
+ },
2393
+ detach: () => {
2394
+ return;
2395
+ }
2396
+ };
2397
+ return refused;
2398
+ }
2399
+ if (scope && (!scope.key || scope.key.length > 512))
2400
+ throw new Error("Invalid watch admission scope");
2401
+ const keys = new Map;
1892
2402
  this.held.set(subscriber, keys);
1893
- return {
2403
+ let detached = false;
2404
+ const attached = {
1894
2405
  open: (key, args, have) => {
2406
+ if (detached || scope?.signal.aborted)
2407
+ return { accepted: false, reason: "Watch scope ended" };
1895
2408
  if (this.closed)
1896
2409
  return { accepted: false, reason: "the watch hub is closed" };
1897
2410
  const operation = { service: key.service, action: key.action };
@@ -1901,8 +2414,9 @@ class WatchHubCore {
1901
2414
  reason: `${key.service}.${key.action} is not watchable`
1902
2415
  };
1903
2416
  }
1904
- const id = watchKeyString(key);
1905
- if (keys.has(id))
2417
+ const route = watchKeyString(key);
2418
+ const id = sourceId(scope?.key, key);
2419
+ if (keys.has(route))
1906
2420
  return { accepted: true };
1907
2421
  if (keys.size >= this.maxWatches) {
1908
2422
  return {
@@ -1910,82 +2424,91 @@ class WatchHubCore {
1910
2424
  reason: `this connection is already watching ${keys.size} reads (limit ${this.maxWatches})`
1911
2425
  };
1912
2426
  }
1913
- const source = this.acquire(operation, key, args);
2427
+ if (!this.sources.has(id) && this.sources.size >= (this.config.maxSources ?? 1024))
2428
+ return { accepted: false, reason: "Watch source capacity exceeded" };
2429
+ const source = this.acquire(operation, key, args, scope?.key);
2430
+ const alreadyRouted = source.routes.has(subscriber);
1914
2431
  source.subscribers.add(subscriber);
1915
- keys.add(id);
2432
+ const routes = source.routes.get(subscriber) ?? new Map;
2433
+ routes.set(route, key);
2434
+ source.routes.set(subscriber, routes);
2435
+ keys.set(route, id);
1916
2436
  if (have)
1917
- this.adoptBaseline(source, subscriber, have);
2437
+ adoptBaseline(source, subscriber, have);
1918
2438
  try {
1919
2439
  if (source.signature !== undefined) {
1920
- const frame = this.frameFor(source, subscriber);
1921
- subscriber.value(frame);
2440
+ const frame = alreadyRouted && !have ? fullFrame(source) : frameFor(source, subscriber);
2441
+ subscriber.value({ ...frame, key });
1922
2442
  source.baselines.set(subscriber, source.revision);
1923
2443
  }
1924
- subscriber.state(source.state);
2444
+ subscriber.state({ ...source.state, key });
1925
2445
  } catch (error) {
1926
- source.subscribers.delete(subscriber);
1927
- source.baselines.delete(subscriber);
1928
- keys.delete(id);
1929
- this.release(source);
2446
+ keys.delete(route);
2447
+ this.unroute(source, subscriber, route);
1930
2448
  return {
1931
2449
  accepted: false,
1932
2450
  reason: error instanceof Error ? error.message : String(error)
1933
2451
  };
1934
2452
  }
1935
- this.pump(source);
2453
+ if (!source.task.running && (source.signature === undefined || source.state.phase !== "live" || source.task.pending))
2454
+ source.task.trigger();
1936
2455
  return { accepted: true };
1937
2456
  },
1938
2457
  close: (key) => {
1939
- const id = watchKeyString(key);
1940
- if (!keys.delete(id))
2458
+ const route = watchKeyString(key);
2459
+ const id = keys.get(route);
2460
+ if (id === undefined)
1941
2461
  return;
2462
+ keys.delete(route);
1942
2463
  const source = this.sources.get(id);
1943
- if (!source)
1944
- return;
1945
- source.subscribers.delete(subscriber);
1946
- source.baselines.delete(subscriber);
1947
- this.release(source);
2464
+ if (source)
2465
+ this.unroute(source, subscriber, route);
1948
2466
  },
1949
2467
  detach: () => {
1950
- for (const id of keys) {
2468
+ detached = true;
2469
+ scope?.signal.removeEventListener("abort", onAbort);
2470
+ for (const [route, id] of keys) {
1951
2471
  const source = this.sources.get(id);
1952
- if (!source)
1953
- continue;
1954
- source.subscribers.delete(subscriber);
1955
- source.baselines.delete(subscriber);
1956
- this.release(source);
2472
+ if (source)
2473
+ this.unroute(source, subscriber, route);
1957
2474
  }
1958
2475
  keys.clear();
1959
2476
  this.held.delete(subscriber);
1960
2477
  }
1961
2478
  };
2479
+ const onAbort = () => attached.detach();
2480
+ if (scope?.signal.aborted)
2481
+ attached.detach();
2482
+ else
2483
+ scope?.signal.addEventListener("abort", onAbort, { once: true });
2484
+ return attached;
1962
2485
  }
1963
2486
  close() {
1964
2487
  this.closed = true;
2488
+ clearInterval(this.periodic);
1965
2489
  const teardown = [...this.sources.values()];
1966
2490
  this.sources.clear();
1967
2491
  this.held.clear();
1968
2492
  for (const source of teardown) {
1969
- clearTimeout(source.retry);
2493
+ source.task.close();
1970
2494
  clearTimeout(source.release);
1971
2495
  this.announceState(source, {
1972
2496
  key: source.key,
1973
2497
  phase: "unavailable",
1974
2498
  reason: "source-unavailable"
1975
2499
  });
2500
+ source.task.close();
1976
2501
  for (const unsubscribe of source.unsubscribes)
1977
2502
  unsubscribe();
1978
2503
  }
1979
2504
  }
1980
2505
  }
1981
- function valueFingerprint(value) {
1982
- return argumentsDigest({ value });
2506
+ function sharedKey(key) {
2507
+ const { instance: _routing, ...shared } = key;
2508
+ return shared;
1983
2509
  }
1984
- function readErrorCode(error) {
1985
- if (typeof error !== "object" || error === null)
1986
- return;
1987
- const code = Reflect.get(error, "code");
1988
- return typeof code === "string" ? code : undefined;
2510
+ function sourceId(scope, key) {
2511
+ return JSON.stringify([scope ?? null, watchKeyString(sharedKey(key))]);
1989
2512
  }
1990
2513
 
1991
2514
  // src/application/watch-hub.ts
@@ -2000,7 +2523,7 @@ function watchKey(operation, args) {
2000
2523
  function createWatchHub(config) {
2001
2524
  const hub = new WatchHubCore(config);
2002
2525
  return {
2003
- attach: (subscriber) => hub.attach(subscriber),
2526
+ attach: (subscriber, scope) => hub.attach(subscriber, scope),
2004
2527
  readCount: () => hub.reads,
2005
2528
  size: () => hub.sources.size,
2006
2529
  close: () => hub.close()
@@ -2040,6 +2563,7 @@ export {
2040
2563
  createApplicationEventSink,
2041
2564
  createApplication,
2042
2565
  createActivityProjection,
2566
+ changeSubscriptionResource,
2043
2567
  applicationLifecycleEvent,
2044
2568
  RevisionSignalWaitResultSchema,
2045
2569
  RevisionSignalWaitOutcomeSchema,
@@ -2048,6 +2572,8 @@ export {
2048
2572
  RevisionSignalAdvanceResultSchema,
2049
2573
  PreviousExitSchema,
2050
2574
  PolicyDecisionSchema,
2575
+ NotificationPlanVersionError,
2576
+ NotificationDeliveryStateSchema,
2051
2577
  ManagedScheduleStatusSchema,
2052
2578
  ManagedScheduleOverlapSchema,
2053
2579
  ManagedScheduleErrorPolicySchema,