@afokapu/atdd-flow 0.3.28 → 0.3.29
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/package.json +1 -1
- package/src/threads.ts +34 -4
package/package.json
CHANGED
package/src/threads.ts
CHANGED
|
@@ -90,6 +90,18 @@ type PendingReference = { thread: string; message: string; created_at: string; p
|
|
|
90
90
|
type PendingSegment = { schema: "atdd-flow/pi-inbox-segment/v1"; entries: PendingReference[]; next?: string };
|
|
91
91
|
type PendingQueue = { schema: "atdd-flow/pi-inbox-queue/v1"; head?: string; tail?: string };
|
|
92
92
|
|
|
93
|
+
function isPendingQueue(value: unknown): value is Required<PendingQueue> {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
95
|
+
const queue = value as PendingQueue;
|
|
96
|
+
return queue.schema === "atdd-flow/pi-inbox-queue/v1"
|
|
97
|
+
&& typeof queue.head === "string" && queue.head.length > 0
|
|
98
|
+
&& typeof queue.tail === "string" && queue.tail.length > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function missingFile(error: unknown) {
|
|
102
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
function inboxDirectory(root: string, address: string) {
|
|
94
106
|
return join(root, ".atdd-flow", "pi-inbox", encodeURIComponent(address));
|
|
95
107
|
}
|
|
@@ -129,8 +141,16 @@ export async function enqueueNativeMail(root: string, address: string, reference
|
|
|
129
141
|
return withInboxLock(directory, async () => {
|
|
130
142
|
const statePath = join(directory, "queue.yaml");
|
|
131
143
|
let queue: PendingQueue;
|
|
132
|
-
try {
|
|
133
|
-
|
|
144
|
+
try {
|
|
145
|
+
queue = await readYaml<PendingQueue>(statePath);
|
|
146
|
+
if (!isPendingQueue(queue)) throw new Error(`Malformed pending inbox queue ${statePath}`);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// A reconciler deletes the index once it drains the final segment. Only
|
|
149
|
+
// that absent-index state may be initialized by normal enqueue; parse,
|
|
150
|
+
// permission, and competing-state failures remain fail-closed.
|
|
151
|
+
if (!missingFile(error)) throw error;
|
|
152
|
+
queue = { schema: "atdd-flow/pi-inbox-queue/v1" };
|
|
153
|
+
}
|
|
134
154
|
const entriesPath = (segment: string) => join(directory, "pending", `${segment}.yaml`);
|
|
135
155
|
const compare = (left: PendingReference, right: PendingReference) => left.created_at.localeCompare(right.created_at) || left.message.localeCompare(right.message);
|
|
136
156
|
const capacity = 32;
|
|
@@ -176,10 +196,20 @@ export async function enqueueNativeMail(root: string, address: string, reference
|
|
|
176
196
|
});
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
async function publishNativeMail(root: string, address: string, _segment: string, message: string) {
|
|
199
|
+
export async function publishNativeMail(root: string, address: string, _segment: string, message: string) {
|
|
180
200
|
const directory = inboxDirectory(root, address);
|
|
181
201
|
await withInboxLock(directory, async () => {
|
|
182
|
-
const
|
|
202
|
+
const statePath = join(directory, "queue.yaml");
|
|
203
|
+
let queue: PendingQueue;
|
|
204
|
+
try { queue = await readYaml<PendingQueue>(statePath); }
|
|
205
|
+
catch (error) {
|
|
206
|
+
// A native reconciler may consume the newly authoritative message and
|
|
207
|
+
// drain the queue between prepare and this advisory publish marker.
|
|
208
|
+
// Its deletion is a successful exactly-once delivery, not an error.
|
|
209
|
+
if (missingFile(error)) return;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
if (!isPendingQueue(queue)) throw new Error(`Malformed pending inbox queue ${statePath}`);
|
|
183
213
|
let segment = queue.head;
|
|
184
214
|
while (segment) {
|
|
185
215
|
const path = join(directory, "pending", `${segment}.yaml`);
|