stitchkit 0.98.2 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- 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 +22 -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 +18 -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 +46 -0
- package/dist/agent-runtime/invocations.d.ts.map +1 -0
- 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/store-create.d.ts.map +1 -1
- package/dist/agent-runtime/store-event-once.d.ts +29 -0
- package/dist/agent-runtime/store-event-once.d.ts.map +1 -0
- package/dist/agent-runtime/store.d.ts +5 -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 +32 -0
- package/dist/application/notification-delivery.d.ts.map +1 -0
- package/dist/application/notification-outbox-state.d.ts +37 -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 +10 -15
- package/dist/application/watch-hub-core.d.ts.map +1 -1
- 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 +20 -0
- package/dist/browser/session-credentials.d.ts.map +1 -0
- package/dist/browser/session.d.ts +31 -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 +5 -4
- package/dist/entrypoints/agent-runtime/harness.js +8 -6
- package/dist/entrypoints/agent-runtime/sandbox.js +5 -4
- 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 +21 -10
- 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 +629 -189
- package/dist/entrypoints/cli.js +6 -6
- package/dist/entrypoints/files.js +2 -1
- package/dist/entrypoints/index.d.ts +2 -0
- package/dist/entrypoints/index.d.ts.map +1 -1
- package/dist/entrypoints/index.js +172 -0
- package/dist/entrypoints/live.js +68 -13
- 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/testing.js +1 -1
- package/dist/entrypoints/tools/connections.js +2 -2
- package/dist/entrypoints/tools/invoker.js +4 -4
- package/dist/entrypoints/tools.js +8 -8
- package/dist/{index-wsm5w4ee.js → index-0b1cnze2.js} +35 -3
- package/dist/{index-np4m8hd3.js → index-0r0fbk7z.js} +9 -7
- package/dist/{index-p9sh0xx4.js → index-5p2m7we1.js} +3 -3
- package/dist/{index-55hkhm67.js → index-8qt2m66q.js} +1 -1
- package/dist/{index-hmp4800j.js → index-9t3dffv2.js} +3 -3
- package/dist/{index-khtgb21r.js → index-bm5rx7n6.js} +2 -2
- package/dist/{index-gxrt5h6v.js → index-cyktc4bp.js} +3 -0
- package/dist/{index-xw3s75nj.js → index-d8m8zpd8.js} +1 -1
- package/dist/{index-px2gm2jp.js → index-fhwqwxe0.js} +17 -55
- package/dist/{index-het2he5g.js → index-gw8bmyaj.js} +5 -3
- package/dist/{index-8p52kd5p.js → index-js7gqrb1.js} +96 -14
- package/dist/{index-wt743f7f.js → index-k6gpggb9.js} +2 -1
- package/dist/index-q3pyb9n0.js +22 -0
- package/dist/{index-f1nb70ae.js → index-r066jx76.js} +4 -4
- package/dist/{index-sp1tzwnb.js → index-s84he4v7.js} +2 -2
- package/dist/{index-863ea9jb.js → index-sh3s86na.js} +3 -3
- package/dist/{index-nts4a2br.js → index-xwhjjw2x.js} +879 -280
- package/dist/{index-1m2thnq0.js → index-y9qn426m.js} +2 -2
- package/dist/index-zjjyynm3.js +42 -0
- package/dist/internal/coalesced-task.d.ts +15 -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/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/tools/schema/flatten-join.d.ts.map +1 -1
- package/llms/agent-runtime.2.txt +94 -0
- package/llms/agent-runtime.3.txt +24 -0
- package/llms/application.2.txt +68 -2
- package/llms/application.txt +32 -2
- package/llms/files.txt +1 -1
- package/llms/live.txt +7 -0
- package/llms/react.txt +8 -0
- package/llms/server.3.txt +35 -0
- package/llms/stitchkit.2.txt +41 -0
- package/llms/stitchkit.3.txt +22 -0
- package/llms/stitchkit.txt +3 -3
- package/llms/tools.txt +9 -8
- 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.98.0.txt → upgrading-0.74.0-0.100.0.txt} +96 -122
- package/llms/upgrading.txt +4 -3
- package/llms-full.txt +431 -14
- package/llms.txt +11 -10
- 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
|
@@ -73,7 +73,7 @@ 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,
|
|
@@ -483,12 +483,317 @@ function createBoundedAdmission(config) {
|
|
|
483
483
|
getSnapshot
|
|
484
484
|
};
|
|
485
485
|
}
|
|
486
|
-
// src/
|
|
486
|
+
// src/application/change-subscription.ts
|
|
487
487
|
import { z as z2 } from "zod";
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
488
|
+
|
|
489
|
+
// src/internal/coalesced-task.ts
|
|
490
|
+
function createCoalescedTask(config) {
|
|
491
|
+
const backoff = createBackoff(config.backoff);
|
|
492
|
+
let running = false;
|
|
493
|
+
let dirty = false;
|
|
494
|
+
let closed = false;
|
|
495
|
+
let retry;
|
|
496
|
+
const pauseRetry = () => {
|
|
497
|
+
clearTimeout(retry);
|
|
498
|
+
retry = undefined;
|
|
499
|
+
};
|
|
500
|
+
const pump = async () => {
|
|
501
|
+
if (running || closed || !config.active() || retry)
|
|
502
|
+
return;
|
|
503
|
+
running = true;
|
|
504
|
+
try {
|
|
505
|
+
while (dirty && !closed && config.active()) {
|
|
506
|
+
dirty = false;
|
|
507
|
+
try {
|
|
508
|
+
await config.run();
|
|
509
|
+
backoff.reset();
|
|
510
|
+
} catch (error) {
|
|
511
|
+
dirty = true;
|
|
512
|
+
if (!closed && config.active()) {
|
|
513
|
+
retry = setTimeout(() => {
|
|
514
|
+
retry = undefined;
|
|
515
|
+
pump();
|
|
516
|
+
}, backoff.next());
|
|
517
|
+
retry.unref?.();
|
|
518
|
+
config.onError(error);
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} finally {
|
|
524
|
+
running = false;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
get running() {
|
|
529
|
+
return running;
|
|
530
|
+
},
|
|
531
|
+
trigger() {
|
|
532
|
+
if (closed)
|
|
533
|
+
return;
|
|
534
|
+
dirty = true;
|
|
535
|
+
pump();
|
|
536
|
+
},
|
|
537
|
+
pauseRetry,
|
|
538
|
+
close() {
|
|
539
|
+
closed = true;
|
|
540
|
+
dirty = false;
|
|
541
|
+
pauseRetry();
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/application/change-subscription.ts
|
|
547
|
+
class ChangeSubscription {
|
|
548
|
+
config;
|
|
549
|
+
keys;
|
|
550
|
+
interval;
|
|
551
|
+
timeout;
|
|
552
|
+
backoff;
|
|
553
|
+
tasks;
|
|
554
|
+
failedKeys = new Set;
|
|
555
|
+
lifetime;
|
|
556
|
+
attempt;
|
|
557
|
+
connection;
|
|
558
|
+
opening;
|
|
559
|
+
closing;
|
|
560
|
+
retry;
|
|
561
|
+
periodic;
|
|
562
|
+
context;
|
|
563
|
+
removeAbort;
|
|
564
|
+
constructor(config) {
|
|
565
|
+
this.config = config;
|
|
566
|
+
this.keys = z2.array(z2.string().min(1).max(256)).min(1).max(128).parse(config.keys);
|
|
567
|
+
if (new Set(this.keys).size !== this.keys.length)
|
|
568
|
+
throw new Error("Subscription keys must be unique");
|
|
569
|
+
this.interval = z2.number().int().min(10).max(3600000).parse(config.reconcileIntervalMs);
|
|
570
|
+
this.timeout = z2.number().int().min(1).max(3600000).parse(config.timeoutMs ?? 30000);
|
|
571
|
+
const policy = config.backoff ?? { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
|
|
572
|
+
this.backoff = createBackoff(policy);
|
|
573
|
+
this.tasks = new Map(this.keys.map((key) => [
|
|
574
|
+
key,
|
|
575
|
+
createCoalescedTask({
|
|
576
|
+
active: () => this.connection !== undefined && this.attempt?.signal.aborted === false,
|
|
577
|
+
run: () => this.read(key),
|
|
578
|
+
onError: (error) => {
|
|
579
|
+
this.failedKeys.add(key);
|
|
580
|
+
this.report(error);
|
|
581
|
+
},
|
|
582
|
+
backoff: policy
|
|
583
|
+
})
|
|
584
|
+
]));
|
|
585
|
+
}
|
|
586
|
+
report(error) {
|
|
587
|
+
this.context?.reportHealth("degraded");
|
|
588
|
+
try {
|
|
589
|
+
this.config.onError(error);
|
|
590
|
+
} catch {}
|
|
591
|
+
}
|
|
592
|
+
async read(key) {
|
|
593
|
+
const connection = this.connection;
|
|
594
|
+
const attempt = this.attempt;
|
|
595
|
+
if (!connection || !attempt)
|
|
596
|
+
return;
|
|
597
|
+
const readAbort = new AbortController;
|
|
598
|
+
const signal = AbortSignal.any([attempt.signal, readAbort.signal]);
|
|
599
|
+
const work = this.config.reconcile(key, {
|
|
600
|
+
signal,
|
|
601
|
+
commit: (publish) => {
|
|
602
|
+
if (signal.aborted || this.connection !== connection)
|
|
603
|
+
return false;
|
|
604
|
+
publish();
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
const outcome = await withDeadline(work, this.timeout);
|
|
609
|
+
if (!outcome.settled) {
|
|
610
|
+
readAbort.abort();
|
|
611
|
+
this.report(new Error("Reconciliation deadline exceeded"));
|
|
612
|
+
await work;
|
|
613
|
+
throw new Error("Reconciliation deadline exceeded");
|
|
614
|
+
}
|
|
615
|
+
if (!signal.aborted && this.connection === connection) {
|
|
616
|
+
this.failedKeys.delete(key);
|
|
617
|
+
if (this.failedKeys.size === 0)
|
|
618
|
+
this.context?.reportHealth("healthy");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
reconcile() {
|
|
622
|
+
for (const task of this.tasks.values())
|
|
623
|
+
task.trigger();
|
|
624
|
+
}
|
|
625
|
+
schedule() {
|
|
626
|
+
if (!this.lifetime || this.lifetime.signal.aborted || this.retry)
|
|
627
|
+
return;
|
|
628
|
+
this.retry = setTimeout(() => {
|
|
629
|
+
this.retry = undefined;
|
|
630
|
+
this.open();
|
|
631
|
+
}, this.backoff.next());
|
|
632
|
+
this.retry.unref?.();
|
|
633
|
+
}
|
|
634
|
+
async release(handle) {
|
|
635
|
+
const work = Promise.resolve().then(() => handle.close());
|
|
636
|
+
this.closing = work;
|
|
637
|
+
try {
|
|
638
|
+
const outcome = await withDeadline(work, this.timeout);
|
|
639
|
+
if (!outcome.settled) {
|
|
640
|
+
this.report(new Error("Subscription close deadline exceeded"));
|
|
641
|
+
await work;
|
|
642
|
+
}
|
|
643
|
+
} finally {
|
|
644
|
+
if (this.closing === work)
|
|
645
|
+
this.closing = undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
lost(handle, error) {
|
|
649
|
+
if (this.connection !== handle)
|
|
650
|
+
return;
|
|
651
|
+
this.connection = undefined;
|
|
652
|
+
this.attempt?.abort();
|
|
653
|
+
this.report(error);
|
|
654
|
+
this.release(handle).then(() => this.schedule(), (cause) => this.report(cause));
|
|
655
|
+
}
|
|
656
|
+
open() {
|
|
657
|
+
if (this.opening)
|
|
658
|
+
return this.opening;
|
|
659
|
+
const lifetime = this.lifetime;
|
|
660
|
+
if (!lifetime || lifetime.signal.aborted || this.closing)
|
|
661
|
+
return Promise.resolve();
|
|
662
|
+
this.opening = this.connect(lifetime).finally(() => {
|
|
663
|
+
this.opening = undefined;
|
|
664
|
+
});
|
|
665
|
+
return this.opening;
|
|
666
|
+
}
|
|
667
|
+
async connect(lifetime) {
|
|
668
|
+
const attempt = new AbortController;
|
|
669
|
+
this.attempt = attempt;
|
|
670
|
+
const onAbort = () => attempt.abort();
|
|
671
|
+
lifetime.signal.addEventListener("abort", onAbort, { once: true });
|
|
672
|
+
let handle;
|
|
673
|
+
let lost = false;
|
|
674
|
+
const work = (async () => {
|
|
675
|
+
handle = await this.config.connect(attempt.signal);
|
|
676
|
+
if (attempt.signal.aborted)
|
|
677
|
+
return;
|
|
678
|
+
const owned = handle;
|
|
679
|
+
await owned.subscribe({
|
|
680
|
+
signal: attempt.signal,
|
|
681
|
+
hint: (key) => {
|
|
682
|
+
if (attempt.signal.aborted || lost)
|
|
683
|
+
return;
|
|
684
|
+
const task = this.tasks.get(key);
|
|
685
|
+
if (task)
|
|
686
|
+
task.trigger();
|
|
687
|
+
else
|
|
688
|
+
this.report(new Error("Unknown subscription key"));
|
|
689
|
+
},
|
|
690
|
+
disconnected: (error) => {
|
|
691
|
+
if (attempt.signal.aborted || this.lifetime !== lifetime)
|
|
692
|
+
return;
|
|
693
|
+
lost = true;
|
|
694
|
+
if (this.connection === owned)
|
|
695
|
+
this.lost(owned, error);
|
|
696
|
+
else
|
|
697
|
+
this.report(error);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
})();
|
|
701
|
+
try {
|
|
702
|
+
const outcome = await withDeadline(work, this.timeout);
|
|
703
|
+
if (!outcome.settled) {
|
|
704
|
+
attempt.abort();
|
|
705
|
+
this.report(new Error("Subscription connect deadline exceeded"));
|
|
706
|
+
await work;
|
|
707
|
+
}
|
|
708
|
+
if (attempt.signal.aborted || lifetime !== this.lifetime || lost) {
|
|
709
|
+
if (handle)
|
|
710
|
+
await this.release(handle);
|
|
711
|
+
this.schedule();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this.connection = handle;
|
|
715
|
+
this.backoff.reset();
|
|
716
|
+
this.context?.reportHealth("healthy");
|
|
717
|
+
this.reconcile();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
attempt.abort();
|
|
720
|
+
if (handle) {
|
|
721
|
+
try {
|
|
722
|
+
await this.release(handle);
|
|
723
|
+
} catch (cause) {
|
|
724
|
+
this.report(cause);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!lifetime.signal.aborted) {
|
|
729
|
+
this.report(error);
|
|
730
|
+
this.schedule();
|
|
731
|
+
}
|
|
732
|
+
} finally {
|
|
733
|
+
lifetime.signal.removeEventListener("abort", onAbort);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
start(context) {
|
|
737
|
+
if (this.lifetime && !this.lifetime.signal.aborted)
|
|
738
|
+
throw new Error("Subscription already started");
|
|
739
|
+
if (this.opening || this.closing || [...this.tasks.values()].some((task) => task.running))
|
|
740
|
+
throw new Error("Previous subscription work has not settled");
|
|
741
|
+
this.context = context;
|
|
742
|
+
this.lifetime = new AbortController;
|
|
743
|
+
const abort = () => {
|
|
744
|
+
this.close();
|
|
745
|
+
};
|
|
746
|
+
context.signal.addEventListener("abort", abort, { once: true });
|
|
747
|
+
this.removeAbort = () => context.signal.removeEventListener("abort", abort);
|
|
748
|
+
if (context.signal.aborted) {
|
|
749
|
+
this.close();
|
|
750
|
+
return {};
|
|
751
|
+
}
|
|
752
|
+
this.periodic = setInterval(() => this.reconcile(), this.interval);
|
|
753
|
+
this.periodic.unref?.();
|
|
754
|
+
const ready = (async () => {
|
|
755
|
+
const outcome = await withDeadline(this.open(), this.timeout);
|
|
756
|
+
if (!outcome.settled || !this.connection) {
|
|
757
|
+
this.close().catch((error) => this.report(error));
|
|
758
|
+
throw new Error("Subscription did not become ready before its deadline");
|
|
759
|
+
}
|
|
760
|
+
})();
|
|
761
|
+
return { ready };
|
|
762
|
+
}
|
|
763
|
+
async close() {
|
|
764
|
+
this.lifetime?.abort();
|
|
765
|
+
this.attempt?.abort();
|
|
766
|
+
this.removeAbort?.();
|
|
767
|
+
clearTimeout(this.retry);
|
|
768
|
+
this.retry = undefined;
|
|
769
|
+
clearInterval(this.periodic);
|
|
770
|
+
this.periodic = undefined;
|
|
771
|
+
for (const task of this.tasks.values())
|
|
772
|
+
task.pauseRetry();
|
|
773
|
+
const handle = this.connection;
|
|
774
|
+
this.connection = undefined;
|
|
775
|
+
if (handle) {
|
|
776
|
+
const outcome = await withDeadline(this.release(handle), this.timeout);
|
|
777
|
+
if (!outcome.settled)
|
|
778
|
+
this.report(new Error("Subscription shutdown deadline exceeded"));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function changeSubscriptionResource(config) {
|
|
783
|
+
const subscription = new ChangeSubscription(config);
|
|
784
|
+
return defineManagedResource({
|
|
785
|
+
id: config.id,
|
|
786
|
+
start: (context) => subscription.start(context),
|
|
787
|
+
close: () => subscription.close(),
|
|
788
|
+
force: () => subscription.close()
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
// src/internal/decision.ts
|
|
792
|
+
import { z as z3 } from "zod";
|
|
793
|
+
var PolicyDecisionSchema = z3.discriminatedUnion("outcome", [
|
|
794
|
+
z3.object({ outcome: z3.literal("allow") }).strict(),
|
|
795
|
+
z3.object({ outcome: z3.literal("deny"), reason: z3.string().min(1) }).strict(),
|
|
796
|
+
z3.object({ outcome: z3.literal("defer") }).strict()
|
|
492
797
|
]);
|
|
493
798
|
|
|
494
799
|
// src/application/decisions.ts
|
|
@@ -562,17 +867,17 @@ function createDecisionPipeline(policies, config) {
|
|
|
562
867
|
};
|
|
563
868
|
}
|
|
564
869
|
// src/application/events.ts
|
|
565
|
-
import { z as
|
|
566
|
-
var ApplicationLifecycleEventSchema =
|
|
567
|
-
type:
|
|
870
|
+
import { z as z4 } from "zod";
|
|
871
|
+
var ApplicationLifecycleEventSchema = z4.object({
|
|
872
|
+
type: z4.literal("application-state"),
|
|
568
873
|
applicationId: ApplicationIdSchema,
|
|
569
|
-
epoch:
|
|
570
|
-
revision:
|
|
874
|
+
epoch: z4.string().uuid(),
|
|
875
|
+
revision: z4.number().int().nonnegative(),
|
|
571
876
|
lifecycle: ApplicationLifecycleSchema,
|
|
572
877
|
health: ApplicationHealthSchema,
|
|
573
|
-
ready:
|
|
574
|
-
capturedAt:
|
|
575
|
-
resources:
|
|
878
|
+
ready: z4.boolean(),
|
|
879
|
+
capturedAt: z4.string().datetime({ offset: true }),
|
|
880
|
+
resources: z4.array(ManagedResourceSnapshotSchema).readonly()
|
|
576
881
|
}).strict().readonly();
|
|
577
882
|
function applicationLifecycleEvent(snapshot) {
|
|
578
883
|
return ApplicationLifecycleEventSchema.parse({
|
|
@@ -603,13 +908,13 @@ function createApplicationEventSink(config) {
|
|
|
603
908
|
};
|
|
604
909
|
}
|
|
605
910
|
// src/application/health.ts
|
|
606
|
-
import { z as
|
|
607
|
-
var ApplicationHealthHandlerOptionsSchema =
|
|
608
|
-
kind:
|
|
609
|
-
retryAfterSeconds:
|
|
911
|
+
import { z as z5 } from "zod";
|
|
912
|
+
var ApplicationHealthHandlerOptionsSchema = z5.object({
|
|
913
|
+
kind: z5.enum(["liveness", "readiness"]),
|
|
914
|
+
retryAfterSeconds: z5.number().int().nonnegative().default(5)
|
|
610
915
|
});
|
|
611
|
-
var ApplicationOperationalHandlersOptionsSchema =
|
|
612
|
-
retryAfterSeconds:
|
|
916
|
+
var ApplicationOperationalHandlersOptionsSchema = z5.object({
|
|
917
|
+
retryAfterSeconds: z5.number().int().nonnegative().default(5)
|
|
613
918
|
});
|
|
614
919
|
function createApplicationHealthHandler(application, options) {
|
|
615
920
|
const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
|
|
@@ -804,41 +1109,97 @@ function sqliteKeyspaceBackend(declaration, config) {
|
|
|
804
1109
|
}
|
|
805
1110
|
};
|
|
806
1111
|
}
|
|
807
|
-
// src/application/notification-
|
|
808
|
-
import { z as
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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();
|
|
1112
|
+
// src/application/notification-delivery.ts
|
|
1113
|
+
import { z as z6 } from "zod";
|
|
1114
|
+
var NotificationDeliveryStateSchema = z6.object({
|
|
1115
|
+
version: z6.string().min(1).max(128),
|
|
1116
|
+
actions: z6.array(z6.string().min(1).max(128)).min(1).max(128),
|
|
1117
|
+
receipts: z6.array(z6.object({ action: z6.string(), receipt: z6.json() }).strict()).max(128),
|
|
1118
|
+
projectedAt: z6.string().datetime({ offset: true }).optional()
|
|
1119
|
+
}).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");
|
|
1120
|
+
function notificationDeliveryPlan(config, payload) {
|
|
1121
|
+
return NotificationDeliveryStateSchema.parse({
|
|
1122
|
+
version: config.version,
|
|
1123
|
+
actions: [...config.actions(payload)],
|
|
1124
|
+
receipts: []
|
|
1125
|
+
});
|
|
827
1126
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1127
|
+
async function deliverNotification(input) {
|
|
1128
|
+
const { item, config } = input;
|
|
1129
|
+
const plan = item.delivery;
|
|
1130
|
+
if (!plan || plan.version !== config.version)
|
|
1131
|
+
throw new Error("Notification action plan version does not match the executor");
|
|
1132
|
+
const owns = (current2) => current2.key === item.key && current2.leaseId === item.leaseId;
|
|
1133
|
+
const checkpoint = async (change) => input.store.update((state) => {
|
|
1134
|
+
if (!state)
|
|
1135
|
+
throw new Error("Outbox state disappeared");
|
|
1136
|
+
const live2 = state.queue.find(owns);
|
|
1137
|
+
if (!live2?.delivery)
|
|
1138
|
+
return { state, result: false };
|
|
1139
|
+
const delivery = NotificationDeliveryStateSchema.parse(change(live2.delivery));
|
|
1140
|
+
return {
|
|
1141
|
+
state: input.bounded({
|
|
1142
|
+
...state,
|
|
1143
|
+
queue: state.queue.map((row) => row === live2 ? {
|
|
1144
|
+
...row,
|
|
1145
|
+
delivery,
|
|
1146
|
+
leaseUntil: new Date(input.clock().getTime() + input.leaseMs).toISOString()
|
|
1147
|
+
} : row)
|
|
1148
|
+
}),
|
|
1149
|
+
result: true
|
|
1150
|
+
};
|
|
1151
|
+
});
|
|
1152
|
+
const notification = { key: item.key, payload: item.payload, attempt: item.attempts };
|
|
1153
|
+
for (const action of plan.actions) {
|
|
1154
|
+
if (!input.active())
|
|
1155
|
+
return false;
|
|
1156
|
+
if (plan.receipts.some((receipt2) => receipt2.action === action))
|
|
1157
|
+
continue;
|
|
1158
|
+
if (!await checkpoint((value) => value))
|
|
1159
|
+
return false;
|
|
1160
|
+
const result = await input.send({
|
|
1161
|
+
...notification,
|
|
1162
|
+
action,
|
|
1163
|
+
idempotencyKey: JSON.stringify([item.key, plan.version, action])
|
|
1164
|
+
});
|
|
1165
|
+
const receipt = z6.json().parse(result ?? null);
|
|
1166
|
+
if (!await checkpoint((value) => ({
|
|
1167
|
+
...value,
|
|
1168
|
+
receipts: [...value.receipts, { action, receipt }]
|
|
1169
|
+
})))
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
if (!input.active())
|
|
1173
|
+
return false;
|
|
1174
|
+
const current = await input.store.read();
|
|
1175
|
+
const live = current?.queue.find(owns);
|
|
1176
|
+
if (!live?.delivery)
|
|
1177
|
+
return false;
|
|
1178
|
+
if (live.delivery.projectedAt === undefined) {
|
|
1179
|
+
await config.project(notification, live.delivery.receipts);
|
|
1180
|
+
if (!await checkpoint((value) => ({ ...value, projectedAt: input.clock().toISOString() })))
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
// src/application/notification-outbox.ts
|
|
1186
|
+
import { z as z8 } from "zod";
|
|
1187
|
+
|
|
1188
|
+
// src/application/notification-outbox-state.ts
|
|
1189
|
+
import { z as z7 } from "zod";
|
|
833
1190
|
var sizeOf = (value) => measureSize(value).responseBytes;
|
|
834
|
-
var DEFAULT_OUTBOX_BACKOFF = {
|
|
1191
|
+
var DEFAULT_OUTBOX_BACKOFF = {
|
|
1192
|
+
minDelayMs: 1000,
|
|
1193
|
+
maxDelayMs: 60000,
|
|
1194
|
+
jitter: 0
|
|
1195
|
+
};
|
|
835
1196
|
function resolveOutboxLimits(config) {
|
|
836
|
-
const pollIntervalMs =
|
|
837
|
-
const leaseMs =
|
|
838
|
-
const maxAttempts =
|
|
839
|
-
const maxQueue =
|
|
840
|
-
const maxStateBytes =
|
|
841
|
-
const retainReceipts =
|
|
1197
|
+
const pollIntervalMs = z7.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
|
|
1198
|
+
const leaseMs = z7.number().int().min(100).parse(config.leaseMs ?? 30000);
|
|
1199
|
+
const maxAttempts = z7.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
|
|
1200
|
+
const maxQueue = z7.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
|
|
1201
|
+
const maxStateBytes = z7.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
|
|
1202
|
+
const retainReceipts = z7.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
|
|
842
1203
|
return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
|
|
843
1204
|
}
|
|
844
1205
|
function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
|
|
@@ -863,7 +1224,7 @@ function dropReason(classification, attempts, maxAttempts) {
|
|
|
863
1224
|
return "terminal";
|
|
864
1225
|
return attempts >= maxAttempts ? "attempt-limit" : null;
|
|
865
1226
|
}
|
|
866
|
-
var RetryDelaySchema =
|
|
1227
|
+
var RetryDelaySchema = z7.number().nonnegative().max(24 * 60 * 60 * 1000);
|
|
867
1228
|
var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
|
|
868
1229
|
function claimDue(queue, now, ownerId, leaseMs) {
|
|
869
1230
|
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 +1238,80 @@ function claimDue(queue, now, ownerId, leaseMs) {
|
|
|
877
1238
|
leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
|
|
878
1239
|
};
|
|
879
1240
|
}
|
|
1241
|
+
|
|
1242
|
+
// src/application/notification-outbox.ts
|
|
1243
|
+
function notificationOutboxStateSchema(payload) {
|
|
1244
|
+
return z8.object({
|
|
1245
|
+
schemaVersion: z8.literal(1),
|
|
1246
|
+
queue: z8.array(z8.object({
|
|
1247
|
+
key: z8.string().min(1).max(512),
|
|
1248
|
+
payload,
|
|
1249
|
+
delivery: NotificationDeliveryStateSchema.optional(),
|
|
1250
|
+
createdAt: z8.string().datetime({ offset: true }),
|
|
1251
|
+
attempts: z8.number().int().nonnegative(),
|
|
1252
|
+
nextAttemptAt: z8.string().datetime({ offset: true }),
|
|
1253
|
+
leaseOwner: z8.string().min(1).max(128).nullable(),
|
|
1254
|
+
leaseId: z8.string().min(1).max(128).nullable(),
|
|
1255
|
+
leaseUntil: z8.string().datetime({ offset: true }).nullable()
|
|
1256
|
+
}).strict()),
|
|
1257
|
+
receipts: z8.array(z8.object({
|
|
1258
|
+
key: z8.string().min(1).max(512),
|
|
1259
|
+
completedAt: z8.string().datetime({ offset: true }),
|
|
1260
|
+
delivery: NotificationDeliveryStateSchema.optional()
|
|
1261
|
+
}).strict())
|
|
1262
|
+
}).strict();
|
|
1263
|
+
}
|
|
1264
|
+
var emptyOutboxState = () => ({
|
|
1265
|
+
schemaVersion: 1,
|
|
1266
|
+
queue: [],
|
|
1267
|
+
receipts: []
|
|
1268
|
+
});
|
|
880
1269
|
async function reportOutboxError(config, error) {
|
|
881
1270
|
try {
|
|
882
1271
|
await config.onError?.(error);
|
|
883
1272
|
} catch {}
|
|
884
1273
|
}
|
|
1274
|
+
function notificationRejection(config, parse, bounded, clock, backoffMs) {
|
|
1275
|
+
const maxAttempts = config.maxAttempts ?? 100;
|
|
1276
|
+
return async (claimed, error) => {
|
|
1277
|
+
const classification = await config.classify(error);
|
|
1278
|
+
return config.store.update((current) => {
|
|
1279
|
+
const state = parse(current);
|
|
1280
|
+
const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
|
|
1281
|
+
if (!live)
|
|
1282
|
+
return { state, result: null };
|
|
1283
|
+
const attempts = live.attempts;
|
|
1284
|
+
const reason = dropReason(classification, attempts, maxAttempts);
|
|
1285
|
+
if (reason) {
|
|
1286
|
+
return {
|
|
1287
|
+
state: bounded({
|
|
1288
|
+
...state,
|
|
1289
|
+
queue: state.queue.filter((item) => item.key !== live.key)
|
|
1290
|
+
}),
|
|
1291
|
+
result: { item: { ...live, attempts }, error, reason }
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const wait = RetryDelaySchema.parse(backoffMs(attempts));
|
|
1295
|
+
const retry = {
|
|
1296
|
+
...live,
|
|
1297
|
+
nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
|
|
1298
|
+
...NO_LEASE
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
state: bounded({
|
|
1302
|
+
...state,
|
|
1303
|
+
queue: state.queue.map((item) => item.key === live.key ? retry : item)
|
|
1304
|
+
}),
|
|
1305
|
+
result: null
|
|
1306
|
+
};
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
885
1310
|
function createNotificationOutbox(config) {
|
|
886
1311
|
const schema = notificationOutboxStateSchema(config.payloadSchema);
|
|
887
1312
|
const clock = config.clock ?? (() => new Date);
|
|
888
1313
|
const ownerId = config.ownerId ?? crypto.randomUUID();
|
|
889
|
-
const { pollIntervalMs, leaseMs,
|
|
1314
|
+
const { pollIntervalMs, leaseMs, maxQueue, maxStateBytes, retainReceipts } = resolveOutboxLimits(config);
|
|
890
1315
|
const backoffMs = config.backoffMs ?? ((attempt) => backoffDelay(DEFAULT_OUTBOX_BACKOFF, attempt));
|
|
891
1316
|
let timer;
|
|
892
1317
|
let running = false;
|
|
@@ -895,7 +1320,7 @@ function createNotificationOutbox(config) {
|
|
|
895
1320
|
const bounded = (input) => boundedOutboxState(schema.parse(input), maxQueue, retainReceipts, maxStateBytes);
|
|
896
1321
|
const parse = (state) => schema.parse(state ?? emptyOutboxState());
|
|
897
1322
|
const enqueue = async (input) => {
|
|
898
|
-
const key =
|
|
1323
|
+
const key = z8.string().min(1).max(512).parse(input.key);
|
|
899
1324
|
const payload = config.payloadSchema.parse(input.payload);
|
|
900
1325
|
const supersedes = new Set(input.supersedes ?? []);
|
|
901
1326
|
return config.store.update((current) => {
|
|
@@ -904,7 +1329,16 @@ function createNotificationOutbox(config) {
|
|
|
904
1329
|
return { state, result: false };
|
|
905
1330
|
}
|
|
906
1331
|
const now = clock().toISOString();
|
|
907
|
-
const fresh = {
|
|
1332
|
+
const fresh = {
|
|
1333
|
+
key,
|
|
1334
|
+
payload,
|
|
1335
|
+
createdAt: now,
|
|
1336
|
+
attempts: 0,
|
|
1337
|
+
nextAttemptAt: now,
|
|
1338
|
+
...config.delivery && {
|
|
1339
|
+
delivery: notificationDeliveryPlan(config.delivery, payload)
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
908
1342
|
const queue = [
|
|
909
1343
|
...state.queue.filter((item) => !supersedes.has(item.key)),
|
|
910
1344
|
{ ...fresh, ...NO_LEASE }
|
|
@@ -932,11 +1366,15 @@ function createNotificationOutbox(config) {
|
|
|
932
1366
|
const acknowledge = async (claimed) => {
|
|
933
1367
|
await config.store.update((current) => {
|
|
934
1368
|
const state = parse(current);
|
|
935
|
-
const
|
|
936
|
-
if (!
|
|
1369
|
+
const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
|
|
1370
|
+
if (!live)
|
|
937
1371
|
return { state, result: undefined };
|
|
938
1372
|
const receipts = [
|
|
939
|
-
{
|
|
1373
|
+
{
|
|
1374
|
+
key: claimed.key,
|
|
1375
|
+
completedAt: clock().toISOString(),
|
|
1376
|
+
...live.delivery && { delivery: live.delivery }
|
|
1377
|
+
},
|
|
940
1378
|
...state.receipts.filter((receipt) => receipt.key !== claimed.key)
|
|
941
1379
|
].slice(0, retainReceipts);
|
|
942
1380
|
return {
|
|
@@ -949,39 +1387,7 @@ function createNotificationOutbox(config) {
|
|
|
949
1387
|
};
|
|
950
1388
|
});
|
|
951
1389
|
};
|
|
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
|
-
};
|
|
1390
|
+
const reject = notificationRejection(config, parse, bounded, clock, backoffMs);
|
|
985
1391
|
const flushPass = async () => {
|
|
986
1392
|
let delivered = 0;
|
|
987
1393
|
while (!interrupted) {
|
|
@@ -989,11 +1395,28 @@ function createNotificationOutbox(config) {
|
|
|
989
1395
|
if (!claimed)
|
|
990
1396
|
return delivered;
|
|
991
1397
|
try {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1398
|
+
if (config.delivery) {
|
|
1399
|
+
const finished = await deliverNotification({
|
|
1400
|
+
item: claimed,
|
|
1401
|
+
store: config.store,
|
|
1402
|
+
config: config.delivery,
|
|
1403
|
+
send: config.send,
|
|
1404
|
+
clock,
|
|
1405
|
+
bounded,
|
|
1406
|
+
leaseMs,
|
|
1407
|
+
active: () => !interrupted
|
|
1408
|
+
});
|
|
1409
|
+
if (!finished)
|
|
1410
|
+
return delivered;
|
|
1411
|
+
} else {
|
|
1412
|
+
if (claimed.delivery)
|
|
1413
|
+
throw new Error("An action-plan notification requires its delivery executor");
|
|
1414
|
+
await config.send({
|
|
1415
|
+
key: claimed.key,
|
|
1416
|
+
payload: claimed.payload,
|
|
1417
|
+
attempt: claimed.attempts
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
997
1420
|
await acknowledge(claimed);
|
|
998
1421
|
delivered += 1;
|
|
999
1422
|
} catch (error) {
|
|
@@ -1072,11 +1495,11 @@ function notificationOutboxResource(outbox, config = {}) {
|
|
|
1072
1495
|
});
|
|
1073
1496
|
}
|
|
1074
1497
|
// src/application/process-lifecycle.ts
|
|
1075
|
-
import { z as
|
|
1498
|
+
import { z as z10 } from "zod";
|
|
1076
1499
|
|
|
1077
1500
|
// src/application/process-lifecycle-transitions.ts
|
|
1078
|
-
import { z as
|
|
1079
|
-
var LifecycleTerminationSchema =
|
|
1501
|
+
import { z as z9 } from "zod";
|
|
1502
|
+
var LifecycleTerminationSchema = z9.enum([
|
|
1080
1503
|
"active",
|
|
1081
1504
|
"draining",
|
|
1082
1505
|
"clean",
|
|
@@ -1085,22 +1508,22 @@ var LifecycleTerminationSchema = z6.enum([
|
|
|
1085
1508
|
"hot-reload",
|
|
1086
1509
|
"abnormal"
|
|
1087
1510
|
]);
|
|
1088
|
-
var timestamp =
|
|
1089
|
-
var LifecycleRunSchema =
|
|
1090
|
-
runId:
|
|
1091
|
-
pid:
|
|
1092
|
-
version:
|
|
1511
|
+
var timestamp = z9.string().datetime({ offset: true });
|
|
1512
|
+
var LifecycleRunSchema = z9.object({
|
|
1513
|
+
runId: z9.string().min(1).max(128),
|
|
1514
|
+
pid: z9.number().int().positive(),
|
|
1515
|
+
version: z9.string().min(1).max(256),
|
|
1093
1516
|
startedAt: timestamp,
|
|
1094
1517
|
readyAt: timestamp.nullable(),
|
|
1095
1518
|
unavailableAt: timestamp.nullable().default(null),
|
|
1096
1519
|
stoppedAt: timestamp.nullable(),
|
|
1097
1520
|
termination: LifecycleTerminationSchema
|
|
1098
1521
|
}).strict();
|
|
1099
|
-
var LifecycleStateSchema =
|
|
1100
|
-
schemaVersion:
|
|
1101
|
-
runs:
|
|
1522
|
+
var LifecycleStateSchema = z9.object({
|
|
1523
|
+
schemaVersion: z9.literal(1),
|
|
1524
|
+
runs: z9.array(LifecycleRunSchema)
|
|
1102
1525
|
}).strict();
|
|
1103
|
-
var PreviousExitSchema =
|
|
1526
|
+
var PreviousExitSchema = z9.enum([
|
|
1104
1527
|
"first-boot",
|
|
1105
1528
|
"hot-reload",
|
|
1106
1529
|
"clean",
|
|
@@ -1127,7 +1550,7 @@ var UNKNOWN_VERSION = "unknown";
|
|
|
1127
1550
|
function isVersionChange(previous, next) {
|
|
1128
1551
|
return previous !== UNKNOWN_VERSION && next !== UNKNOWN_VERSION && previous !== next;
|
|
1129
1552
|
}
|
|
1130
|
-
var RetainSchema =
|
|
1553
|
+
var RetainSchema = z9.number().int().min(1).max(1000);
|
|
1131
1554
|
function previousExitOf(previous, run, overlap) {
|
|
1132
1555
|
if (!previous)
|
|
1133
1556
|
return "first-boot";
|
|
@@ -1315,8 +1738,8 @@ function transitionProcessShutdown(source, input) {
|
|
|
1315
1738
|
// src/application/process-lifecycle.ts
|
|
1316
1739
|
function createProcessLifecycleLedger(config) {
|
|
1317
1740
|
const clock = config.clock ?? (() => new Date);
|
|
1318
|
-
const pid =
|
|
1319
|
-
const retain =
|
|
1741
|
+
const pid = z10.number().int().positive().parse(config.pid ?? process.pid);
|
|
1742
|
+
const retain = z10.number().int().min(1).max(1000).parse(config.retain ?? 20);
|
|
1320
1743
|
const configuredRunId = config.runId;
|
|
1321
1744
|
const nextRunId = typeof configuredRunId === "function" ? configuredRunId : configuredRunId === undefined ? () => crypto.randomUUID() : () => configuredRunId;
|
|
1322
1745
|
let runId = nextRunId();
|
|
@@ -1441,27 +1864,27 @@ function lifecycleLedgerResource(ledger, config) {
|
|
|
1441
1864
|
});
|
|
1442
1865
|
}
|
|
1443
1866
|
// src/application/revision-signal.ts
|
|
1444
|
-
import { z as
|
|
1445
|
-
var NonNegativeSafeIntegerSchema =
|
|
1446
|
-
var PositiveSafeIntegerSchema2 =
|
|
1447
|
-
var TimerDelaySchema =
|
|
1448
|
-
var RevisionSignalStateSchema =
|
|
1449
|
-
var RevisionSignalWaitOutcomeSchema =
|
|
1867
|
+
import { z as z11 } from "zod";
|
|
1868
|
+
var NonNegativeSafeIntegerSchema = z11.number().int().nonnegative().safe();
|
|
1869
|
+
var PositiveSafeIntegerSchema2 = z11.number().int().positive().safe();
|
|
1870
|
+
var TimerDelaySchema = z11.number().int().positive().max(2147483647);
|
|
1871
|
+
var RevisionSignalStateSchema = z11.enum(["open", "closed"]);
|
|
1872
|
+
var RevisionSignalWaitOutcomeSchema = z11.enum([
|
|
1450
1873
|
"changed",
|
|
1451
1874
|
"timed-out",
|
|
1452
1875
|
"aborted",
|
|
1453
1876
|
"closed",
|
|
1454
1877
|
"capacity"
|
|
1455
1878
|
]);
|
|
1456
|
-
var RevisionSignalWaitResultSchema =
|
|
1879
|
+
var RevisionSignalWaitResultSchema = z11.object({
|
|
1457
1880
|
outcome: RevisionSignalWaitOutcomeSchema,
|
|
1458
1881
|
revision: NonNegativeSafeIntegerSchema
|
|
1459
1882
|
}).strict().readonly();
|
|
1460
|
-
var RevisionSignalAdvanceResultSchema =
|
|
1461
|
-
outcome:
|
|
1883
|
+
var RevisionSignalAdvanceResultSchema = z11.object({
|
|
1884
|
+
outcome: z11.enum(["advanced", "closed"]),
|
|
1462
1885
|
revision: NonNegativeSafeIntegerSchema
|
|
1463
1886
|
}).strict().readonly();
|
|
1464
|
-
var RevisionSignalSnapshotSchema =
|
|
1887
|
+
var RevisionSignalSnapshotSchema = z11.object({
|
|
1465
1888
|
state: RevisionSignalStateSchema,
|
|
1466
1889
|
revision: NonNegativeSafeIntegerSchema,
|
|
1467
1890
|
pending: NonNegativeSafeIntegerSchema,
|
|
@@ -1678,9 +2101,23 @@ class WatchHubCore {
|
|
|
1678
2101
|
same;
|
|
1679
2102
|
reads = 0;
|
|
1680
2103
|
closed = false;
|
|
2104
|
+
periodic;
|
|
1681
2105
|
constructor(config) {
|
|
1682
2106
|
this.config = config;
|
|
1683
2107
|
this.maxWatches = config.maxWatchesPerSubscriber ?? 64;
|
|
2108
|
+
for (const bound of [config.maxSources ?? 1024, config.maxSubscribers ?? 1024]) {
|
|
2109
|
+
if (!Number.isInteger(bound) || bound < 1)
|
|
2110
|
+
throw new RangeError("Watch capacity must be positive");
|
|
2111
|
+
}
|
|
2112
|
+
if (config.reconcileIntervalMs !== undefined) {
|
|
2113
|
+
if (!Number.isInteger(config.reconcileIntervalMs) || config.reconcileIntervalMs < 10 || config.reconcileIntervalMs > 3600000)
|
|
2114
|
+
throw new RangeError("Invalid reconciliation interval");
|
|
2115
|
+
this.periodic = setInterval(() => {
|
|
2116
|
+
for (const source of this.sources.values())
|
|
2117
|
+
source.task.trigger();
|
|
2118
|
+
}, config.reconcileIntervalMs);
|
|
2119
|
+
this.periodic.unref?.();
|
|
2120
|
+
}
|
|
1684
2121
|
this.holdMs = config.holdMs ?? 0;
|
|
1685
2122
|
this.deltaMemoryBytes = config.deltaMemoryBytes ?? 262144;
|
|
1686
2123
|
this.same = config.same ?? ((previous, next) => serializeCanonicalJson(previous) === serializeCanonicalJson(next));
|
|
@@ -1783,58 +2220,35 @@ class WatchHubCore {
|
|
|
1783
2220
|
for (const subscriber of source.subscribers)
|
|
1784
2221
|
this.deliver(source, subscriber);
|
|
1785
2222
|
}
|
|
1786
|
-
async
|
|
1787
|
-
|
|
2223
|
+
async readSource(source) {
|
|
2224
|
+
this.reads += 1;
|
|
2225
|
+
const value = await this.config.read(source.operation, source.args, source.scope);
|
|
2226
|
+
if (this.closed || this.sources.get(source.id) !== source)
|
|
1788
2227
|
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
|
-
}
|
|
2228
|
+
const signature = serializeCanonicalJson(value);
|
|
2229
|
+
const unchanged = source.signature !== undefined && this.same(source.value, value);
|
|
2230
|
+
source.signature = signature;
|
|
2231
|
+
if (!unchanged)
|
|
2232
|
+
this.publish(source, value, valueFingerprint(value));
|
|
2233
|
+
if (source.state.phase !== "live")
|
|
2234
|
+
this.announceState(source, { key: source.key, phase: "live" });
|
|
1825
2235
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
source
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2236
|
+
readFailed(source, error) {
|
|
2237
|
+
const code = readErrorCode(error);
|
|
2238
|
+
this.announceState(source, {
|
|
2239
|
+
key: source.key,
|
|
2240
|
+
phase: "unavailable",
|
|
2241
|
+
reason: "source-error",
|
|
2242
|
+
...code !== undefined && { code },
|
|
2243
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2244
|
+
});
|
|
2245
|
+
this.config.logger?.warn?.("[stitchkit] watched read failed", {
|
|
2246
|
+
key: watchKeyString(source.key),
|
|
2247
|
+
error
|
|
2248
|
+
});
|
|
1835
2249
|
}
|
|
1836
|
-
acquire(operation, key, args) {
|
|
1837
|
-
const id = watchKeyString(key);
|
|
2250
|
+
acquire(operation, key, args, scope) {
|
|
2251
|
+
const id = JSON.stringify([scope ?? null, watchKeyString(key)]);
|
|
1838
2252
|
const existing = this.sources.get(id);
|
|
1839
2253
|
if (existing) {
|
|
1840
2254
|
clearTimeout(existing.release);
|
|
@@ -1842,6 +2256,8 @@ class WatchHubCore {
|
|
|
1842
2256
|
return existing;
|
|
1843
2257
|
}
|
|
1844
2258
|
const source = {
|
|
2259
|
+
id,
|
|
2260
|
+
scope,
|
|
1845
2261
|
key,
|
|
1846
2262
|
operation,
|
|
1847
2263
|
args,
|
|
@@ -1851,15 +2267,17 @@ class WatchHubCore {
|
|
|
1851
2267
|
history: new Map,
|
|
1852
2268
|
historyBytes: 0,
|
|
1853
2269
|
baselines: new Map,
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2270
|
+
task: createCoalescedTask({
|
|
2271
|
+
run: () => this.readSource(source),
|
|
2272
|
+
active: () => !this.closed && source.subscribers.size > 0,
|
|
2273
|
+
onError: (error) => this.readFailed(source, error),
|
|
2274
|
+
backoff: this.config.backoff ?? DEFAULT_BACKOFF
|
|
2275
|
+
}),
|
|
2276
|
+
state: { key, phase: "opening" }
|
|
1858
2277
|
};
|
|
1859
2278
|
for (const topic of this.config.invalidatedBy(operation, args)) {
|
|
1860
2279
|
source.unsubscribes.push(this.config.subscribe(topic, () => {
|
|
1861
|
-
source.
|
|
1862
|
-
this.pump(source);
|
|
2280
|
+
source.task.trigger();
|
|
1863
2281
|
}));
|
|
1864
2282
|
}
|
|
1865
2283
|
this.sources.set(id, source);
|
|
@@ -1868,17 +2286,17 @@ class WatchHubCore {
|
|
|
1868
2286
|
release(source) {
|
|
1869
2287
|
if (source.subscribers.size > 0)
|
|
1870
2288
|
return;
|
|
1871
|
-
|
|
1872
|
-
source.retry = undefined;
|
|
2289
|
+
source.task.pauseRetry();
|
|
1873
2290
|
const drop = () => {
|
|
1874
|
-
if (source.
|
|
2291
|
+
if (source.task.running) {
|
|
1875
2292
|
source.release = setTimeout(drop, 10);
|
|
1876
2293
|
source.release.unref?.();
|
|
1877
2294
|
return;
|
|
1878
2295
|
}
|
|
2296
|
+
source.task.close();
|
|
1879
2297
|
for (const unsubscribe of source.unsubscribes)
|
|
1880
2298
|
unsubscribe();
|
|
1881
|
-
this.sources.delete(
|
|
2299
|
+
this.sources.delete(source.id);
|
|
1882
2300
|
};
|
|
1883
2301
|
if (this.holdMs === 0) {
|
|
1884
2302
|
drop();
|
|
@@ -1887,11 +2305,18 @@ class WatchHubCore {
|
|
|
1887
2305
|
source.release = setTimeout(drop, this.holdMs);
|
|
1888
2306
|
source.release.unref?.();
|
|
1889
2307
|
}
|
|
1890
|
-
attach(subscriber) {
|
|
2308
|
+
attach(subscriber, scope) {
|
|
2309
|
+
if (this.held.size >= (this.config.maxSubscribers ?? 1024))
|
|
2310
|
+
throw new Error("Watch subscriber capacity exceeded");
|
|
2311
|
+
if (scope && (!scope.key || scope.key.length > 512))
|
|
2312
|
+
throw new Error("Invalid watch admission scope");
|
|
1891
2313
|
const keys = new Set;
|
|
1892
2314
|
this.held.set(subscriber, keys);
|
|
1893
|
-
|
|
2315
|
+
let detached = false;
|
|
2316
|
+
const attached = {
|
|
1894
2317
|
open: (key, args, have) => {
|
|
2318
|
+
if (detached || scope?.signal.aborted)
|
|
2319
|
+
return { accepted: false, reason: "Watch scope ended" };
|
|
1895
2320
|
if (this.closed)
|
|
1896
2321
|
return { accepted: false, reason: "the watch hub is closed" };
|
|
1897
2322
|
const operation = { service: key.service, action: key.action };
|
|
@@ -1901,7 +2326,7 @@ class WatchHubCore {
|
|
|
1901
2326
|
reason: `${key.service}.${key.action} is not watchable`
|
|
1902
2327
|
};
|
|
1903
2328
|
}
|
|
1904
|
-
const id = watchKeyString(key);
|
|
2329
|
+
const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
|
|
1905
2330
|
if (keys.has(id))
|
|
1906
2331
|
return { accepted: true };
|
|
1907
2332
|
if (keys.size >= this.maxWatches) {
|
|
@@ -1910,7 +2335,9 @@ class WatchHubCore {
|
|
|
1910
2335
|
reason: `this connection is already watching ${keys.size} reads (limit ${this.maxWatches})`
|
|
1911
2336
|
};
|
|
1912
2337
|
}
|
|
1913
|
-
|
|
2338
|
+
if (!this.sources.has(id) && this.sources.size >= (this.config.maxSources ?? 1024))
|
|
2339
|
+
return { accepted: false, reason: "Watch source capacity exceeded" };
|
|
2340
|
+
const source = this.acquire(operation, key, args, scope?.key);
|
|
1914
2341
|
source.subscribers.add(subscriber);
|
|
1915
2342
|
keys.add(id);
|
|
1916
2343
|
if (have)
|
|
@@ -1932,11 +2359,12 @@ class WatchHubCore {
|
|
|
1932
2359
|
reason: error instanceof Error ? error.message : String(error)
|
|
1933
2360
|
};
|
|
1934
2361
|
}
|
|
1935
|
-
|
|
2362
|
+
if (!source.task.running && (source.signature === undefined || source.state.phase !== "live"))
|
|
2363
|
+
source.task.trigger();
|
|
1936
2364
|
return { accepted: true };
|
|
1937
2365
|
},
|
|
1938
2366
|
close: (key) => {
|
|
1939
|
-
const id = watchKeyString(key);
|
|
2367
|
+
const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
|
|
1940
2368
|
if (!keys.delete(id))
|
|
1941
2369
|
return;
|
|
1942
2370
|
const source = this.sources.get(id);
|
|
@@ -1947,6 +2375,8 @@ class WatchHubCore {
|
|
|
1947
2375
|
this.release(source);
|
|
1948
2376
|
},
|
|
1949
2377
|
detach: () => {
|
|
2378
|
+
detached = true;
|
|
2379
|
+
scope?.signal.removeEventListener("abort", onAbort);
|
|
1950
2380
|
for (const id of keys) {
|
|
1951
2381
|
const source = this.sources.get(id);
|
|
1952
2382
|
if (!source)
|
|
@@ -1959,20 +2389,28 @@ class WatchHubCore {
|
|
|
1959
2389
|
this.held.delete(subscriber);
|
|
1960
2390
|
}
|
|
1961
2391
|
};
|
|
2392
|
+
const onAbort = () => attached.detach();
|
|
2393
|
+
if (scope?.signal.aborted)
|
|
2394
|
+
attached.detach();
|
|
2395
|
+
else
|
|
2396
|
+
scope?.signal.addEventListener("abort", onAbort, { once: true });
|
|
2397
|
+
return attached;
|
|
1962
2398
|
}
|
|
1963
2399
|
close() {
|
|
1964
2400
|
this.closed = true;
|
|
2401
|
+
clearInterval(this.periodic);
|
|
1965
2402
|
const teardown = [...this.sources.values()];
|
|
1966
2403
|
this.sources.clear();
|
|
1967
2404
|
this.held.clear();
|
|
1968
2405
|
for (const source of teardown) {
|
|
1969
|
-
|
|
2406
|
+
source.task.close();
|
|
1970
2407
|
clearTimeout(source.release);
|
|
1971
2408
|
this.announceState(source, {
|
|
1972
2409
|
key: source.key,
|
|
1973
2410
|
phase: "unavailable",
|
|
1974
2411
|
reason: "source-unavailable"
|
|
1975
2412
|
});
|
|
2413
|
+
source.task.close();
|
|
1976
2414
|
for (const unsubscribe of source.unsubscribes)
|
|
1977
2415
|
unsubscribe();
|
|
1978
2416
|
}
|
|
@@ -2000,7 +2438,7 @@ function watchKey(operation, args) {
|
|
|
2000
2438
|
function createWatchHub(config) {
|
|
2001
2439
|
const hub = new WatchHubCore(config);
|
|
2002
2440
|
return {
|
|
2003
|
-
attach: (subscriber) => hub.attach(subscriber),
|
|
2441
|
+
attach: (subscriber, scope) => hub.attach(subscriber, scope),
|
|
2004
2442
|
readCount: () => hub.reads,
|
|
2005
2443
|
size: () => hub.sources.size,
|
|
2006
2444
|
close: () => hub.close()
|
|
@@ -2040,6 +2478,7 @@ export {
|
|
|
2040
2478
|
createApplicationEventSink,
|
|
2041
2479
|
createApplication,
|
|
2042
2480
|
createActivityProjection,
|
|
2481
|
+
changeSubscriptionResource,
|
|
2043
2482
|
applicationLifecycleEvent,
|
|
2044
2483
|
RevisionSignalWaitResultSchema,
|
|
2045
2484
|
RevisionSignalWaitOutcomeSchema,
|
|
@@ -2048,6 +2487,7 @@ export {
|
|
|
2048
2487
|
RevisionSignalAdvanceResultSchema,
|
|
2049
2488
|
PreviousExitSchema,
|
|
2050
2489
|
PolicyDecisionSchema,
|
|
2490
|
+
NotificationDeliveryStateSchema,
|
|
2051
2491
|
ManagedScheduleStatusSchema,
|
|
2052
2492
|
ManagedScheduleOverlapSchema,
|
|
2053
2493
|
ManagedScheduleErrorPolicySchema,
|