pixivflow 2.19.3 → 2.19.4
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/dist/commands/scheduler-runtime.js +4 -15
- package/dist/delivery/DeliveryAck.d.ts +14 -0
- package/dist/delivery/DeliveryAck.js +18 -0
- package/dist/delivery/OutboxWorker.js +13 -0
- package/dist/delivery/settleDeliveryTerminal.d.ts +25 -0
- package/dist/delivery/settleDeliveryTerminal.js +40 -0
- package/dist/package.json +1 -1
- package/dist/version.js +1 -1
- package/dist/webui/package.json +1 -1
- package/package.json +1 -1
|
@@ -59,6 +59,7 @@ const schedules_1 = require("../scheduler/schedules");
|
|
|
59
59
|
const SlotCoordinator_1 = require("../scheduler/SlotCoordinator");
|
|
60
60
|
const DeliveryLedgerPort_1 = require("../delivery/DeliveryLedgerPort");
|
|
61
61
|
const OutboxWorker_1 = require("../delivery/OutboxWorker");
|
|
62
|
+
const settleDeliveryTerminal_1 = require("../delivery/settleDeliveryTerminal");
|
|
62
63
|
const LegacyOutboxMigration_1 = require("../delivery/LegacyOutboxMigration");
|
|
63
64
|
const NotificationPolicy_1 = require("../notification/NotificationPolicy");
|
|
64
65
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -241,22 +242,10 @@ async function createSchedulerRuntime(configPathArg) {
|
|
|
241
242
|
const outboxWorker = new OutboxWorker_1.OutboxWorker(database, deliveryDispatcher, {
|
|
242
243
|
retryBaseMs: config.delivery?.outboxRetryBaseMs,
|
|
243
244
|
retryMaxMs: config.delivery?.outboxRetryMaxMs,
|
|
244
|
-
// A confirmed ACK
|
|
245
|
+
// A confirmed ACK settles the owning Slot cell (submitted / duplicate /
|
|
246
|
+
// failed); see settleDeliveryTerminal for the invariant it enforces.
|
|
245
247
|
onDeliveryTerminal: (deliveryId, ack) => {
|
|
246
|
-
|
|
247
|
-
if (!row || !row.slotId || !row.targetId)
|
|
248
|
-
return;
|
|
249
|
-
if (ack.kind === 'duplicate_existing') {
|
|
250
|
-
const coord = new SlotCoordinator_1.SlotCoordinator(database);
|
|
251
|
-
coord.applyOutcome(row.slotId, row.targetId, {
|
|
252
|
-
kind: 'duplicate',
|
|
253
|
-
workId: row.pixivId,
|
|
254
|
-
reason: 'downstream attested historical duplicate',
|
|
255
|
-
});
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const coord = new SlotCoordinator_1.SlotCoordinator(database);
|
|
259
|
-
coord.markDelivered(row.slotId, row.targetId, row.pixivId, row.workType);
|
|
248
|
+
(0, settleDeliveryTerminal_1.settleDeliveryTerminal)(database, deliveryId, ack);
|
|
260
249
|
},
|
|
261
250
|
});
|
|
262
251
|
const notificationPolicy = new NotificationPolicy_1.NotificationPolicy(database, config);
|
|
@@ -35,6 +35,20 @@ export type DeliveryAck = {
|
|
|
35
35
|
matchedKey?: string;
|
|
36
36
|
raw?: unknown;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The provider accepted the request and persisted a record, but that record
|
|
40
|
+
* is in a TERMINAL FAILURE state (`failed`/`rejected`/`invalid`/`expired`).
|
|
41
|
+
* Nothing will ever be published from this intent, and because the provider
|
|
42
|
+
* keys its record by OUR idempotency_key, another attempt only returns the
|
|
43
|
+
* same failed record. Terminal: never retried, never reported as delivered.
|
|
44
|
+
*/
|
|
45
|
+
| {
|
|
46
|
+
kind: 'remote_failed';
|
|
47
|
+
remoteId?: string;
|
|
48
|
+
remoteStatus: string;
|
|
49
|
+
error: string;
|
|
50
|
+
raw?: unknown;
|
|
51
|
+
}
|
|
38
52
|
/** Transient failure: timeouts, 5xx, 429, connection errors. Retryable. */
|
|
39
53
|
| {
|
|
40
54
|
kind: 'retryable_failure';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseDeliveryAck = parseDeliveryAck;
|
|
4
|
+
/** Downstream record states that will never become a published delivery. */
|
|
5
|
+
const TERMINAL_REMOTE_STATUSES = new Set(['failed', 'rejected', 'invalid', 'expired']);
|
|
4
6
|
const DEFAULT_HINTS = {
|
|
5
7
|
dataPath: 'data',
|
|
6
8
|
idField: 'review_id',
|
|
@@ -49,6 +51,22 @@ function parseDeliveryAck(status, body, hints = {}) {
|
|
|
49
51
|
const remoteId = typeof idValue === 'number' ? String(idValue) : asString(idValue);
|
|
50
52
|
const remoteStatus = asString(rec[h.statusField]);
|
|
51
53
|
const reused = rec[h.reusedField] === true;
|
|
54
|
+
// A 2xx envelope can still describe a RECORD THAT FAILED downstream: the
|
|
55
|
+
// provider persists the record before doing the real work, so "accepted" and
|
|
56
|
+
// "the persisted record is broken" arrive on the same HTTP status. The record
|
|
57
|
+
// is what gets published (and what a retry will find again), so its terminal
|
|
58
|
+
// state outranks the transport-level success of this request — otherwise a
|
|
59
|
+
// remote failure is recorded here as an end-to-end success.
|
|
60
|
+
const terminalStatus = remoteStatus?.trim().toLowerCase();
|
|
61
|
+
if (terminalStatus && TERMINAL_REMOTE_STATUSES.has(terminalStatus)) {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'remote_failed',
|
|
64
|
+
remoteId,
|
|
65
|
+
remoteStatus: terminalStatus,
|
|
66
|
+
error: `delivery endpoint reported terminal status ${terminalStatus}`,
|
|
67
|
+
raw: body,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
52
70
|
if (!reused) {
|
|
53
71
|
return { kind: 'accepted', remoteId, remoteStatus, raw: body };
|
|
54
72
|
}
|
|
@@ -312,6 +312,19 @@ class OutboxWorker {
|
|
|
312
312
|
});
|
|
313
313
|
this.onDeliveryTerminal?.(row.deliveryId, ack, payload);
|
|
314
314
|
break;
|
|
315
|
+
case 'remote_failed':
|
|
316
|
+
// The provider persisted the record, but it will never publish and the
|
|
317
|
+
// idempotency key pins us to that same record forever: retrying cannot
|
|
318
|
+
// change the outcome. Terminal, so the failure reaches the Slot instead
|
|
319
|
+
// of being masked as a delivered cell. Content is kept for inspection.
|
|
320
|
+
this.database.deliveries.recordAck(row.deliveryId, {
|
|
321
|
+
status: 'failed',
|
|
322
|
+
remoteId: ack.remoteId,
|
|
323
|
+
remoteStatus: ack.remoteStatus,
|
|
324
|
+
error: ack.error,
|
|
325
|
+
});
|
|
326
|
+
this.onDeliveryTerminal?.(row.deliveryId, ack, payload);
|
|
327
|
+
break;
|
|
315
328
|
case 'permanent_failure':
|
|
316
329
|
// Deterministic rejection: still retry a couple times to survive a
|
|
317
330
|
// misconfigured blip, the outbox max-attempts then dead-letters it.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-ACK settlement: the ONE place a confirmed downstream delivery ACK
|
|
3
|
+
* becomes a Slot cell state.
|
|
4
|
+
*
|
|
5
|
+
* The outbox worker already recorded the durable delivery ledger outcome before
|
|
6
|
+
* calling here; this maps that ack onto the Slot FSM. Keeping it as a named,
|
|
7
|
+
* dependency-light function (instead of an inline closure in the scheduler
|
|
8
|
+
* runtime) is what makes the central invariant directly testable:
|
|
9
|
+
*
|
|
10
|
+
* REMOTE FAILURE MUST NOT BE REPORTED AS END-TO-END SUCCESS.
|
|
11
|
+
*
|
|
12
|
+
* A provider can answer HTTP 2xx while the record it persisted is terminally
|
|
13
|
+
* broken. The provider keys that record by OUR idempotency key, so a retry only
|
|
14
|
+
* returns the same broken record. Such an ack must settle the cell as `failed`,
|
|
15
|
+
* never promote it to `submitted` as though the content had been published.
|
|
16
|
+
*/
|
|
17
|
+
import type { Database } from '../storage/Database';
|
|
18
|
+
import type { DeliveryAck } from './DeliveryAck';
|
|
19
|
+
/**
|
|
20
|
+
* Apply a terminal delivery ack to the Slot cell that owns the delivery intent.
|
|
21
|
+
* Returns false when the delivery has no Slot cell (ad-hoc / batch runs), which
|
|
22
|
+
* is a normal no-op rather than an error.
|
|
23
|
+
*/
|
|
24
|
+
export declare function settleDeliveryTerminal(database: Database, deliveryId: string, ack: DeliveryAck): boolean;
|
|
25
|
+
//# sourceMappingURL=settleDeliveryTerminal.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.settleDeliveryTerminal = settleDeliveryTerminal;
|
|
4
|
+
const SlotCoordinator_1 = require("../scheduler/SlotCoordinator");
|
|
5
|
+
/**
|
|
6
|
+
* Apply a terminal delivery ack to the Slot cell that owns the delivery intent.
|
|
7
|
+
* Returns false when the delivery has no Slot cell (ad-hoc / batch runs), which
|
|
8
|
+
* is a normal no-op rather than an error.
|
|
9
|
+
*/
|
|
10
|
+
function settleDeliveryTerminal(database, deliveryId, ack) {
|
|
11
|
+
const row = database.deliveries.getById(deliveryId);
|
|
12
|
+
if (!row || !row.slotId || !row.targetId)
|
|
13
|
+
return false;
|
|
14
|
+
const coord = new SlotCoordinator_1.SlotCoordinator(database);
|
|
15
|
+
if (ack.kind === 'duplicate_existing') {
|
|
16
|
+
// Historical duplicate: a record from a DIFFERENT intent already exists, so
|
|
17
|
+
// THIS intent published nothing. Never a successful submission.
|
|
18
|
+
coord.applyOutcome(row.slotId, row.targetId, {
|
|
19
|
+
kind: 'duplicate',
|
|
20
|
+
workId: row.pixivId,
|
|
21
|
+
reason: 'downstream attested historical duplicate',
|
|
22
|
+
});
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (ack.kind === 'remote_failed') {
|
|
26
|
+
// The downstream record is terminally broken, so nothing was published:
|
|
27
|
+
// settle the cell as failed instead of promoting it to submitted. Not
|
|
28
|
+
// retryable — the same idempotency key can only return that record.
|
|
29
|
+
coord.applyOutcome(row.slotId, row.targetId, {
|
|
30
|
+
kind: 'failed',
|
|
31
|
+
retryable: false,
|
|
32
|
+
error: `downstream reported terminal status ${ack.remoteStatus} (review ${ack.remoteId ?? 'unknown'})`,
|
|
33
|
+
});
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// accepted / idempotent_replay: a confirmed ACK is the sole path to submitted.
|
|
37
|
+
coord.markDelivered(row.slotId, row.targetId, row.pixivId, row.workType);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=settleDeliveryTerminal.js.map
|
package/dist/package.json
CHANGED
package/dist/version.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BUILD = void 0;
|
|
4
4
|
// GENERATED by scripts/write-version.js — do not edit manually.
|
|
5
|
-
exports.BUILD = { version: '2.19.
|
|
5
|
+
exports.BUILD = { version: '2.19.4', commit: 'f331cd4bb164' };
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/dist/webui/package.json
CHANGED
package/package.json
CHANGED