@reaatech/media-pipeline-mcp-persistence 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.cjs +423 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +255 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +399 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// src/in-memory-store.ts
|
|
2
|
+
import { RunInProgressError, RunNotFoundError } from "@reaatech/media-pipeline-mcp-core";
|
|
3
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
|
|
4
|
+
var LOCK_POLL_INTERVAL_MS = 25;
|
|
5
|
+
function cloneRun(run) {
|
|
6
|
+
return {
|
|
7
|
+
...run,
|
|
8
|
+
steps: run.steps.map((s) => ({ ...s })),
|
|
9
|
+
events: run.events.map((e) => ({ ...e }))
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function matchesFilter(run, filter) {
|
|
13
|
+
if (!filter) return true;
|
|
14
|
+
if (filter.status) {
|
|
15
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
16
|
+
if (!statuses.includes(run.status)) return false;
|
|
17
|
+
}
|
|
18
|
+
if (filter.tenantId && run.tenantId !== filter.tenantId) return false;
|
|
19
|
+
if (filter.idempotencyKey && run.idempotencyKey !== filter.idempotencyKey) return false;
|
|
20
|
+
if (filter.since && run.createdAt < filter.since) return false;
|
|
21
|
+
if (filter.until && run.createdAt > filter.until) return false;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
var InMemoryPipelineStateStore = class {
|
|
25
|
+
runs = /* @__PURE__ */ new Map();
|
|
26
|
+
locks = /* @__PURE__ */ new Map();
|
|
27
|
+
async create(run) {
|
|
28
|
+
if (this.runs.has(run.runId)) {
|
|
29
|
+
throw new RunInProgressError();
|
|
30
|
+
}
|
|
31
|
+
const stored = cloneRun(run);
|
|
32
|
+
this.runs.set(run.runId, stored);
|
|
33
|
+
}
|
|
34
|
+
async get(runId) {
|
|
35
|
+
const run = this.runs.get(runId);
|
|
36
|
+
if (!run) return null;
|
|
37
|
+
return cloneRun(run);
|
|
38
|
+
}
|
|
39
|
+
async update(runId, patch, expectedVersion) {
|
|
40
|
+
const existing = this.runs.get(runId);
|
|
41
|
+
if (!existing) {
|
|
42
|
+
throw new RunNotFoundError();
|
|
43
|
+
}
|
|
44
|
+
if (expectedVersion !== void 0 && existing.version !== expectedVersion) {
|
|
45
|
+
throw new RunInProgressError();
|
|
46
|
+
}
|
|
47
|
+
const stored = cloneRun(existing);
|
|
48
|
+
Object.assign(stored, patch);
|
|
49
|
+
stored.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
50
|
+
stored.version = (stored.version ?? 0) + 1;
|
|
51
|
+
this.runs.set(runId, stored);
|
|
52
|
+
}
|
|
53
|
+
async cancel(runId, reason) {
|
|
54
|
+
const run = this.runs.get(runId);
|
|
55
|
+
if (!run) {
|
|
56
|
+
throw new RunNotFoundError();
|
|
57
|
+
}
|
|
58
|
+
run.status = "cancelled";
|
|
59
|
+
run.error = reason;
|
|
60
|
+
run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
run.version = (run.version ?? 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
async appendEvent(runId, event) {
|
|
64
|
+
const run = this.runs.get(runId);
|
|
65
|
+
if (!run) {
|
|
66
|
+
throw new RunNotFoundError();
|
|
67
|
+
}
|
|
68
|
+
run.events.push(event);
|
|
69
|
+
run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
70
|
+
}
|
|
71
|
+
async listEvents(runId, sinceSeq) {
|
|
72
|
+
const run = this.runs.get(runId);
|
|
73
|
+
if (!run) {
|
|
74
|
+
throw new RunNotFoundError();
|
|
75
|
+
}
|
|
76
|
+
const events = run.events.map((e) => ({ ...e }));
|
|
77
|
+
if (sinceSeq === void 0 || sinceSeq < 0) return events;
|
|
78
|
+
return events.slice(sinceSeq);
|
|
79
|
+
}
|
|
80
|
+
async listRuns(filter) {
|
|
81
|
+
let result = Array.from(this.runs.values()).filter((r) => matchesFilter(r, filter));
|
|
82
|
+
result.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
83
|
+
if (filter?.offset) {
|
|
84
|
+
result = result.slice(filter.offset);
|
|
85
|
+
}
|
|
86
|
+
if (filter?.limit) {
|
|
87
|
+
result = result.slice(0, filter.limit);
|
|
88
|
+
}
|
|
89
|
+
return result.map(cloneRun);
|
|
90
|
+
}
|
|
91
|
+
async findByExternalJobId(provider, jobId) {
|
|
92
|
+
const key = `${provider}:${jobId}`;
|
|
93
|
+
for (const run of this.runs.values()) {
|
|
94
|
+
if (run.externalJobId === key || run.externalJobIds?.[provider] === jobId) {
|
|
95
|
+
return cloneRun(run);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* In-memory mutex via a Promise chain. Wraps the lock acquisition in a timeout race
|
|
102
|
+
* — if the prior holder hasn't released within `timeoutMs`, throws `RunInProgressError`.
|
|
103
|
+
*
|
|
104
|
+
* NOTE: this is single-process only. Cross-process locking (Redis SET NX EX, Postgres
|
|
105
|
+
* advisory locks) belongs in those backends' implementations.
|
|
106
|
+
*/
|
|
107
|
+
async withLock(runId, fn, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
|
|
108
|
+
const prev = this.locks.get(runId) ?? Promise.resolve();
|
|
109
|
+
const deadline = Date.now() + timeoutMs;
|
|
110
|
+
while (this.locks.has(runId) && Date.now() < deadline) {
|
|
111
|
+
try {
|
|
112
|
+
await Promise.race([
|
|
113
|
+
prev,
|
|
114
|
+
new Promise(
|
|
115
|
+
(resolve) => setTimeout(resolve, Math.min(LOCK_POLL_INTERVAL_MS, deadline - Date.now()))
|
|
116
|
+
)
|
|
117
|
+
]);
|
|
118
|
+
} catch {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!this.locks.has(runId)) break;
|
|
122
|
+
}
|
|
123
|
+
if (this.locks.has(runId) && Date.now() >= deadline) {
|
|
124
|
+
throw new RunInProgressError();
|
|
125
|
+
}
|
|
126
|
+
const next = (async () => {
|
|
127
|
+
const run = await this.get(runId);
|
|
128
|
+
if (!run) throw new RunNotFoundError();
|
|
129
|
+
return fn(run);
|
|
130
|
+
})();
|
|
131
|
+
this.locks.set(
|
|
132
|
+
runId,
|
|
133
|
+
next.catch(() => {
|
|
134
|
+
}).finally(() => {
|
|
135
|
+
if (this.locks.get(runId) === void 0) return;
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
try {
|
|
139
|
+
return await next;
|
|
140
|
+
} finally {
|
|
141
|
+
const current = this.locks.get(runId);
|
|
142
|
+
if (current === next.catch(() => {
|
|
143
|
+
})) {
|
|
144
|
+
this.locks.delete(runId);
|
|
145
|
+
} else {
|
|
146
|
+
this.locks.delete(runId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/redis-store.ts
|
|
153
|
+
import {
|
|
154
|
+
RunInProgressError as RunInProgressError2,
|
|
155
|
+
RunNotFoundError as RunNotFoundError2,
|
|
156
|
+
StateStoreUnavailableError
|
|
157
|
+
} from "@reaatech/media-pipeline-mcp-core";
|
|
158
|
+
var DAY = 86400;
|
|
159
|
+
var RedisPipelineStateStore = class {
|
|
160
|
+
client;
|
|
161
|
+
prefix;
|
|
162
|
+
runTtlSeconds;
|
|
163
|
+
lockTtlSeconds;
|
|
164
|
+
idempotencyTtlSeconds;
|
|
165
|
+
externalJobTtlSeconds;
|
|
166
|
+
lockAcquireTimeoutMs;
|
|
167
|
+
lockPollIntervalMs;
|
|
168
|
+
constructor(config) {
|
|
169
|
+
this.client = config.client;
|
|
170
|
+
this.prefix = config.prefix ?? "mp";
|
|
171
|
+
this.runTtlSeconds = config.runTtlSeconds ?? 30 * DAY;
|
|
172
|
+
this.lockTtlSeconds = config.lockTtlSeconds ?? 60;
|
|
173
|
+
this.idempotencyTtlSeconds = config.idempotencyTtlSeconds ?? DAY;
|
|
174
|
+
this.externalJobTtlSeconds = config.externalJobTtlSeconds ?? 7 * DAY;
|
|
175
|
+
this.lockAcquireTimeoutMs = config.lockAcquireTimeoutMs ?? 5e3;
|
|
176
|
+
this.lockPollIntervalMs = config.lockPollIntervalMs ?? 100;
|
|
177
|
+
}
|
|
178
|
+
runKey(runId) {
|
|
179
|
+
return `${this.prefix}:run:${runId}`;
|
|
180
|
+
}
|
|
181
|
+
eventsKey(runId) {
|
|
182
|
+
return `${this.prefix}:run:${runId}:events`;
|
|
183
|
+
}
|
|
184
|
+
lockKey(runId) {
|
|
185
|
+
return `${this.prefix}:run:${runId}:lock`;
|
|
186
|
+
}
|
|
187
|
+
idempotencyKey(key) {
|
|
188
|
+
return `${this.prefix}:idem:${key}`;
|
|
189
|
+
}
|
|
190
|
+
jobKey(provider, jobId) {
|
|
191
|
+
return `${this.prefix}:job:${provider}:${jobId}`;
|
|
192
|
+
}
|
|
193
|
+
tenantKey(tenantId) {
|
|
194
|
+
return `${this.prefix}:tenant:${tenantId}:runs`;
|
|
195
|
+
}
|
|
196
|
+
async wrap(op) {
|
|
197
|
+
try {
|
|
198
|
+
return await op();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const error = err;
|
|
201
|
+
if (error?.code === "ECONNREFUSED" || error?.code === "ECONNRESET" || error?.code === "ETIMEDOUT" || /redis|connection/i.test(error?.message ?? "")) {
|
|
202
|
+
throw new StateStoreUnavailableError();
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async create(run) {
|
|
208
|
+
return this.wrap(async () => {
|
|
209
|
+
const key = this.runKey(run.runId);
|
|
210
|
+
const existing = await this.client.get(key);
|
|
211
|
+
if (existing) {
|
|
212
|
+
throw new RunInProgressError2();
|
|
213
|
+
}
|
|
214
|
+
await this.client.set(key, JSON.stringify(run), "EX", this.runTtlSeconds);
|
|
215
|
+
if (run.tenantId) {
|
|
216
|
+
const ts = Date.parse(run.createdAt) || Date.now();
|
|
217
|
+
await this.client.zadd(this.tenantKey(run.tenantId), ts, run.runId);
|
|
218
|
+
await this.client.expire(this.tenantKey(run.tenantId), this.runTtlSeconds);
|
|
219
|
+
}
|
|
220
|
+
if (run.idempotencyKey) {
|
|
221
|
+
await this.client.set(
|
|
222
|
+
this.idempotencyKey(run.idempotencyKey),
|
|
223
|
+
run.runId,
|
|
224
|
+
"EX",
|
|
225
|
+
this.idempotencyTtlSeconds
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
for (const [provider, jobId] of Object.entries(run.externalJobIds ?? {})) {
|
|
229
|
+
await this.client.set(
|
|
230
|
+
this.jobKey(provider, jobId),
|
|
231
|
+
run.runId,
|
|
232
|
+
"EX",
|
|
233
|
+
this.externalJobTtlSeconds
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async get(runId) {
|
|
239
|
+
return this.wrap(async () => {
|
|
240
|
+
const raw = await this.client.get(this.runKey(runId));
|
|
241
|
+
if (!raw) return null;
|
|
242
|
+
const run = JSON.parse(raw);
|
|
243
|
+
const evs = await this.client.lrange(this.eventsKey(runId), 0, -1);
|
|
244
|
+
run.events = evs.map((s) => JSON.parse(s));
|
|
245
|
+
return run;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async update(runId, patch, expectedVersion) {
|
|
249
|
+
return this.wrap(async () => {
|
|
250
|
+
const raw = await this.client.get(this.runKey(runId));
|
|
251
|
+
if (!raw) {
|
|
252
|
+
throw new RunNotFoundError2();
|
|
253
|
+
}
|
|
254
|
+
const existing = JSON.parse(raw);
|
|
255
|
+
if (expectedVersion !== void 0 && existing.version !== expectedVersion) {
|
|
256
|
+
throw new RunInProgressError2();
|
|
257
|
+
}
|
|
258
|
+
const previousJobs = existing.externalJobIds ?? {};
|
|
259
|
+
const merged = {
|
|
260
|
+
...existing,
|
|
261
|
+
...patch,
|
|
262
|
+
// Don't let patch reset events; those live in a separate list.
|
|
263
|
+
events: existing.events,
|
|
264
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
265
|
+
version: (existing.version ?? 0) + 1
|
|
266
|
+
};
|
|
267
|
+
await this.client.set(
|
|
268
|
+
this.runKey(runId),
|
|
269
|
+
JSON.stringify({ ...merged, events: [] }),
|
|
270
|
+
"EX",
|
|
271
|
+
this.runTtlSeconds
|
|
272
|
+
);
|
|
273
|
+
const newJobs = merged.externalJobIds ?? {};
|
|
274
|
+
for (const [provider, jobId] of Object.entries(newJobs)) {
|
|
275
|
+
if (previousJobs[provider] !== jobId) {
|
|
276
|
+
await this.client.set(
|
|
277
|
+
this.jobKey(provider, jobId),
|
|
278
|
+
runId,
|
|
279
|
+
"EX",
|
|
280
|
+
this.externalJobTtlSeconds
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async cancel(runId, reason) {
|
|
287
|
+
return this.wrap(async () => {
|
|
288
|
+
const raw = await this.client.get(this.runKey(runId));
|
|
289
|
+
if (!raw) {
|
|
290
|
+
throw new RunNotFoundError2();
|
|
291
|
+
}
|
|
292
|
+
const existing = JSON.parse(raw);
|
|
293
|
+
existing.status = "cancelled";
|
|
294
|
+
existing.error = reason;
|
|
295
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
296
|
+
existing.version = (existing.version ?? 0) + 1;
|
|
297
|
+
await this.client.set(
|
|
298
|
+
this.runKey(runId),
|
|
299
|
+
JSON.stringify({ ...existing, events: [] }),
|
|
300
|
+
"EX",
|
|
301
|
+
this.runTtlSeconds
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async appendEvent(runId, event) {
|
|
306
|
+
return this.wrap(async () => {
|
|
307
|
+
const exists = await this.client.get(this.runKey(runId));
|
|
308
|
+
if (!exists) throw new RunNotFoundError2();
|
|
309
|
+
await this.client.rpush(this.eventsKey(runId), JSON.stringify(event));
|
|
310
|
+
await this.client.expire(this.eventsKey(runId), this.runTtlSeconds);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async listEvents(runId, sinceSeq) {
|
|
314
|
+
return this.wrap(async () => {
|
|
315
|
+
const exists = await this.client.get(this.runKey(runId));
|
|
316
|
+
if (!exists) throw new RunNotFoundError2();
|
|
317
|
+
const start = sinceSeq !== void 0 && sinceSeq >= 0 ? sinceSeq : 0;
|
|
318
|
+
const raw = await this.client.lrange(this.eventsKey(runId), start, -1);
|
|
319
|
+
return raw.map((s) => JSON.parse(s));
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async listRuns(filter) {
|
|
323
|
+
return this.wrap(async () => {
|
|
324
|
+
if (filter?.tenantId) {
|
|
325
|
+
const min = filter.since ? Date.parse(filter.since) || 0 : 0;
|
|
326
|
+
const max = filter.until ? Date.parse(filter.until) || Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
|
|
327
|
+
const runIds = await this.client.zrangebyscore(this.tenantKey(filter.tenantId), min, max);
|
|
328
|
+
const runs = [];
|
|
329
|
+
for (const id of runIds) {
|
|
330
|
+
const run = await this.get(id);
|
|
331
|
+
if (run && matchesFilter2(run, filter)) runs.push(run);
|
|
332
|
+
}
|
|
333
|
+
runs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
334
|
+
return applyPaging(runs, filter);
|
|
335
|
+
}
|
|
336
|
+
if (filter?.idempotencyKey) {
|
|
337
|
+
const runId = await this.client.get(this.idempotencyKey(filter.idempotencyKey));
|
|
338
|
+
if (!runId) return [];
|
|
339
|
+
const run = await this.get(runId);
|
|
340
|
+
return run && matchesFilter2(run, filter) ? [run] : [];
|
|
341
|
+
}
|
|
342
|
+
return [];
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async findByExternalJobId(provider, jobId) {
|
|
346
|
+
return this.wrap(async () => {
|
|
347
|
+
const runId = await this.client.get(this.jobKey(provider, jobId));
|
|
348
|
+
if (!runId) return null;
|
|
349
|
+
return this.get(runId);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async withLock(runId, fn, timeoutMs) {
|
|
353
|
+
const deadline = Date.now() + (timeoutMs ?? this.lockAcquireTimeoutMs);
|
|
354
|
+
const lockKey = this.lockKey(runId);
|
|
355
|
+
let acquired = false;
|
|
356
|
+
while (Date.now() < deadline) {
|
|
357
|
+
const res = await this.wrap(
|
|
358
|
+
() => this.client.set(lockKey, "1", "NX", "EX", this.lockTtlSeconds)
|
|
359
|
+
);
|
|
360
|
+
if (res === "OK" || res === 1) {
|
|
361
|
+
acquired = true;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
await new Promise((r) => setTimeout(r, this.lockPollIntervalMs));
|
|
365
|
+
}
|
|
366
|
+
if (!acquired) {
|
|
367
|
+
throw new RunInProgressError2();
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const run = await this.get(runId);
|
|
371
|
+
if (!run) throw new RunNotFoundError2();
|
|
372
|
+
return await fn(run);
|
|
373
|
+
} finally {
|
|
374
|
+
await this.client.del(lockKey).catch(() => void 0);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function matchesFilter2(run, filter) {
|
|
379
|
+
if (!filter) return true;
|
|
380
|
+
if (filter.status) {
|
|
381
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
382
|
+
if (!statuses.includes(run.status)) return false;
|
|
383
|
+
}
|
|
384
|
+
if (filter.idempotencyKey && run.idempotencyKey !== filter.idempotencyKey) return false;
|
|
385
|
+
if (filter.since && run.createdAt < filter.since) return false;
|
|
386
|
+
if (filter.until && run.createdAt > filter.until) return false;
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
function applyPaging(runs, filter) {
|
|
390
|
+
let result = runs;
|
|
391
|
+
if (filter?.offset) result = result.slice(filter.offset);
|
|
392
|
+
if (filter?.limit) result = result.slice(0, filter.limit);
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
export {
|
|
396
|
+
InMemoryPipelineStateStore,
|
|
397
|
+
RedisPipelineStateStore
|
|
398
|
+
};
|
|
399
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/in-memory-store.ts","../src/redis-store.ts"],"sourcesContent":["import { RunInProgressError, RunNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { PipelineEvent, PipelineRun, PipelineStateStore, RunFilter } from './types.js';\n\nconst DEFAULT_LOCK_TIMEOUT_MS = 5_000;\nconst LOCK_POLL_INTERVAL_MS = 25;\n\nfunction cloneRun(run: PipelineRun): PipelineRun {\n return {\n ...run,\n steps: run.steps.map((s) => ({ ...s })),\n events: run.events.map((e) => ({ ...e })),\n };\n}\n\nfunction matchesFilter(run: PipelineRun, filter?: RunFilter): boolean {\n if (!filter) return true;\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (!statuses.includes(run.status)) return false;\n }\n if (filter.tenantId && run.tenantId !== filter.tenantId) return false;\n if (filter.idempotencyKey && run.idempotencyKey !== filter.idempotencyKey) return false;\n if (filter.since && run.createdAt < filter.since) return false;\n if (filter.until && run.createdAt > filter.until) return false;\n return true;\n}\n\nexport class InMemoryPipelineStateStore implements PipelineStateStore {\n private runs = new Map<string, PipelineRun>();\n private locks = new Map<string, Promise<unknown>>();\n\n async create(run: PipelineRun): Promise<void> {\n if (this.runs.has(run.runId)) {\n throw new RunInProgressError();\n }\n const stored = cloneRun(run);\n this.runs.set(run.runId, stored);\n }\n\n async get(runId: string): Promise<PipelineRun | null> {\n const run = this.runs.get(runId);\n if (!run) return null;\n return cloneRun(run);\n }\n\n async update(\n runId: string,\n patch: Partial<PipelineRun>,\n expectedVersion?: number,\n ): Promise<void> {\n const existing = this.runs.get(runId);\n if (!existing) {\n throw new RunNotFoundError();\n }\n if (expectedVersion !== undefined && existing.version !== expectedVersion) {\n throw new RunInProgressError();\n }\n const stored = cloneRun(existing);\n Object.assign(stored, patch);\n stored.updatedAt = new Date().toISOString();\n stored.version = (stored.version ?? 0) + 1;\n this.runs.set(runId, stored);\n }\n\n async cancel(runId: string, reason: string): Promise<void> {\n const run = this.runs.get(runId);\n if (!run) {\n throw new RunNotFoundError();\n }\n run.status = 'cancelled';\n run.error = reason;\n run.updatedAt = new Date().toISOString();\n // cancel() is a mutating op; bump the version so concurrent expectedVersion-update\n // attempts see the conflict.\n run.version = (run.version ?? 0) + 1;\n }\n\n async appendEvent(runId: string, event: PipelineEvent): Promise<void> {\n const run = this.runs.get(runId);\n if (!run) {\n throw new RunNotFoundError();\n }\n run.events.push(event);\n run.updatedAt = new Date().toISOString();\n }\n\n async listEvents(runId: string, sinceSeq?: number): Promise<PipelineEvent[]> {\n const run = this.runs.get(runId);\n if (!run) {\n throw new RunNotFoundError();\n }\n const events = run.events.map((e) => ({ ...e }) as PipelineEvent);\n if (sinceSeq === undefined || sinceSeq < 0) return events;\n return events.slice(sinceSeq);\n }\n\n async listRuns(filter?: RunFilter): Promise<PipelineRun[]> {\n let result = Array.from(this.runs.values()).filter((r) => matchesFilter(r, filter));\n result.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n if (filter?.offset) {\n result = result.slice(filter.offset);\n }\n if (filter?.limit) {\n result = result.slice(0, filter.limit);\n }\n return result.map(cloneRun);\n }\n\n async findByExternalJobId(provider: string, jobId: string): Promise<PipelineRun | null> {\n const key = `${provider}:${jobId}`;\n for (const run of this.runs.values()) {\n if (run.externalJobId === key || run.externalJobIds?.[provider] === jobId) {\n return cloneRun(run);\n }\n }\n return null;\n }\n\n /**\n * In-memory mutex via a Promise chain. Wraps the lock acquisition in a timeout race\n * — if the prior holder hasn't released within `timeoutMs`, throws `RunInProgressError`.\n *\n * NOTE: this is single-process only. Cross-process locking (Redis SET NX EX, Postgres\n * advisory locks) belongs in those backends' implementations.\n */\n async withLock<T>(\n runId: string,\n fn: (run: PipelineRun) => Promise<T>,\n timeoutMs: number = DEFAULT_LOCK_TIMEOUT_MS,\n ): Promise<T> {\n const prev = this.locks.get(runId) ?? Promise.resolve();\n\n // Race the previous holder against a timeout. If timeout wins, refuse to acquire.\n const deadline = Date.now() + timeoutMs;\n while (this.locks.has(runId) && Date.now() < deadline) {\n try {\n await Promise.race([\n prev,\n new Promise((resolve) =>\n setTimeout(resolve, Math.min(LOCK_POLL_INTERVAL_MS, deadline - Date.now())),\n ),\n ]);\n } catch {\n // Previous holder rejected — chain is free for us.\n break;\n }\n if (!this.locks.has(runId)) break;\n }\n\n if (this.locks.has(runId) && Date.now() >= deadline) {\n throw new RunInProgressError();\n }\n\n const next = (async () => {\n const run = await this.get(runId);\n if (!run) throw new RunNotFoundError();\n return fn(run);\n })();\n\n this.locks.set(\n runId,\n next\n .catch(() => {})\n .finally(() => {\n // Release iff this is still the active holder.\n if (this.locks.get(runId) === undefined) return;\n }),\n );\n\n try {\n return await next;\n } finally {\n // Always release after the work completes so the next acquirer can proceed.\n const current = this.locks.get(runId);\n if (current === next.catch(() => {})) {\n this.locks.delete(runId);\n } else {\n // Different promise in the slot (shouldn't happen given our serialization),\n // but clear if the slot still resolves to a settled promise.\n this.locks.delete(runId);\n }\n }\n }\n}\n","import {\n RunInProgressError,\n RunNotFoundError,\n StateStoreUnavailableError,\n} from '@reaatech/media-pipeline-mcp-core';\nimport type { PipelineEvent, PipelineRun, PipelineStateStore, RunFilter } from './types.js';\n\n/**\n * Minimal slice of the ioredis API that this store relies on. Importing the real\n * type would force `ioredis` to be a hard dep; declaring locally keeps it optional\n * (plan §0.1 — Redis is an opt-in backend).\n */\nexport interface RedisClientLike {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n del(...keys: string[]): Promise<number>;\n rpush(key: string, ...values: string[]): Promise<number>;\n lrange(key: string, start: number, stop: number): Promise<string[]>;\n zadd(key: string, ...args: (string | number)[]): Promise<number | string>;\n zrange(key: string, start: number, stop: number): Promise<string[]>;\n zrangebyscore(\n key: string,\n min: number | string,\n max: number | string,\n ...args: unknown[]\n ): Promise<string[]>;\n expire(key: string, seconds: number): Promise<number>;\n ping(): Promise<string>;\n quit?(): Promise<unknown>;\n}\n\nexport interface RedisPipelineStateStoreConfig {\n client: RedisClientLike;\n /** Key namespace. Default `mp`. */\n prefix?: string;\n /** TTL for run, events, tenant index. Default 30d (plan §0.1 table). */\n runTtlSeconds?: number;\n /** Lock TTL (plan §0.1 — `SET key 1 NX EX 60`). Default 60s. */\n lockTtlSeconds?: number;\n /** Idempotency entry TTL. Default 24h. */\n idempotencyTtlSeconds?: number;\n /** External-job mapping TTL. Default 7d. */\n externalJobTtlSeconds?: number;\n /** Hard upper bound on lock acquisition wait. Default 5_000ms. */\n lockAcquireTimeoutMs?: number;\n /** Lock acquisition poll interval. Default 100ms. */\n lockPollIntervalMs?: number;\n}\n\nconst DAY = 86_400;\n\n/**\n * Redis-backed PipelineStateStore. Implements the schema in plan §0.1:\n *\n * | Key | Type | Value | TTL |\n * |------------------------------|--------|-------------------|------|\n * | `mp:run:<runId>` | string | JSON(PipelineRun) | 30d |\n * | `mp:run:<runId>:events` | list | JSON event lines | 30d |\n * | `mp:run:<runId>:lock` | string | \"1\" (SET NX EX) | 60s |\n * | `mp:idem:<key>` | string | runId | 24h |\n * | `mp:job:<provider>:<jobId>` | string | runId | 7d |\n * | `mp:tenant:<tenantId>:runs` | zset | runId (createdAt) | 30d |\n *\n * Locking is `SET NX EX <lockTtlSeconds>` with up to `lockAcquireTimeoutMs` of\n * polling. On Redis errors, `StateStoreUnavailableError` is thrown (retryable).\n */\nexport class RedisPipelineStateStore implements PipelineStateStore {\n private readonly client: RedisClientLike;\n private readonly prefix: string;\n private readonly runTtlSeconds: number;\n private readonly lockTtlSeconds: number;\n private readonly idempotencyTtlSeconds: number;\n private readonly externalJobTtlSeconds: number;\n private readonly lockAcquireTimeoutMs: number;\n private readonly lockPollIntervalMs: number;\n\n constructor(config: RedisPipelineStateStoreConfig) {\n this.client = config.client;\n this.prefix = config.prefix ?? 'mp';\n this.runTtlSeconds = config.runTtlSeconds ?? 30 * DAY;\n this.lockTtlSeconds = config.lockTtlSeconds ?? 60;\n this.idempotencyTtlSeconds = config.idempotencyTtlSeconds ?? DAY;\n this.externalJobTtlSeconds = config.externalJobTtlSeconds ?? 7 * DAY;\n this.lockAcquireTimeoutMs = config.lockAcquireTimeoutMs ?? 5_000;\n this.lockPollIntervalMs = config.lockPollIntervalMs ?? 100;\n }\n\n private runKey(runId: string): string {\n return `${this.prefix}:run:${runId}`;\n }\n private eventsKey(runId: string): string {\n return `${this.prefix}:run:${runId}:events`;\n }\n private lockKey(runId: string): string {\n return `${this.prefix}:run:${runId}:lock`;\n }\n private idempotencyKey(key: string): string {\n return `${this.prefix}:idem:${key}`;\n }\n private jobKey(provider: string, jobId: string): string {\n return `${this.prefix}:job:${provider}:${jobId}`;\n }\n private tenantKey(tenantId: string): string {\n return `${this.prefix}:tenant:${tenantId}:runs`;\n }\n\n private async wrap<T>(op: () => Promise<T>): Promise<T> {\n try {\n return await op();\n } catch (err) {\n const error = err as Error & { code?: string };\n if (\n error?.code === 'ECONNREFUSED' ||\n error?.code === 'ECONNRESET' ||\n error?.code === 'ETIMEDOUT' ||\n /redis|connection/i.test(error?.message ?? '')\n ) {\n throw new StateStoreUnavailableError();\n }\n throw err;\n }\n }\n\n async create(run: PipelineRun): Promise<void> {\n return this.wrap(async () => {\n const key = this.runKey(run.runId);\n const existing = await this.client.get(key);\n if (existing) {\n throw new RunInProgressError();\n }\n await this.client.set(key, JSON.stringify(run), 'EX', this.runTtlSeconds);\n if (run.tenantId) {\n const ts = Date.parse(run.createdAt) || Date.now();\n await this.client.zadd(this.tenantKey(run.tenantId), ts, run.runId);\n await this.client.expire(this.tenantKey(run.tenantId), this.runTtlSeconds);\n }\n if (run.idempotencyKey) {\n await this.client.set(\n this.idempotencyKey(run.idempotencyKey),\n run.runId,\n 'EX',\n this.idempotencyTtlSeconds,\n );\n }\n for (const [provider, jobId] of Object.entries(run.externalJobIds ?? {})) {\n await this.client.set(\n this.jobKey(provider, jobId),\n run.runId,\n 'EX',\n this.externalJobTtlSeconds,\n );\n }\n });\n }\n\n async get(runId: string): Promise<PipelineRun | null> {\n return this.wrap(async () => {\n const raw = await this.client.get(this.runKey(runId));\n if (!raw) return null;\n const run = JSON.parse(raw) as PipelineRun;\n // events are stored separately; hydrate so callers see the canonical shape.\n const evs = await this.client.lrange(this.eventsKey(runId), 0, -1);\n run.events = evs.map((s) => JSON.parse(s) as PipelineEvent);\n return run;\n });\n }\n\n async update(\n runId: string,\n patch: Partial<PipelineRun>,\n expectedVersion?: number,\n ): Promise<void> {\n return this.wrap(async () => {\n const raw = await this.client.get(this.runKey(runId));\n if (!raw) {\n throw new RunNotFoundError();\n }\n const existing = JSON.parse(raw) as PipelineRun;\n if (expectedVersion !== undefined && existing.version !== expectedVersion) {\n throw new RunInProgressError();\n }\n const previousJobs = existing.externalJobIds ?? {};\n const merged: PipelineRun = {\n ...existing,\n ...patch,\n // Don't let patch reset events; those live in a separate list.\n events: existing.events,\n updatedAt: new Date().toISOString(),\n version: (existing.version ?? 0) + 1,\n };\n await this.client.set(\n this.runKey(runId),\n JSON.stringify({ ...merged, events: [] }),\n 'EX',\n this.runTtlSeconds,\n );\n // Update job index for any newly assigned external job ids.\n const newJobs = merged.externalJobIds ?? {};\n for (const [provider, jobId] of Object.entries(newJobs)) {\n if (previousJobs[provider] !== jobId) {\n await this.client.set(\n this.jobKey(provider, jobId),\n runId,\n 'EX',\n this.externalJobTtlSeconds,\n );\n }\n }\n });\n }\n\n async cancel(runId: string, reason: string): Promise<void> {\n return this.wrap(async () => {\n const raw = await this.client.get(this.runKey(runId));\n if (!raw) {\n throw new RunNotFoundError();\n }\n const existing = JSON.parse(raw) as PipelineRun;\n existing.status = 'cancelled';\n existing.error = reason;\n existing.updatedAt = new Date().toISOString();\n existing.version = (existing.version ?? 0) + 1;\n await this.client.set(\n this.runKey(runId),\n JSON.stringify({ ...existing, events: [] }),\n 'EX',\n this.runTtlSeconds,\n );\n });\n }\n\n async appendEvent(runId: string, event: PipelineEvent): Promise<void> {\n return this.wrap(async () => {\n const exists = await this.client.get(this.runKey(runId));\n if (!exists) throw new RunNotFoundError();\n await this.client.rpush(this.eventsKey(runId), JSON.stringify(event));\n await this.client.expire(this.eventsKey(runId), this.runTtlSeconds);\n });\n }\n\n async listEvents(runId: string, sinceSeq?: number): Promise<PipelineEvent[]> {\n return this.wrap(async () => {\n const exists = await this.client.get(this.runKey(runId));\n if (!exists) throw new RunNotFoundError();\n const start = sinceSeq !== undefined && sinceSeq >= 0 ? sinceSeq : 0;\n const raw = await this.client.lrange(this.eventsKey(runId), start, -1);\n return raw.map((s) => JSON.parse(s) as PipelineEvent);\n });\n }\n\n async listRuns(filter?: RunFilter): Promise<PipelineRun[]> {\n return this.wrap(async () => {\n // Fast path when scoped by tenant — use the per-tenant zset index.\n if (filter?.tenantId) {\n const min = filter.since ? Date.parse(filter.since) || 0 : 0;\n const max = filter.until\n ? Date.parse(filter.until) || Number.MAX_SAFE_INTEGER\n : Number.MAX_SAFE_INTEGER;\n const runIds = await this.client.zrangebyscore(this.tenantKey(filter.tenantId), min, max);\n const runs: PipelineRun[] = [];\n for (const id of runIds) {\n const run = await this.get(id);\n if (run && matchesFilter(run, filter)) runs.push(run);\n }\n runs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n return applyPaging(runs, filter);\n }\n // No tenant index — this is an O(N) op and is documented as such; Redis store\n // users querying without a tenant scope should keep result sets bounded.\n // (See plan §0.1: production deployments scope by tenant.)\n // For idempotency key lookups we have a direct index.\n if (filter?.idempotencyKey) {\n const runId = await this.client.get(this.idempotencyKey(filter.idempotencyKey));\n if (!runId) return [];\n const run = await this.get(runId);\n return run && matchesFilter(run, filter) ? [run] : [];\n }\n return [];\n });\n }\n\n async findByExternalJobId(provider: string, jobId: string): Promise<PipelineRun | null> {\n return this.wrap(async () => {\n const runId = await this.client.get(this.jobKey(provider, jobId));\n if (!runId) return null;\n return this.get(runId);\n });\n }\n\n async withLock<T>(\n runId: string,\n fn: (run: PipelineRun) => Promise<T>,\n timeoutMs?: number,\n ): Promise<T> {\n const deadline = Date.now() + (timeoutMs ?? this.lockAcquireTimeoutMs);\n const lockKey = this.lockKey(runId);\n let acquired = false;\n while (Date.now() < deadline) {\n const res = await this.wrap(() =>\n this.client.set(lockKey, '1', 'NX', 'EX', this.lockTtlSeconds),\n );\n if (res === 'OK' || res === 1) {\n acquired = true;\n break;\n }\n await new Promise((r) => setTimeout(r, this.lockPollIntervalMs));\n }\n if (!acquired) {\n throw new RunInProgressError();\n }\n try {\n const run = await this.get(runId);\n if (!run) throw new RunNotFoundError();\n return await fn(run);\n } finally {\n // Best-effort release — TTL ensures locks never leak.\n await this.client.del(lockKey).catch(() => undefined);\n }\n }\n}\n\nfunction matchesFilter(run: PipelineRun, filter?: RunFilter): boolean {\n if (!filter) return true;\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (!statuses.includes(run.status)) return false;\n }\n if (filter.idempotencyKey && run.idempotencyKey !== filter.idempotencyKey) return false;\n if (filter.since && run.createdAt < filter.since) return false;\n if (filter.until && run.createdAt > filter.until) return false;\n return true;\n}\n\nfunction applyPaging(runs: PipelineRun[], filter?: RunFilter): PipelineRun[] {\n let result = runs;\n if (filter?.offset) result = result.slice(filter.offset);\n if (filter?.limit) result = result.slice(0, filter.limit);\n return result;\n}\n"],"mappings":"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","names":["RunInProgressError","RunNotFoundError","matchesFilter"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reaatech/media-pipeline-mcp-persistence",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Pipeline state persistence — run store, event log, locking",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Rick Somers <rick@reaatech.com> (https://reaatech.com)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/reaatech/media-pipeline-mcp.git",
|
|
10
|
+
"directory": "packages/persistence"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/reaatech/media-pipeline-mcp/tree/main/packages/persistence#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/reaatech/media-pipeline-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@reaatech/media-pipeline-mcp-core": "0.3.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"debug": "^4.4.3",
|
|
39
|
+
"ioredis": "^5.4.1",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"ioredis": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"ioredis": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|