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.
- package/CHANGELOG.md +124 -1
- package/dist/agent-runtime/contained-darwin.d.ts +0 -17
- package/dist/agent-runtime/contained-darwin.d.ts.map +1 -1
- package/dist/agent-runtime/contained-write.d.ts.map +1 -1
- package/dist/agent-runtime/invocation-attempt.d.ts +28 -0
- package/dist/agent-runtime/invocation-attempt.d.ts.map +1 -0
- package/dist/agent-runtime/invocation-failure.d.ts +6 -0
- package/dist/agent-runtime/invocation-failure.d.ts.map +1 -0
- package/dist/agent-runtime/invocation-payload.d.ts +20 -0
- package/dist/agent-runtime/invocation-payload.d.ts.map +1 -0
- package/dist/agent-runtime/invocation-read.d.ts +131 -0
- package/dist/agent-runtime/invocation-read.d.ts.map +1 -0
- package/dist/agent-runtime/invocation-schema.d.ts +378 -0
- package/dist/agent-runtime/invocation-schema.d.ts.map +1 -0
- package/dist/agent-runtime/invocations.d.ts +52 -0
- package/dist/agent-runtime/invocations.d.ts.map +1 -0
- package/dist/agent-runtime/projections.d.ts.map +1 -1
- package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
- package/dist/agent-runtime/run-execution-state.d.ts +4 -2
- package/dist/agent-runtime/run-execution-state.d.ts.map +1 -1
- package/dist/agent-runtime/run-execution.d.ts.map +1 -1
- package/dist/agent-runtime/run-failure.d.ts.map +1 -1
- package/dist/agent-runtime/run-model-attempts.d.ts.map +1 -1
- package/dist/agent-runtime/run-operation-lifecycle.d.ts +2 -2
- package/dist/agent-runtime/run-operation-lifecycle.d.ts.map +1 -1
- package/dist/agent-runtime/run-provider-ledger.d.ts +1 -0
- package/dist/agent-runtime/run-provider-ledger.d.ts.map +1 -1
- package/dist/agent-runtime/runtime.d.ts +11 -0
- package/dist/agent-runtime/runtime.d.ts.map +1 -1
- package/dist/agent-runtime/schedule-dispatch.d.ts +9 -2
- package/dist/agent-runtime/schedule-dispatch.d.ts.map +1 -1
- package/dist/agent-runtime/schedule-records.d.ts +8 -2
- package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
- package/dist/agent-runtime/schedules.d.ts +5 -0
- package/dist/agent-runtime/schedules.d.ts.map +1 -1
- package/dist/agent-runtime/sqlite-events.d.ts.map +1 -1
- package/dist/agent-runtime/store-create.d.ts.map +1 -1
- package/dist/agent-runtime/store-driver-contract.d.ts +9 -0
- package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
- package/dist/agent-runtime/store-event-once.d.ts +37 -0
- package/dist/agent-runtime/store-event-once.d.ts.map +1 -0
- package/dist/agent-runtime/store-memory.d.ts.map +1 -1
- package/dist/agent-runtime/store.d.ts +11 -0
- package/dist/agent-runtime/store.d.ts.map +1 -1
- package/dist/application/change-subscription.d.ts +36 -0
- package/dist/application/change-subscription.d.ts.map +1 -0
- package/dist/application/diagnostic-journal-contract.d.ts +3 -6
- package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
- package/dist/application/notification-delivery.d.ts +55 -0
- package/dist/application/notification-delivery.d.ts.map +1 -0
- package/dist/application/notification-outbox-state.d.ts +41 -0
- package/dist/application/notification-outbox-state.d.ts.map +1 -0
- package/dist/application/notification-outbox.d.ts +7 -1
- package/dist/application/notification-outbox.d.ts.map +1 -1
- package/dist/application/watch-hub-core.d.ts +14 -84
- package/dist/application/watch-hub-core.d.ts.map +1 -1
- package/dist/application/watch-hub-source.d.ts +98 -0
- package/dist/application/watch-hub-source.d.ts.map +1 -0
- package/dist/application/watch-hub.d.ts +10 -2
- package/dist/application/watch-hub.d.ts.map +1 -1
- package/dist/browser/session-credentials.d.ts +26 -0
- package/dist/browser/session-credentials.d.ts.map +1 -0
- package/dist/browser/session.d.ts +33 -0
- package/dist/browser/session.d.ts.map +1 -0
- package/dist/durability/events.d.ts +6 -0
- package/dist/durability/events.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/coding-tools.js +3 -2
- package/dist/entrypoints/agent-runtime/harness.js +5 -3
- package/dist/entrypoints/agent-runtime/sandbox.js +3 -2
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +3 -3
- package/dist/entrypoints/agent-runtime/sqlite/node.js +3 -3
- package/dist/entrypoints/agent-runtime/testing.js +3 -3
- package/dist/entrypoints/agent-runtime.d.ts +3 -0
- package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime.js +65 -20
- package/dist/entrypoints/application/diagnostic-journal.js +2 -1
- package/dist/entrypoints/application.d.ts +3 -1
- package/dist/entrypoints/application.d.ts.map +1 -1
- package/dist/entrypoints/application.js +820 -294
- package/dist/entrypoints/files.js +9 -2
- package/dist/entrypoints/index.d.ts +2 -0
- package/dist/entrypoints/index.d.ts.map +1 -1
- package/dist/entrypoints/index.js +199 -0
- package/dist/entrypoints/live.js +78 -19
- package/dist/entrypoints/node.js +1 -1
- package/dist/entrypoints/react.d.ts +1 -1
- package/dist/entrypoints/react.d.ts.map +1 -1
- package/dist/entrypoints/react.js +59 -16
- package/dist/entrypoints/server.js +4 -4
- package/dist/{index-sp1tzwnb.js → index-10x2hbx0.js} +5 -2
- package/dist/{index-8p52kd5p.js → index-c6bxbsnc.js} +100 -14
- package/dist/{index-gxrt5h6v.js → index-cyktc4bp.js} +3 -0
- package/dist/{index-1m2thnq0.js → index-eaysmdtq.js} +11 -2
- package/dist/{index-jhx828hc.js → index-f4fx2tza.js} +948 -279
- package/dist/{index-px2gm2jp.js → index-fhwqwxe0.js} +17 -55
- package/dist/{index-e4hkgm5t.js → index-gw8bmyaj.js} +4 -2
- package/dist/{index-p3vsv1fb.js → index-hadmp193.js} +14 -5
- package/dist/{index-wt743f7f.js → index-k6gpggb9.js} +2 -1
- package/dist/index-q3pyb9n0.js +22 -0
- package/dist/{index-wsm5w4ee.js → index-vqr7f7v8.js} +51 -3
- package/dist/index-zjjyynm3.js +42 -0
- package/dist/internal/coalesced-task.d.ts +17 -0
- package/dist/internal/coalesced-task.d.ts.map +1 -0
- package/dist/internal/darwin-binding.d.ts +19 -0
- package/dist/internal/darwin-binding.d.ts.map +1 -0
- package/dist/internal/exclusive-lock.d.ts +20 -7
- package/dist/internal/exclusive-lock.d.ts.map +1 -1
- package/dist/internal/mutation-queue.d.ts +4 -0
- package/dist/internal/mutation-queue.d.ts.map +1 -0
- package/dist/internal/process-identity.d.ts +1 -1
- package/dist/internal/process-identity.d.ts.map +1 -1
- package/dist/internal/process-instance.d.ts +26 -0
- package/dist/internal/process-instance.d.ts.map +1 -0
- package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
- package/dist/live/watch-client-state.d.ts +71 -0
- package/dist/live/watch-client-state.d.ts.map +1 -0
- package/dist/live/watch-client.d.ts +5 -1
- package/dist/live/watch-client.d.ts.map +1 -1
- package/dist/live/watch-contract.d.ts +12 -0
- package/dist/live/watch-contract.d.ts.map +1 -1
- package/dist/react/cache-bridge.d.ts +10 -0
- package/dist/react/cache-bridge.d.ts.map +1 -1
- package/dist/server/realtime.d.ts.map +1 -1
- package/llms/agent-runtime.2.txt +102 -0
- package/llms/agent-runtime.3.txt +30 -1
- package/llms/agent-runtime.txt +8 -3
- package/llms/application.2.txt +84 -2
- package/llms/application.txt +32 -2
- package/llms/files.txt +1 -1
- package/llms/live.txt +8 -0
- package/llms/react.txt +8 -0
- package/llms/server.3.txt +57 -0
- package/llms/stitchkit.2.txt +41 -0
- package/llms/stitchkit.3.txt +26 -0
- package/llms/stitchkit.txt +3 -3
- package/llms/upgrading-0.37.0.txt +176 -0
- package/llms/{upgrading-0.37.0-0.58.0.txt → upgrading-0.44.0-0.60.0.txt} +229 -169
- package/llms/{upgrading-0.59.0-0.71.0.txt → upgrading-0.61.0-0.73.0.txt} +122 -229
- package/llms/{upgrading-0.72.0-0.99.0.txt → upgrading-0.74.0-0.100.0.txt} +80 -123
- package/llms/upgrading.txt +4 -3
- package/llms-full.txt +471 -11
- package/llms.txt +10 -9
- package/native/darwin-arm64.node +0 -0
- package/native/darwin-x64.node +0 -0
- package/package.json +1 -1
- package/dist/agent-runtime/run-mutation-queue.d.ts +0 -21
- 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-
|
|
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
|
|
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/
|
|
488
|
+
// src/application/change-subscription.ts
|
|
487
489
|
import { z as z2 } from "zod";
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
566
|
-
var ApplicationLifecycleEventSchema =
|
|
567
|
-
type:
|
|
874
|
+
import { z as z4 } from "zod";
|
|
875
|
+
var ApplicationLifecycleEventSchema = z4.object({
|
|
876
|
+
type: z4.literal("application-state"),
|
|
568
877
|
applicationId: ApplicationIdSchema,
|
|
569
|
-
epoch:
|
|
570
|
-
revision:
|
|
878
|
+
epoch: z4.string().uuid(),
|
|
879
|
+
revision: z4.number().int().nonnegative(),
|
|
571
880
|
lifecycle: ApplicationLifecycleSchema,
|
|
572
881
|
health: ApplicationHealthSchema,
|
|
573
|
-
ready:
|
|
574
|
-
capturedAt:
|
|
575
|
-
resources:
|
|
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
|
|
607
|
-
var ApplicationHealthHandlerOptionsSchema =
|
|
608
|
-
kind:
|
|
609
|
-
retryAfterSeconds:
|
|
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 =
|
|
612
|
-
retryAfterSeconds:
|
|
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-
|
|
808
|
-
import { z as
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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 = {
|
|
1122
|
+
var DEFAULT_OUTBOX_BACKOFF = {
|
|
1123
|
+
minDelayMs: 1000,
|
|
1124
|
+
maxDelayMs: 60000,
|
|
1125
|
+
jitter: 0
|
|
1126
|
+
};
|
|
835
1127
|
function resolveOutboxLimits(config) {
|
|
836
|
-
const pollIntervalMs =
|
|
837
|
-
const leaseMs =
|
|
838
|
-
const maxAttempts =
|
|
839
|
-
const maxQueue =
|
|
840
|
-
const maxStateBytes =
|
|
841
|
-
const retainReceipts =
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 = {
|
|
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
|
|
936
|
-
if (!
|
|
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
|
-
{
|
|
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 =
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
1552
|
+
import { z as z10 } from "zod";
|
|
1076
1553
|
|
|
1077
1554
|
// src/application/process-lifecycle-transitions.ts
|
|
1078
|
-
import { z as
|
|
1079
|
-
var LifecycleTerminationSchema =
|
|
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 =
|
|
1089
|
-
var LifecycleRunSchema =
|
|
1090
|
-
runId:
|
|
1091
|
-
pid:
|
|
1092
|
-
version:
|
|
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 =
|
|
1100
|
-
schemaVersion:
|
|
1101
|
-
runs:
|
|
1576
|
+
var LifecycleStateSchema = z9.object({
|
|
1577
|
+
schemaVersion: z9.literal(1),
|
|
1578
|
+
runs: z9.array(LifecycleRunSchema)
|
|
1102
1579
|
}).strict();
|
|
1103
|
-
var PreviousExitSchema =
|
|
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 =
|
|
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 =
|
|
1319
|
-
const retain =
|
|
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
|
|
1445
|
-
var NonNegativeSafeIntegerSchema =
|
|
1446
|
-
var PositiveSafeIntegerSchema2 =
|
|
1447
|
-
var TimerDelaySchema =
|
|
1448
|
-
var RevisionSignalStateSchema =
|
|
1449
|
-
var RevisionSignalWaitOutcomeSchema =
|
|
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 =
|
|
1933
|
+
var RevisionSignalWaitResultSchema = z11.object({
|
|
1457
1934
|
outcome: RevisionSignalWaitOutcomeSchema,
|
|
1458
1935
|
revision: NonNegativeSafeIntegerSchema
|
|
1459
1936
|
}).strict().readonly();
|
|
1460
|
-
var RevisionSignalAdvanceResultSchema =
|
|
1461
|
-
outcome:
|
|
1937
|
+
var RevisionSignalAdvanceResultSchema = z11.object({
|
|
1938
|
+
outcome: z11.enum(["advanced", "closed"]),
|
|
1462
1939
|
revision: NonNegativeSafeIntegerSchema
|
|
1463
1940
|
}).strict().readonly();
|
|
1464
|
-
var RevisionSignalSnapshotSchema =
|
|
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
|
-
|
|
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 =
|
|
1771
|
-
|
|
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
|
-
|
|
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
|
|
1787
|
-
|
|
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
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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,
|
|
1837
|
-
const id =
|
|
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
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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.
|
|
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
|
-
|
|
1872
|
-
source.retry = undefined;
|
|
2357
|
+
source.task.pauseRetry();
|
|
1873
2358
|
const drop = () => {
|
|
1874
|
-
if (source.
|
|
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(
|
|
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
|
-
|
|
1891
|
-
const
|
|
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
|
-
|
|
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
|
|
1905
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2437
|
+
adoptBaseline(source, subscriber, have);
|
|
1918
2438
|
try {
|
|
1919
2439
|
if (source.signature !== undefined) {
|
|
1920
|
-
const frame =
|
|
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
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
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
|
|
1940
|
-
|
|
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 (
|
|
1944
|
-
|
|
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
|
-
|
|
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 (
|
|
1953
|
-
|
|
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
|
-
|
|
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
|
|
1982
|
-
|
|
2506
|
+
function sharedKey(key) {
|
|
2507
|
+
const { instance: _routing, ...shared } = key;
|
|
2508
|
+
return shared;
|
|
1983
2509
|
}
|
|
1984
|
-
function
|
|
1985
|
-
|
|
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,
|