stitchkit 0.95.0 → 0.95.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 CHANGED
@@ -15,6 +15,23 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.95.1] — 2026-09-23
19
+
20
+ ### Added
21
+
22
+ - `stitchkit/application/directory-inbox` (new, server, evolving) —
23
+ **`createDirectoryInbox({ id, directory, schema, handle })`**, with its
24
+ contract (`DirectoryInboxConfig`, `DirectoryInboxStateSchema`, …) in
25
+ `stitchkit/application`: a managed resource that delivers each `<name>.json` entry
26
+ another program drops into a directory to the application at least once,
27
+ after readiness. Taken and done are separate durable records — a claim with a
28
+ lease before `handle`, a receipt after it and only then the file removed — so
29
+ a restart neither loses an entry nor repeats a finished one. A throwing
30
+ handler is retried with a backoff; an entry that fails its schema, is too
31
+ large or used up `maxAttempts` is moved to `rejected/` with its reason and
32
+ the queue keeps flowing. `stopAdmission` stops taking entries, `force` aborts
33
+ the delivery in flight. A consuming project wrote this receiver by hand.
34
+
18
35
  ## [0.95.0] — 2026-09-23
19
36
 
20
37
  ### ⚠️ Breaking changes
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import type { ManagedResource, ManagedResourceDependency } from './resource.js';
3
+ import type { StateStore } from './state-store.js';
4
+ export declare const DirectoryInboxRejectionReasonSchema: z.ZodEnum<{
5
+ "attempt-limit": "attempt-limit";
6
+ invalid: "invalid";
7
+ "too-large": "too-large";
8
+ }>;
9
+ export type DirectoryInboxRejectionReason = z.infer<typeof DirectoryInboxRejectionReasonSchema>;
10
+ export declare const DirectoryInboxStateSchema: z.ZodObject<{
11
+ schemaVersion: z.ZodLiteral<1>;
12
+ claims: z.ZodArray<z.ZodObject<{
13
+ key: z.ZodString;
14
+ attempts: z.ZodNumber;
15
+ nextAttemptAt: z.ZodString;
16
+ leaseId: z.ZodNullable<z.ZodString>;
17
+ leaseUntil: z.ZodNullable<z.ZodString>;
18
+ }, z.core.$strict>>;
19
+ receipts: z.ZodArray<z.ZodObject<{
20
+ key: z.ZodString;
21
+ completedAt: z.ZodString;
22
+ }, z.core.$strict>>;
23
+ rejected: z.ZodArray<z.ZodObject<{
24
+ key: z.ZodString;
25
+ reason: z.ZodEnum<{
26
+ "attempt-limit": "attempt-limit";
27
+ invalid: "invalid";
28
+ "too-large": "too-large";
29
+ }>;
30
+ detail: z.ZodString;
31
+ rejectedAt: z.ZodString;
32
+ }, z.core.$strict>>;
33
+ }, z.core.$strict>;
34
+ export type DirectoryInboxState = z.infer<typeof DirectoryInboxStateSchema>;
35
+ export type DirectoryInboxClaim = DirectoryInboxState['claims'][number];
36
+ export type DirectoryInboxRejection = DirectoryInboxState['rejected'][number];
37
+ export declare const emptyDirectoryInboxState: () => DirectoryInboxState;
38
+ /**
39
+ * A directory another program drops entries into, handed to the application
40
+ * at least once each. The producer writes each entry atomically as a
41
+ * `<name>.json` file (a temporary name, then a rename); names starting with a
42
+ * dot are never read, which is where the inbox keeps its own state.
43
+ */
44
+ export interface DirectoryInboxDelivery<TEntry> {
45
+ /** The entry's file name — stable across retries and restarts. */
46
+ readonly key: string;
47
+ readonly entry: TEntry;
48
+ /** 1 on the first delivery of this entry. */
49
+ readonly attempt: number;
50
+ /** Aborted when the application is forced down mid-delivery. */
51
+ readonly signal: AbortSignal;
52
+ }
53
+ export interface DirectoryInboxConfig<TEntry> {
54
+ readonly id: string;
55
+ readonly directory: string;
56
+ readonly schema: z.ZodType<TEntry>;
57
+ /** Throwing leaves the entry for a later attempt. */
58
+ readonly handle: (delivery: DirectoryInboxDelivery<TEntry>) => void | Promise<void>;
59
+ readonly dependsOn?: readonly ManagedResourceDependency[];
60
+ /** Default: `<directory>/.inbox-state.json`, locked across processes. */
61
+ readonly store?: StateStore<DirectoryInboxState>;
62
+ readonly clock?: () => Date;
63
+ /** How often the directory is read while the application runs. Default 1 s. */
64
+ readonly pollIntervalMs?: number;
65
+ /** How long a taken entry is someone's before it may be taken again. Default 5 min. */
66
+ readonly leaseMs?: number;
67
+ /** Deliveries of one entry before it is set aside. Default 20. */
68
+ readonly maxAttempts?: number;
69
+ /** Larger entries are set aside unread. Default 1 MiB. */
70
+ readonly maxEntryBytes?: number;
71
+ /** Receipts and rejections kept. Default 1 000. */
72
+ readonly retain?: number;
73
+ /** An entry was set aside into `<directory>/rejected/`, with the reason. */
74
+ readonly onRejected?: (rejection: DirectoryInboxRejection) => void | Promise<void>;
75
+ /**
76
+ * A delivery failed (the entry is retried after a backoff) or a whole pass
77
+ * did (storage, the directory; the next poll tries again).
78
+ */
79
+ readonly onError?: (error: unknown) => void | Promise<void>;
80
+ }
81
+ export interface DirectoryInbox {
82
+ /** Deliver every entry due now; resolves with how many were handled. */
83
+ flush(): Promise<number>;
84
+ state(): Promise<DirectoryInboxState>;
85
+ }
86
+ export interface DirectoryInboxResource extends ManagedResource {
87
+ start(): Promise<{
88
+ readonly value: DirectoryInbox;
89
+ }>;
90
+ }
91
+ /** What the next step does with one entry of the directory. */
92
+ export type InboxStep = {
93
+ readonly kind: 'skip';
94
+ } | {
95
+ readonly kind: 'remove';
96
+ } | {
97
+ readonly kind: 'set-aside';
98
+ } | {
99
+ readonly kind: 'reject';
100
+ readonly rejection: DirectoryInboxRejection;
101
+ } | {
102
+ readonly kind: 'deliver';
103
+ readonly claim: DirectoryInboxClaim;
104
+ };
105
+ interface StepInput {
106
+ readonly key: string;
107
+ readonly now: Date;
108
+ readonly leaseMs: number;
109
+ readonly maxAttempts: number;
110
+ readonly retain: number;
111
+ }
112
+ export declare function withRejection(state: DirectoryInboxState, rejection: DirectoryInboxRejection, retain: number): DirectoryInboxState;
113
+ /**
114
+ * Decide one entry, and take it when it is due. A claim whose attempts are
115
+ * spent is not taken again: an entry whose handling kills the process would
116
+ * otherwise be retried on every restart, forever.
117
+ */
118
+ export declare function claimEntry(state: DirectoryInboxState, input: StepInput): {
119
+ readonly state: DirectoryInboxState;
120
+ readonly step: InboxStep;
121
+ };
122
+ /** The entry was handled: its receipt replaces its claim. */
123
+ export declare function completeEntry(state: DirectoryInboxState, entryKey: string, now: Date, retain: number): DirectoryInboxState;
124
+ /** The handler refused: wait, then take it again — unless it is no longer ours. */
125
+ export declare function releaseEntry(state: DirectoryInboxState, claim: DirectoryInboxClaim, retryAt: Date): DirectoryInboxState;
126
+ /** Records about entries that are gone from the directory, except a live lease. */
127
+ export declare function forgetMissing(state: DirectoryInboxState, present: ReadonlySet<string>, now: Date): DirectoryInboxState;
128
+ export {};
129
+ //# sourceMappingURL=directory-inbox-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-inbox-contract.d.ts","sourceRoot":"","sources":["../../src/application/directory-inbox-contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { type DirectoryInboxConfig, type DirectoryInboxResource } from './directory-inbox-contract.js';
2
+ export declare function createDirectoryInbox<TEntry>(config: DirectoryInboxConfig<TEntry>): DirectoryInboxResource;
3
+ //# sourceMappingURL=directory-inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-inbox.d.ts","sourceRoot":"","sources":["../../src/application/directory-inbox.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,oBAAoB,EAEzB,KAAK,sBAAsB,EAO5B,MAAM,4BAA4B,CAAC;AAyEpC,wBAAgB,oBAAoB,CAAC,MAAM,EACzC,MAAM,EAAE,oBAAoB,CAAC,MAAM,CAAC,GACnC,sBAAsB,CAiKxB"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The directory inbox — delivers each entry another program drops into a
3
+ * directory to the application at least once.
4
+ *
5
+ * Its own entrypoint because it reads, renames and removes files (`node:fs`),
6
+ * and `stitchkit/application` must stay importable in a browser. Its contract —
7
+ * the configuration, the delivery, the state and rejection schemas — stays in
8
+ * `stitchkit/application`, as the diagnostic journal's does. → ADR 0156.
9
+ */
10
+ export { createDirectoryInbox } from '../../application/directory-inbox.js';
11
+ //# sourceMappingURL=directory-inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-inbox.d.ts","sourceRoot":"","sources":["../../../src/entrypoints/application/directory-inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC"}
@@ -0,0 +1,221 @@
1
+ import {
2
+ DirectoryInboxStateSchema,
3
+ claimEntry,
4
+ completeEntry,
5
+ emptyDirectoryInboxState,
6
+ forgetMissing,
7
+ releaseEntry,
8
+ withRejection
9
+ } from "../../index-5jrd0yng.js";
10
+ import {
11
+ defineManagedResource
12
+ } from "../../index-2k4yrqkc.js";
13
+ import {
14
+ createFileStateStore
15
+ } from "../../index-tm88avnj.js";
16
+ import {
17
+ backoffDelay
18
+ } from "../../index-ngfhvfpd.js";
19
+ import"../../index-hffjpmq9.js";
20
+
21
+ // src/application/directory-inbox.ts
22
+ import { mkdir, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
23
+ import { join } from "node:path";
24
+ import { z } from "zod";
25
+ var REJECTED = "rejected";
26
+ var ENTRY_NAME = /^[^.].*\.json$/;
27
+ var RETRY_BACKOFF = { minDelayMs: 1000, maxDelayMs: 300000, jitter: 0 };
28
+ var absent = (error) => error instanceof Error && ("code" in error) && error.code === "ENOENT";
29
+ async function quietly(run) {
30
+ try {
31
+ await run();
32
+ } catch (error) {
33
+ if (!absent(error))
34
+ throw error;
35
+ }
36
+ }
37
+ function resolveLimits(config) {
38
+ const integer = z.number().int();
39
+ return {
40
+ pollIntervalMs: integer.min(10).parse(config.pollIntervalMs ?? 1000),
41
+ leaseMs: integer.min(100).parse(config.leaseMs ?? 300000),
42
+ maxAttempts: integer.min(1).max(1e4).parse(config.maxAttempts ?? 20),
43
+ maxEntryBytes: integer.min(1).parse(config.maxEntryBytes ?? 1024 * 1024),
44
+ retain: integer.min(1).max(1e5).parse(config.retain ?? 1000)
45
+ };
46
+ }
47
+ async function entryNames(directory) {
48
+ const entries = await readdir(directory, { withFileTypes: true });
49
+ return entries.filter((entry) => entry.isFile() && ENTRY_NAME.test(entry.name)).map((entry) => entry.name).sort();
50
+ }
51
+ async function readEntry(path, schema, maxEntryBytes) {
52
+ try {
53
+ const { size } = await stat(path);
54
+ if (size > maxEntryBytes) {
55
+ return { reason: "too-large", detail: `${size} bytes over the ${maxEntryBytes} limit` };
56
+ }
57
+ const text = await readFile(path, "utf8");
58
+ let json;
59
+ try {
60
+ json = JSON.parse(text);
61
+ } catch (error) {
62
+ return { reason: "invalid", detail: String(error).slice(0, 1000) };
63
+ }
64
+ const parsed = schema.safeParse(json);
65
+ return parsed.success ? { entry: parsed.data } : { reason: "invalid", detail: z.prettifyError(parsed.error).slice(0, 1000) };
66
+ } catch (error) {
67
+ if (absent(error))
68
+ return null;
69
+ throw error;
70
+ }
71
+ }
72
+ function createDirectoryInbox(config) {
73
+ const limits = resolveLimits(config);
74
+ const clock = config.clock ?? (() => new Date);
75
+ const store = config.store ?? createFileStateStore(join(config.directory, ".inbox-state.json"), {
76
+ schema: DirectoryInboxStateSchema
77
+ });
78
+ const parse = (state) => DirectoryInboxStateSchema.parse(state ?? emptyDirectoryInboxState());
79
+ const update = (transition) => store.update((current) => transition(parse(current)));
80
+ let running = false;
81
+ let admitting = false;
82
+ let timer;
83
+ let forced = new AbortController;
84
+ let tail = Promise.resolve();
85
+ const report = async (error) => {
86
+ try {
87
+ await config.onError?.(error);
88
+ } catch {}
89
+ };
90
+ const setAside = async (key, rejection) => {
91
+ await mkdir(join(config.directory, REJECTED), { recursive: true });
92
+ await quietly(() => rename(join(config.directory, key), join(config.directory, REJECTED, key)));
93
+ if (rejection)
94
+ await config.onRejected?.(rejection);
95
+ };
96
+ const reject = async (key, reason, detail) => {
97
+ const rejection = { key, reason, detail, rejectedAt: clock().toISOString() };
98
+ await update((state) => ({
99
+ state: withRejection(state, rejection, limits.retain),
100
+ result: undefined
101
+ }));
102
+ await setAside(key, rejection);
103
+ };
104
+ const deliver = async (claim) => {
105
+ const path = join(config.directory, claim.key);
106
+ const read = await readEntry(path, config.schema, limits.maxEntryBytes);
107
+ if (read === null)
108
+ return false;
109
+ if ("reason" in read) {
110
+ await reject(claim.key, read.reason, read.detail);
111
+ return false;
112
+ }
113
+ try {
114
+ await config.handle({
115
+ key: claim.key,
116
+ entry: read.entry,
117
+ attempt: claim.attempts,
118
+ signal: forced.signal
119
+ });
120
+ } catch (error) {
121
+ await report(error);
122
+ const retryAt = new Date(clock().getTime() + backoffDelay(RETRY_BACKOFF, claim.attempts));
123
+ await update((state) => ({
124
+ state: releaseEntry(state, claim, retryAt),
125
+ result: undefined
126
+ }));
127
+ return false;
128
+ }
129
+ await update((state) => ({
130
+ state: completeEntry(state, claim.key, clock(), limits.retain),
131
+ result: undefined
132
+ }));
133
+ await quietly(() => unlink(path));
134
+ return true;
135
+ };
136
+ const pass = async () => {
137
+ const names = await entryNames(config.directory);
138
+ await update((state) => ({
139
+ state: forgetMissing(state, new Set(names), clock()),
140
+ result: undefined
141
+ }));
142
+ let handled = 0;
143
+ for (const key of names) {
144
+ if (!admitting && running)
145
+ break;
146
+ const { step } = await update((state) => {
147
+ const next = claimEntry(state, { key, now: clock(), ...limits });
148
+ return { state: next.state, result: next };
149
+ });
150
+ if (step.kind === "remove")
151
+ await quietly(() => unlink(join(config.directory, key)));
152
+ else if (step.kind === "set-aside")
153
+ await setAside(key);
154
+ else if (step.kind === "reject")
155
+ await setAside(key, step.rejection);
156
+ else if (step.kind === "deliver" && await deliver(step.claim))
157
+ handled += 1;
158
+ }
159
+ return handled;
160
+ };
161
+ const flush = () => {
162
+ const result = tail.then(pass, pass);
163
+ tail = result.catch(() => {
164
+ return;
165
+ });
166
+ return result;
167
+ };
168
+ const poll = async () => {
169
+ try {
170
+ await flush();
171
+ } catch (error) {
172
+ await report(error);
173
+ }
174
+ if (admitting)
175
+ timer = setTimeout(() => void poll(), limits.pollIntervalMs);
176
+ };
177
+ const stopAdmitting = () => {
178
+ admitting = false;
179
+ if (timer)
180
+ clearTimeout(timer);
181
+ timer = undefined;
182
+ };
183
+ const inbox = {
184
+ flush,
185
+ async state() {
186
+ return parse(await store.read());
187
+ }
188
+ };
189
+ return defineManagedResource({
190
+ id: config.id,
191
+ ...config.dependsOn && { dependsOn: config.dependsOn },
192
+ async start() {
193
+ await mkdir(config.directory, { recursive: true });
194
+ forced = new AbortController;
195
+ return { value: inbox };
196
+ },
197
+ activate() {
198
+ running = true;
199
+ admitting = true;
200
+ poll();
201
+ },
202
+ stopAdmission() {
203
+ stopAdmitting();
204
+ },
205
+ async drain() {
206
+ await tail;
207
+ },
208
+ async close() {
209
+ stopAdmitting();
210
+ await tail;
211
+ },
212
+ async force() {
213
+ stopAdmitting();
214
+ forced.abort();
215
+ await tail;
216
+ }
217
+ });
218
+ }
219
+ export {
220
+ createDirectoryInbox
221
+ };
@@ -2,6 +2,7 @@ export { type ActivityId, ActivityIdSchema, type ActivityLiveState, ActivityLive
2
2
  export { type BoundedAdmission, type BoundedAdmissionClock, type BoundedAdmissionConfig, type BoundedAdmissionDrainOptions, type BoundedAdmissionDrainResult, type BoundedAdmissionForceResult, type BoundedAdmissionLeaseResult, type BoundedAdmissionPerKeyLimitResolver, type BoundedAdmissionPerKeyLimits, BoundedAdmissionPerKeyLimitsSchema, type BoundedAdmissionPolicy, BoundedAdmissionPolicySchema, BoundedAdmissionRefusalError, type BoundedAdmissionRefusalReason, BoundedAdmissionRefusalReasonSchema, type BoundedAdmissionRefusedResult, type BoundedAdmissionResult, type BoundedAdmissionSnapshot, BoundedAdmissionSnapshotSchema, type BoundedAdmissionState, BoundedAdmissionStateSchema, type BoundedOperationLease, type BoundedOperationRunContext, type BoundedOperationRunOptions, BoundedOperationWaitError, type BoundedRateBudget, BoundedRateBudgetSchema, createBoundedAdmission, } from '../application/admission.js';
3
3
  export { createDecisionPipeline, type DecisionPipeline, type DecisionPipelineConfig, type DecisionPolicy, DecisionPolicyError, type DecisionResult, type DecisionTraceEntry, DecisionUndecidedError, type PolicyDecision, PolicyDecisionSchema, } from '../application/decisions.js';
4
4
  export { type DiagnosticJournal, type DiagnosticJournalCloseResult, DiagnosticJournalCloseResultSchema, type DiagnosticJournalConfig, type DiagnosticJournalFailure, type DiagnosticJournalFailurePhase, DiagnosticJournalFailurePhaseSchema, type DiagnosticJournalFrame, DiagnosticJournalFrameSchema, type DiagnosticJournalLimits, DiagnosticJournalLimitsSchema, type DiagnosticJournalLockDiagnosis, type DiagnosticJournalLockPolicy, DiagnosticJournalLockPolicySchema, type DiagnosticJournalRefusalReason, DiagnosticJournalRefusalReasonSchema, type DiagnosticJournalState, DiagnosticJournalStateSchema, type DiagnosticJournalStatus, DiagnosticJournalStatusSchema, type DiagnosticJournalSubmitResult, DiagnosticJournalSubmitResultSchema, type DiagnosticJournalWaitOptions, type DiagnosticJournalWaitResult, DiagnosticJournalWaitResultSchema, readDiagnosticJournalLockDiagnosis, } from '../application/diagnostic-journal-contract.js';
5
+ export { type DirectoryInbox, type DirectoryInboxConfig, type DirectoryInboxDelivery, type DirectoryInboxRejection, type DirectoryInboxRejectionReason, DirectoryInboxRejectionReasonSchema, type DirectoryInboxResource, type DirectoryInboxState, DirectoryInboxStateSchema, } from '../application/directory-inbox-contract.js';
5
6
  export { type ApplicationEventSink, type ApplicationEventSinkConfig, type ApplicationLifecycleEvent, ApplicationLifecycleEventSchema, applicationLifecycleEvent, createApplicationEventSink, } from '../application/events.js';
6
7
  export { type ApplicationHealthHandlerOptions, ApplicationHealthHandlerOptionsSchema, type ApplicationOperationalHandlers, type ApplicationOperationalHandlersOptions, ApplicationOperationalHandlersOptionsSchema, createApplicationHealthHandler, createApplicationOperationalHandlers, } from '../application/health.js';
7
8
  export { createApplication } from '../application/kernel.js';
@@ -1 +1 @@
1
- {"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/entrypoints/application.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/entrypoints/application.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,10 @@ import {
14
14
  createApplicationSnapshotSink,
15
15
  createManagedSchedule
16
16
  } from "../index-ensd0r74.js";
17
+ import {
18
+ DirectoryInboxRejectionReasonSchema,
19
+ DirectoryInboxStateSchema
20
+ } from "../index-5jrd0yng.js";
17
21
  import {
18
22
  DiagnosticJournalCloseResultSchema,
19
23
  DiagnosticJournalFailurePhaseSchema,
@@ -2053,6 +2057,8 @@ export {
2053
2057
  LifecycleTerminationSchema,
2054
2058
  LifecycleStateSchema,
2055
2059
  LifecycleRunSchema,
2060
+ DirectoryInboxStateSchema,
2061
+ DirectoryInboxRejectionReasonSchema,
2056
2062
  DiagnosticJournalWaitResultSchema,
2057
2063
  DiagnosticJournalSubmitResultSchema,
2058
2064
  DiagnosticJournalStatusSchema,
@@ -21,6 +21,9 @@ import {
21
21
  webSocketLane
22
22
  } from "../index-p3vsv1fb.js";
23
23
  import"../index-y91ry9h2.js";
24
+ import {
25
+ createFileStateStore
26
+ } from "../index-tm88avnj.js";
24
27
  import {
25
28
  withDeadline
26
29
  } from "../index-b9aefrqd.js";
@@ -113,222 +116,6 @@ import {
113
116
  import {
114
117
  isRecord
115
118
  } from "../index-hffjpmq9.js";
116
-
117
- // src/application/file-state-store.ts
118
- import { mkdir, open, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
119
- import { basename, dirname, join } from "node:path";
120
- var ABANDONED_LOCK_MULTIPLIER = 10;
121
- var ABANDONED_HEARTBEATS = 30;
122
- var MIN_HEARTBEAT_MS = 10;
123
- var delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
124
- async function missing(error) {
125
- return error instanceof Error && "code" in error && error.code === "ENOENT";
126
- }
127
- function existing(error) {
128
- return error instanceof Error && "code" in error && error.code === "EEXIST";
129
- }
130
- async function ownerState(lockPath) {
131
- let owner;
132
- try {
133
- owner = JSON.parse(await readFile(lockPath, "utf8"));
134
- } catch {
135
- return "unknown";
136
- }
137
- if (typeof owner !== "object" || owner === null || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) {
138
- return "unknown";
139
- }
140
- try {
141
- process.kill(owner.pid, 0);
142
- return "alive";
143
- } catch (error) {
144
- return error instanceof Error && "code" in error && error.code === "ESRCH" ? "gone" : "unknown";
145
- }
146
- }
147
- async function sweepTemporaries(path, staleLockMs) {
148
- const prefix = `${basename(path)}.tmp.`;
149
- const directory = dirname(path);
150
- let entries;
151
- try {
152
- entries = await readdir(directory);
153
- } catch {
154
- return;
155
- }
156
- for (const entry of entries) {
157
- if (!entry.startsWith(prefix))
158
- continue;
159
- const candidate = join(directory, entry);
160
- const info = await stat(candidate).catch(() => null);
161
- if (info && Date.now() - info.mtimeMs > staleLockMs) {
162
- await unlink(candidate).catch(() => {
163
- return;
164
- });
165
- }
166
- }
167
- }
168
- async function reclaim(lockPath, observed, staleLockMs, abandonedAfterMs) {
169
- const guardPath = `${lockPath}.reclaim`;
170
- let guard;
171
- try {
172
- guard = await open(guardPath, "wx");
173
- } catch (error) {
174
- if (!existing(error))
175
- throw error;
176
- const orphan = await stat(guardPath).catch(() => null);
177
- if (orphan && Date.now() - orphan.mtimeMs > staleLockMs) {
178
- await unlink(guardPath).catch(() => {
179
- return;
180
- });
181
- }
182
- return false;
183
- }
184
- try {
185
- const current = await stat(lockPath).catch(() => null);
186
- if (!current || current.dev !== observed.dev || current.ino !== observed.ino || current.mtimeMs !== observed.mtimeMs) {
187
- return false;
188
- }
189
- const owner = await ownerState(lockPath);
190
- const age = Date.now() - current.mtimeMs;
191
- if (owner !== "gone" && age < abandonedAfterMs)
192
- return false;
193
- await unlink(lockPath);
194
- return true;
195
- } finally {
196
- await guard.close().catch(() => {
197
- return;
198
- });
199
- await unlink(guardPath).catch(() => {
200
- return;
201
- });
202
- }
203
- }
204
- function createFileStateStore(path, options) {
205
- const lockPath = `${path}.lock`;
206
- const lockTimeoutMs = options.lockTimeoutMs ?? 1e4;
207
- const staleLockMs = options.staleLockMs ?? 3000;
208
- const retryMs = options.retryMs ?? 10;
209
- const heartbeatMs = Math.max(MIN_HEARTBEAT_MS, Math.floor(staleLockMs / 3));
210
- const abandonedAfterMs = Math.max(staleLockMs * ABANDONED_LOCK_MULTIPLIER, heartbeatMs * ABANDONED_HEARTBEATS);
211
- if (staleLockMs >= lockTimeoutMs) {
212
- throw new Error(`[stitchkit] createFileStateStore: staleLockMs (${staleLockMs}) must be below lockTimeoutMs (${lockTimeoutMs}), or a crashed holder blocks every update until the timeout`);
213
- }
214
- let sweptTemporaries = false;
215
- const parseFile = async () => {
216
- let source;
217
- try {
218
- source = await readFile(path, "utf8");
219
- } catch (error) {
220
- if (await missing(error))
221
- return null;
222
- throw error;
223
- }
224
- try {
225
- return options.schema.parse(JSON.parse(source));
226
- } catch (error) {
227
- await options.onCorrupt?.({ path, error });
228
- if ((options.corrupt ?? "throw") === "empty")
229
- return null;
230
- throw error;
231
- }
232
- };
233
- const acquire = async () => {
234
- await mkdir(dirname(path), { recursive: true });
235
- if (!sweptTemporaries) {
236
- sweptTemporaries = true;
237
- await sweepTemporaries(path, staleLockMs);
238
- }
239
- const deadline = Date.now() + lockTimeoutMs;
240
- while (true) {
241
- try {
242
- const handle = await open(lockPath, "wx");
243
- const token = crypto.randomUUID();
244
- try {
245
- await handle.writeFile(JSON.stringify({ token, pid: process.pid, acquiredAt: Date.now() }));
246
- await handle.sync();
247
- } catch (error) {
248
- await handle.close().catch(() => {
249
- return;
250
- });
251
- await unlink(lockPath).catch(() => {
252
- return;
253
- });
254
- throw error;
255
- }
256
- return {
257
- refresh: async () => {
258
- const now = new Date;
259
- await handle.utimes(now, now);
260
- },
261
- release: async () => {
262
- await handle.close();
263
- try {
264
- const owner = JSON.parse(await readFile(lockPath, "utf8"));
265
- if (isRecord(owner) && owner.token === token)
266
- await unlink(lockPath);
267
- } catch (error) {
268
- if (!await missing(error))
269
- throw error;
270
- }
271
- }
272
- };
273
- } catch (error) {
274
- if (!existing(error))
275
- throw error;
276
- try {
277
- const lock = await stat(lockPath);
278
- if (Date.now() - lock.mtimeMs > staleLockMs) {
279
- if (await reclaim(lockPath, lock, staleLockMs, abandonedAfterMs))
280
- continue;
281
- }
282
- } catch (statError) {
283
- if (await missing(statError))
284
- continue;
285
- throw statError;
286
- }
287
- if (Date.now() >= deadline) {
288
- throw new Error(`[stitchkit] timed out acquiring state lock for ${path}`);
289
- }
290
- await delay(retryMs);
291
- }
292
- }
293
- };
294
- const write = async (state) => {
295
- const validated = options.schema.parse(state);
296
- const temporary = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
297
- const handle = await open(temporary, "wx");
298
- try {
299
- try {
300
- await handle.writeFile(`${JSON.stringify(validated)}
301
- `);
302
- await handle.sync();
303
- } finally {
304
- await handle.close();
305
- }
306
- await rename(temporary, path);
307
- } catch (error) {
308
- await unlink(temporary).catch(() => {
309
- return;
310
- });
311
- throw error;
312
- }
313
- };
314
- return {
315
- read: parseFile,
316
- async update(transition) {
317
- const lock = await acquire();
318
- const heartbeat = setInterval(() => void lock.refresh().catch(() => {
319
- return;
320
- }), heartbeatMs);
321
- try {
322
- const next = await transition(await parseFile());
323
- await write(next.state);
324
- return next.result;
325
- } finally {
326
- clearInterval(heartbeat);
327
- await lock.release();
328
- }
329
- }
330
- };
331
- }
332
119
  // src/realtime/registry.ts
333
120
  var authorizedRoom = Symbol("stitchkit.authorized-room");
334
121
  function freezeRoom(name) {
@@ -1001,7 +788,7 @@ function createEventBus(options = {}) {
1001
788
  };
1002
789
  }
1003
790
  // src/server/file.ts
1004
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
791
+ import { readFile, stat } from "node:fs/promises";
1005
792
  import { resolve } from "node:path";
1006
793
 
1007
794
  // src/server/mime.ts
@@ -1061,10 +848,10 @@ function staticRoute(prefix, dir) {
1061
848
  const realTarget = await realPathWithinDir(root, target);
1062
849
  if (realTarget === null)
1063
850
  return new Response("Not found", { status: 404 });
1064
- const info = await stat2(realTarget).catch(() => null);
851
+ const info = await stat(realTarget).catch(() => null);
1065
852
  if (!info?.isFile())
1066
853
  return new Response("Not found", { status: 404 });
1067
- return new Response(await readFile2(realTarget), {
854
+ return new Response(await readFile(realTarget), {
1068
855
  headers: {
1069
856
  "Content-Type": mimeForPath(realTarget),
1070
857
  "X-Content-Type-Options": "nosniff"
@@ -0,0 +1,114 @@
1
+ // src/application/directory-inbox-contract.ts
2
+ import { z } from "zod";
3
+ var timestamp = z.string().datetime({ offset: true });
4
+ var key = z.string().min(1).max(255);
5
+ var DirectoryInboxRejectionReasonSchema = z.enum([
6
+ "invalid",
7
+ "too-large",
8
+ "attempt-limit"
9
+ ]);
10
+ var DirectoryInboxStateSchema = z.object({
11
+ schemaVersion: z.literal(1),
12
+ claims: z.array(z.object({
13
+ key,
14
+ attempts: z.number().int().positive(),
15
+ nextAttemptAt: timestamp,
16
+ leaseId: z.string().min(1).max(128).nullable(),
17
+ leaseUntil: timestamp.nullable()
18
+ }).strict()),
19
+ receipts: z.array(z.object({ key, completedAt: timestamp }).strict()),
20
+ rejected: z.array(z.object({
21
+ key,
22
+ reason: DirectoryInboxRejectionReasonSchema,
23
+ detail: z.string().max(1000),
24
+ rejectedAt: timestamp
25
+ }).strict())
26
+ }).strict();
27
+ var emptyDirectoryInboxState = () => ({
28
+ schemaVersion: 1,
29
+ claims: [],
30
+ receipts: [],
31
+ rejected: []
32
+ });
33
+ var iso = (milliseconds) => new Date(milliseconds).toISOString();
34
+ function withRejection(state, rejection, retain) {
35
+ return {
36
+ ...state,
37
+ claims: state.claims.filter((claim) => claim.key !== rejection.key),
38
+ rejected: [
39
+ rejection,
40
+ ...state.rejected.filter((item) => item.key !== rejection.key)
41
+ ].slice(0, retain)
42
+ };
43
+ }
44
+ function claimEntry(state, input) {
45
+ if (state.receipts.some((receipt) => receipt.key === input.key)) {
46
+ return { state, step: { kind: "remove" } };
47
+ }
48
+ if (state.rejected.some((rejection) => rejection.key === input.key)) {
49
+ return { state, step: { kind: "set-aside" } };
50
+ }
51
+ const now = input.now.getTime();
52
+ const claim = state.claims.find((candidate) => candidate.key === input.key);
53
+ if (claim) {
54
+ const leased = claim.leaseUntil !== null && Date.parse(claim.leaseUntil) > now;
55
+ if (leased || Date.parse(claim.nextAttemptAt) > now)
56
+ return { state, step: { kind: "skip" } };
57
+ if (claim.attempts >= input.maxAttempts) {
58
+ const rejection = {
59
+ key: input.key,
60
+ reason: "attempt-limit",
61
+ detail: `taken ${claim.attempts} times without completing`,
62
+ rejectedAt: iso(now)
63
+ };
64
+ return {
65
+ state: withRejection(state, rejection, input.retain),
66
+ step: { kind: "reject", rejection }
67
+ };
68
+ }
69
+ }
70
+ const taken = {
71
+ key: input.key,
72
+ attempts: (claim?.attempts ?? 0) + 1,
73
+ nextAttemptAt: iso(now),
74
+ leaseId: crypto.randomUUID(),
75
+ leaseUntil: iso(now + input.leaseMs)
76
+ };
77
+ return {
78
+ state: {
79
+ ...state,
80
+ claims: [...state.claims.filter((candidate) => candidate.key !== input.key), taken]
81
+ },
82
+ step: { kind: "deliver", claim: taken }
83
+ };
84
+ }
85
+ function completeEntry(state, entryKey, now, retain) {
86
+ return {
87
+ ...state,
88
+ claims: state.claims.filter((claim) => claim.key !== entryKey),
89
+ receipts: [
90
+ { key: entryKey, completedAt: now.toISOString() },
91
+ ...state.receipts.filter((receipt) => receipt.key !== entryKey)
92
+ ].slice(0, retain)
93
+ };
94
+ }
95
+ function releaseEntry(state, claim, retryAt) {
96
+ return {
97
+ ...state,
98
+ claims: state.claims.map((candidate) => candidate.key === claim.key && candidate.leaseId === claim.leaseId ? {
99
+ ...candidate,
100
+ nextAttemptAt: retryAt.toISOString(),
101
+ leaseId: null,
102
+ leaseUntil: null
103
+ } : candidate)
104
+ };
105
+ }
106
+ function forgetMissing(state, present, now) {
107
+ const live = (claim) => claim.leaseUntil !== null && Date.parse(claim.leaseUntil) > now.getTime();
108
+ return {
109
+ ...state,
110
+ claims: state.claims.filter((claim) => present.has(claim.key) || live(claim))
111
+ };
112
+ }
113
+
114
+ export { DirectoryInboxRejectionReasonSchema, DirectoryInboxStateSchema, emptyDirectoryInboxState, withRejection, claimEntry, completeEntry, releaseEntry, forgetMissing };
@@ -0,0 +1,221 @@
1
+ import {
2
+ isRecord
3
+ } from "./index-hffjpmq9.js";
4
+
5
+ // src/application/file-state-store.ts
6
+ import { mkdir, open, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
7
+ import { basename, dirname, join } from "node:path";
8
+ var ABANDONED_LOCK_MULTIPLIER = 10;
9
+ var ABANDONED_HEARTBEATS = 30;
10
+ var MIN_HEARTBEAT_MS = 10;
11
+ var delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
12
+ async function missing(error) {
13
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
14
+ }
15
+ function existing(error) {
16
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
17
+ }
18
+ async function ownerState(lockPath) {
19
+ let owner;
20
+ try {
21
+ owner = JSON.parse(await readFile(lockPath, "utf8"));
22
+ } catch {
23
+ return "unknown";
24
+ }
25
+ if (typeof owner !== "object" || owner === null || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) {
26
+ return "unknown";
27
+ }
28
+ try {
29
+ process.kill(owner.pid, 0);
30
+ return "alive";
31
+ } catch (error) {
32
+ return error instanceof Error && "code" in error && error.code === "ESRCH" ? "gone" : "unknown";
33
+ }
34
+ }
35
+ async function sweepTemporaries(path, staleLockMs) {
36
+ const prefix = `${basename(path)}.tmp.`;
37
+ const directory = dirname(path);
38
+ let entries;
39
+ try {
40
+ entries = await readdir(directory);
41
+ } catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ if (!entry.startsWith(prefix))
46
+ continue;
47
+ const candidate = join(directory, entry);
48
+ const info = await stat(candidate).catch(() => null);
49
+ if (info && Date.now() - info.mtimeMs > staleLockMs) {
50
+ await unlink(candidate).catch(() => {
51
+ return;
52
+ });
53
+ }
54
+ }
55
+ }
56
+ async function reclaim(lockPath, observed, staleLockMs, abandonedAfterMs) {
57
+ const guardPath = `${lockPath}.reclaim`;
58
+ let guard;
59
+ try {
60
+ guard = await open(guardPath, "wx");
61
+ } catch (error) {
62
+ if (!existing(error))
63
+ throw error;
64
+ const orphan = await stat(guardPath).catch(() => null);
65
+ if (orphan && Date.now() - orphan.mtimeMs > staleLockMs) {
66
+ await unlink(guardPath).catch(() => {
67
+ return;
68
+ });
69
+ }
70
+ return false;
71
+ }
72
+ try {
73
+ const current = await stat(lockPath).catch(() => null);
74
+ if (!current || current.dev !== observed.dev || current.ino !== observed.ino || current.mtimeMs !== observed.mtimeMs) {
75
+ return false;
76
+ }
77
+ const owner = await ownerState(lockPath);
78
+ const age = Date.now() - current.mtimeMs;
79
+ if (owner !== "gone" && age < abandonedAfterMs)
80
+ return false;
81
+ await unlink(lockPath);
82
+ return true;
83
+ } finally {
84
+ await guard.close().catch(() => {
85
+ return;
86
+ });
87
+ await unlink(guardPath).catch(() => {
88
+ return;
89
+ });
90
+ }
91
+ }
92
+ function createFileStateStore(path, options) {
93
+ const lockPath = `${path}.lock`;
94
+ const lockTimeoutMs = options.lockTimeoutMs ?? 1e4;
95
+ const staleLockMs = options.staleLockMs ?? 3000;
96
+ const retryMs = options.retryMs ?? 10;
97
+ const heartbeatMs = Math.max(MIN_HEARTBEAT_MS, Math.floor(staleLockMs / 3));
98
+ const abandonedAfterMs = Math.max(staleLockMs * ABANDONED_LOCK_MULTIPLIER, heartbeatMs * ABANDONED_HEARTBEATS);
99
+ if (staleLockMs >= lockTimeoutMs) {
100
+ throw new Error(`[stitchkit] createFileStateStore: staleLockMs (${staleLockMs}) must be below lockTimeoutMs (${lockTimeoutMs}), or a crashed holder blocks every update until the timeout`);
101
+ }
102
+ let sweptTemporaries = false;
103
+ const parseFile = async () => {
104
+ let source;
105
+ try {
106
+ source = await readFile(path, "utf8");
107
+ } catch (error) {
108
+ if (await missing(error))
109
+ return null;
110
+ throw error;
111
+ }
112
+ try {
113
+ return options.schema.parse(JSON.parse(source));
114
+ } catch (error) {
115
+ await options.onCorrupt?.({ path, error });
116
+ if ((options.corrupt ?? "throw") === "empty")
117
+ return null;
118
+ throw error;
119
+ }
120
+ };
121
+ const acquire = async () => {
122
+ await mkdir(dirname(path), { recursive: true });
123
+ if (!sweptTemporaries) {
124
+ sweptTemporaries = true;
125
+ await sweepTemporaries(path, staleLockMs);
126
+ }
127
+ const deadline = Date.now() + lockTimeoutMs;
128
+ while (true) {
129
+ try {
130
+ const handle = await open(lockPath, "wx");
131
+ const token = crypto.randomUUID();
132
+ try {
133
+ await handle.writeFile(JSON.stringify({ token, pid: process.pid, acquiredAt: Date.now() }));
134
+ await handle.sync();
135
+ } catch (error) {
136
+ await handle.close().catch(() => {
137
+ return;
138
+ });
139
+ await unlink(lockPath).catch(() => {
140
+ return;
141
+ });
142
+ throw error;
143
+ }
144
+ return {
145
+ refresh: async () => {
146
+ const now = new Date;
147
+ await handle.utimes(now, now);
148
+ },
149
+ release: async () => {
150
+ await handle.close();
151
+ try {
152
+ const owner = JSON.parse(await readFile(lockPath, "utf8"));
153
+ if (isRecord(owner) && owner.token === token)
154
+ await unlink(lockPath);
155
+ } catch (error) {
156
+ if (!await missing(error))
157
+ throw error;
158
+ }
159
+ }
160
+ };
161
+ } catch (error) {
162
+ if (!existing(error))
163
+ throw error;
164
+ try {
165
+ const lock = await stat(lockPath);
166
+ if (Date.now() - lock.mtimeMs > staleLockMs) {
167
+ if (await reclaim(lockPath, lock, staleLockMs, abandonedAfterMs))
168
+ continue;
169
+ }
170
+ } catch (statError) {
171
+ if (await missing(statError))
172
+ continue;
173
+ throw statError;
174
+ }
175
+ if (Date.now() >= deadline) {
176
+ throw new Error(`[stitchkit] timed out acquiring state lock for ${path}`);
177
+ }
178
+ await delay(retryMs);
179
+ }
180
+ }
181
+ };
182
+ const write = async (state) => {
183
+ const validated = options.schema.parse(state);
184
+ const temporary = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
185
+ const handle = await open(temporary, "wx");
186
+ try {
187
+ try {
188
+ await handle.writeFile(`${JSON.stringify(validated)}
189
+ `);
190
+ await handle.sync();
191
+ } finally {
192
+ await handle.close();
193
+ }
194
+ await rename(temporary, path);
195
+ } catch (error) {
196
+ await unlink(temporary).catch(() => {
197
+ return;
198
+ });
199
+ throw error;
200
+ }
201
+ };
202
+ return {
203
+ read: parseFile,
204
+ async update(transition) {
205
+ const lock = await acquire();
206
+ const heartbeat = setInterval(() => void lock.refresh().catch(() => {
207
+ return;
208
+ }), heartbeatMs);
209
+ try {
210
+ const next = await transition(await parseFile());
211
+ await write(next.state);
212
+ return next.result;
213
+ } finally {
214
+ clearInterval(heartbeat);
215
+ await lock.release();
216
+ }
217
+ }
218
+ };
219
+ }
220
+
221
+ export { createFileStateStore };
@@ -0,0 +1,9 @@
1
+ # stitchkit/application/directory-inbox — stitchkit docs slice
2
+
3
+ > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
+ > `llms.txt` in this package.
5
+
6
+ Contains: pointers only
7
+
8
+ Also read:
9
+ - see `llms/application.txt` for the Managed application kernel guide
@@ -299,6 +299,8 @@ cutovers are covered by the executable
299
299
  | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
300
300
  | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
301
301
  | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
302
+ | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | managed resource delivering each `<name>.json` entry another program drops into a directory to the application at least once, after readiness; separate durable taken/done records, leases, bounded retry |
303
+ | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
302
304
  | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
303
305
  | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
304
306
  | `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |
@@ -905,7 +905,36 @@ deadline holds `stop()` — and so the resource's `force()` — for that one cal
905
905
  so give the transport a timeout. Superseding a key that is being sent right now
906
906
  does not recall it: the send completes, and only its receipt is not written.
907
907
 
908
- Both primitives depend on the structural `StateStore`. On a server,
908
+ `createDirectoryInbox` is the receiving side for another program on the same
909
+ host: it drops entries — a release transition, say — into a directory, and
910
+ the application takes each one at least once. It is a managed resource of its
911
+ own (`createDirectoryInbox({ id, directory, schema, handle })`, imported from
912
+ `stitchkit/application/directory-inbox` because it touches files; its types
913
+ and schemas are in `stitchkit/application`) and publishes a handle with
914
+ `flush()` and `state()`. The producer writes each entry
915
+ atomically as `<name>.json` — a temporary name, then a rename; names that start
916
+ with a dot are never read. Entries are delivered in name order, and only after
917
+ `activate`: none reaches the application before it is ready. `stopAdmission`
918
+ stops taking new ones, `drain` waits for the one in flight, and `force` aborts
919
+ the `signal` its handler was given.
920
+
921
+ Taken and done are separate durable records. Before `handle` runs, the entry
922
+ is claimed with a lease (`leaseMs`, default five minutes); after it returns,
923
+ a receipt replaces the claim and only then is the file removed. A process that
924
+ died mid-delivery leaves a claim, and the entry is taken again once the lease
925
+ runs out — at least once, not exactly once, so a handler that sends somewhere
926
+ should carry the entry's `key` as its idempotency key. A process that died
927
+ after the receipt leaves only the file, which the next pass removes without
928
+ delivering it again. A handler that throws is retried after a backoff from one
929
+ second doubling to five minutes; an entry that fails `schema`, is over
930
+ `maxEntryBytes` or was taken `maxAttempts` times (default 20 — the count
931
+ survives restarts, so an entry whose handling kills the process is not taken
932
+ forever) is moved to `rejected/` with its reason and reported to `onRejected`,
933
+ and the entries after it keep flowing. The state lives in
934
+ `<directory>/.inbox-state.json` under `createFileStateStore`'s inter-process
935
+ lock unless a `store` is supplied.
936
+
937
+ These primitives depend on the structural `StateStore`. On a server,
909
938
  `createFileStateStore` supplies the shared Zod-validated JSON adapter with an
910
939
  inter-process lock, unique temporary file, fsync and atomic rename. The lock
911
940
  is a file with a heartbeat: the holder refreshes its mtime every third of
@@ -95,6 +95,7 @@ own, recorded as an ADR.
95
95
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
96
96
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
97
97
  | `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
98
+ | `stitchkit/application/directory-inbox` | server | evolving | the directory inbox — entries another program drops into a directory, delivered to the application at least once |
98
99
 
99
100
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
100
101
  code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
package/llms-full.txt CHANGED
@@ -86,6 +86,7 @@ own, recorded as an ADR.
86
86
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
87
87
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
88
88
  | `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
89
+ | `stitchkit/application/directory-inbox` | server | evolving | the directory inbox — entries another program drops into a directory, delivered to the application at least once |
89
90
 
90
91
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
91
92
  code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
@@ -7151,7 +7152,36 @@ deadline holds `stop()` — and so the resource's `force()` — for that one cal
7151
7152
  so give the transport a timeout. Superseding a key that is being sent right now
7152
7153
  does not recall it: the send completes, and only its receipt is not written.
7153
7154
 
7154
- Both primitives depend on the structural `StateStore`. On a server,
7155
+ `createDirectoryInbox` is the receiving side for another program on the same
7156
+ host: it drops entries — a release transition, say — into a directory, and
7157
+ the application takes each one at least once. It is a managed resource of its
7158
+ own (`createDirectoryInbox({ id, directory, schema, handle })`, imported from
7159
+ `stitchkit/application/directory-inbox` because it touches files; its types
7160
+ and schemas are in `stitchkit/application`) and publishes a handle with
7161
+ `flush()` and `state()`. The producer writes each entry
7162
+ atomically as `<name>.json` — a temporary name, then a rename; names that start
7163
+ with a dot are never read. Entries are delivered in name order, and only after
7164
+ `activate`: none reaches the application before it is ready. `stopAdmission`
7165
+ stops taking new ones, `drain` waits for the one in flight, and `force` aborts
7166
+ the `signal` its handler was given.
7167
+
7168
+ Taken and done are separate durable records. Before `handle` runs, the entry
7169
+ is claimed with a lease (`leaseMs`, default five minutes); after it returns,
7170
+ a receipt replaces the claim and only then is the file removed. A process that
7171
+ died mid-delivery leaves a claim, and the entry is taken again once the lease
7172
+ runs out — at least once, not exactly once, so a handler that sends somewhere
7173
+ should carry the entry's `key` as its idempotency key. A process that died
7174
+ after the receipt leaves only the file, which the next pass removes without
7175
+ delivering it again. A handler that throws is retried after a backoff from one
7176
+ second doubling to five minutes; an entry that fails `schema`, is over
7177
+ `maxEntryBytes` or was taken `maxAttempts` times (default 20 — the count
7178
+ survives restarts, so an entry whose handling kills the process is not taken
7179
+ forever) is moved to `rejected/` with its reason and reported to `onRejected`,
7180
+ and the entries after it keep flowing. The state lives in
7181
+ `<directory>/.inbox-state.json` under `createFileStateStore`'s inter-process
7182
+ lock unless a `store` is supplied.
7183
+
7184
+ These primitives depend on the structural `StateStore`. On a server,
7155
7185
  `createFileStateStore` supplies the shared Zod-validated JSON adapter with an
7156
7186
  inter-process lock, unique temporary file, fsync and atomic rename. The lock
7157
7187
  is a file with a heartbeat: the holder refreshes its mtime every third of
@@ -16978,6 +17008,8 @@ cutovers are covered by the executable
16978
17008
  | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
16979
17009
  | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
16980
17010
  | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
17011
+ | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | managed resource delivering each `<name>.json` entry another program drops into a directory to the application at least once, after readiness; separate durable taken/done records, leases, bounded retry |
17012
+ | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
16981
17013
  | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
16982
17014
  | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
16983
17015
  | `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
9
9
  Load the slice for each entrypoint your code imports — `import … from 'stitchkit/server'` → `llms/server.txt`; the root `stitchkit` import → `llms/stitchkit.txt`. A slice holds that entrypoint's API reference section and the guide pages whose home it is; every guide lives in exactly one slice, and a slice that needs another's guide names it under "Also read". Each file is at most 50.0 KB; a longer one continues in numbered parts, each naming the next at its top. Moving across versions: load `llms/upgrading.txt` and the range slices it lists between your version and the target. `llms-full.txt` inlines everything; it is far larger than a context window, so do not read it whole.
10
10
 
11
11
  ## Entrypoint slices
12
- - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 15.1 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 35.0 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 14.2 KB
12
+ - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 15.3 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 35.0 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 14.2 KB
13
13
  - **stitchkit/live**: [llms/live.txt](llms/live.txt) 21.2 KB
14
14
  - **stitchkit/react**: [llms/react.txt](llms/react.txt) 8.6 KB
15
15
  - **stitchkit/tools**: [llms/tools.txt](llms/tools.txt) 44.9 KB → [llms/tools.2.txt](llms/tools.2.txt) 26.3 KB → [llms/tools.3.txt](llms/tools.3.txt) 25.4 KB
@@ -30,9 +30,10 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
30
30
  - **stitchkit/agent-runtime/openrouter**: [llms/agent-runtime-openrouter.txt](llms/agent-runtime-openrouter.txt) 1.1 KB
31
31
  - **stitchkit/agent-runtime/sqlite/bun**: [llms/agent-runtime-sqlite-bun.txt](llms/agent-runtime-sqlite-bun.txt) 1.8 KB
32
32
  - **stitchkit/agent-runtime/sqlite/node**: [llms/agent-runtime-sqlite-node.txt](llms/agent-runtime-sqlite-node.txt) 1.7 KB
33
- - **stitchkit/application**: [llms/application.txt](llms/application.txt) 40.1 KB → [llms/application.2.txt](llms/application.2.txt) 31.9 KB
33
+ - **stitchkit/application**: [llms/application.txt](llms/application.txt) 42.1 KB → [llms/application.2.txt](llms/application.2.txt) 32.6 KB
34
34
  - **stitchkit/application/grammy**: [llms/application-grammy.txt](llms/application-grammy.txt) 1.5 KB
35
35
  - **stitchkit/application/opentelemetry**: [llms/application-opentelemetry.txt](llms/application-opentelemetry.txt) 1.6 KB
36
+ - **stitchkit/application/directory-inbox**: [llms/application-directory-inbox.txt](llms/application-directory-inbox.txt) 0.3 KB
36
37
  - **stitchkit/application/diagnostic-journal**: [llms/application-diagnostic-journal.txt](llms/application-diagnostic-journal.txt) 1.4 KB
37
38
  - **stitchkit/application/schemas**: [llms/application-schemas.txt](llms/application-schemas.txt) 1.8 KB
38
39
  - **stitchkit/testing**: [llms/testing.txt](llms/testing.txt) 30.8 KB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.95.0",
3
+ "version": "0.95.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -146,6 +146,10 @@
146
146
  "types": "./dist/entrypoints/application/opentelemetry.d.ts",
147
147
  "import": "./dist/entrypoints/application/opentelemetry.js"
148
148
  },
149
+ "./application/directory-inbox": {
150
+ "types": "./dist/entrypoints/application/directory-inbox.d.ts",
151
+ "import": "./dist/entrypoints/application/directory-inbox.js"
152
+ },
149
153
  "./application/diagnostic-journal": {
150
154
  "types": "./dist/entrypoints/application/diagnostic-journal.d.ts",
151
155
  "import": "./dist/entrypoints/application/diagnostic-journal.js"