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 +17 -0
- package/dist/application/directory-inbox-contract.d.ts +129 -0
- package/dist/application/directory-inbox-contract.d.ts.map +1 -0
- package/dist/application/directory-inbox.d.ts +3 -0
- package/dist/application/directory-inbox.d.ts.map +1 -0
- package/dist/entrypoints/application/directory-inbox.d.ts +11 -0
- package/dist/entrypoints/application/directory-inbox.d.ts.map +1 -0
- package/dist/entrypoints/application/directory-inbox.js +221 -0
- package/dist/entrypoints/application.d.ts +1 -0
- package/dist/entrypoints/application.d.ts.map +1 -1
- package/dist/entrypoints/application.js +6 -0
- package/dist/entrypoints/server.js +6 -219
- package/dist/index-5jrd0yng.js +114 -0
- package/dist/index-tm88avnj.js +221 -0
- package/llms/application-directory-inbox.txt +9 -0
- package/llms/application.2.txt +2 -0
- package/llms/application.txt +30 -1
- package/llms/stitchkit.txt +1 -0
- package/llms-full.txt +33 -1
- package/llms.txt +3 -2
- package/package.json +5 -1
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 @@
|
|
|
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":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,iCAAiC,EACtC,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,KAAK,eAAe,EACpB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,2BAA2B,EAC3B,KAAK,aAAa,EAClB,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,mCAAmC,EACxC,KAAK,4BAA4B,EACjC,kCAAkC,EAClC,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,4BAA4B,EAC5B,KAAK,6BAA6B,EAClC,mCAAmC,EACnC,KAAK,6BAA6B,EAClC,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,8BAA8B,EAC9B,KAAK,qBAAqB,EAC1B,2BAA2B,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAC/B,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,sBAAsB,EACtB,KAAK,cAAc,EACnB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,4BAA4B,EACjC,kCAAkC,EAClC,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,EAClC,mCAAmC,EACnC,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,6BAA6B,EAC7B,KAAK,8BAA8B,EACnC,KAAK,2BAA2B,EAChC,iCAAiC,EACjC,KAAK,8BAA8B,EACnC,oCAAoC,EACpC,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,6BAA6B,EAC7B,KAAK,6BAA6B,EAClC,mCAAmC,EACnC,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,iCAAiC,EACjC,kCAAkC,GACnC,MAAM,4CAA4C,CAAC;AACpD,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,mCAAmC,EACnC,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,GAC1B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,+BAA+B,EACpC,qCAAqC,EACrC,KAAK,8BAA8B,EACnC,KAAK,qCAAqC,EAC1C,2CAA2C,EAC3C,8BAA8B,EAC9B,oCAAoC,GACrC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EACL,KAAK,oBAAoB,EACzB,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,6BAA6B,EAC7B,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,KAAK,wBAAwB,EAC7B,8BAA8B,EAC9B,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,KAAK,0BAA0B,EAC/B,gCAAgC,GACjC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,YAAY,GACb,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,2BAA2B,EAChC,qBAAqB,GACtB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,KAAK,6BAA6B,GACnC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iCAAiC,EACtC,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,0BAA0B,EAC1B,6BAA6B,GAC9B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,GAClC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,kBAAkB,EAClB,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,oBAAoB,EACzB,0BAA0B,EAC1B,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,kBAAkB,EAClB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,yBAAyB,EACzB,sBAAsB,EACtB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,0BAA0B,EAC/B,2BAA2B,GAC5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,2BAA2B,EAChC,iCAAiC,EACjC,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,KAAK,wBAAwB,EAC7B,8BAA8B,GAC/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,KAAK,0BAA0B,EAC/B,gCAAgC,EAChC,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC1B,2BAA2B,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,qBAAqB,GACtB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC/E,OAAO,EACL,KAAK,eAAe,EACpB,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,QAAQ,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,0BAA0B,EAC1B,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,0BAA0B,EAC1B,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACvF,cAAc,uBAAuB,CAAC"}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
package/llms/application.2.txt
CHANGED
|
@@ -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 |
|
package/llms/application.txt
CHANGED
|
@@ -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
|
-
|
|
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
|
package/llms/stitchkit.txt
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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)
|
|
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.
|
|
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"
|