pixivflow 3.1.0 → 3.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.
Files changed (61) hide show
  1. package/README.md +26 -1
  2. package/dist/commands/SchedulerCommand.js +31 -51
  3. package/dist/commands/scheduler-runtime.js +47 -7
  4. package/dist/config/defaults.d.ts +2 -0
  5. package/dist/config/defaults.js +6 -0
  6. package/dist/config/types.d.ts +20 -0
  7. package/dist/config/validation.js +11 -0
  8. package/dist/delivery/DeliveryDispatcher.d.ts +14 -0
  9. package/dist/delivery/DeliveryDispatcher.js +14 -0
  10. package/dist/delivery/EventCallbackDelivery.d.ts +54 -0
  11. package/dist/delivery/EventCallbackDelivery.js +89 -0
  12. package/dist/delivery/OutboxWorker.d.ts +11 -0
  13. package/dist/delivery/OutboxWorker.js +51 -1
  14. package/dist/package.json +1 -1
  15. package/dist/scheduler/CandidateSearchParams.d.ts +76 -0
  16. package/dist/scheduler/CandidateSearchParams.js +94 -0
  17. package/dist/scheduler/JobCancellation.d.ts +36 -0
  18. package/dist/scheduler/JobCancellation.js +73 -0
  19. package/dist/scheduler/JobEventStream.d.ts +94 -0
  20. package/dist/scheduler/JobEventStream.js +251 -0
  21. package/dist/scheduler/JobFacade.d.ts +298 -0
  22. package/dist/scheduler/JobFacade.js +622 -0
  23. package/dist/scheduler/JobProjection.d.ts +67 -0
  24. package/dist/scheduler/JobProjection.js +32 -0
  25. package/dist/scheduler/JobView.d.ts +34 -0
  26. package/dist/scheduler/JobView.js +82 -0
  27. package/dist/scheduler/ManualJobAdmission.d.ts +126 -0
  28. package/dist/scheduler/ManualJobAdmission.js +310 -0
  29. package/dist/scheduler/ManualJobService.d.ts +57 -0
  30. package/dist/scheduler/ManualJobService.js +91 -0
  31. package/dist/scheduler/ManualRefetchAdapter.d.ts +26 -0
  32. package/dist/scheduler/ManualRefetchAdapter.js +41 -0
  33. package/dist/scheduler/MultiScheduleManager.d.ts +17 -0
  34. package/dist/scheduler/MultiScheduleManager.js +68 -6
  35. package/dist/scheduler/ProtocolErrors.d.ts +68 -0
  36. package/dist/scheduler/ProtocolErrors.js +94 -0
  37. package/dist/scheduler/ScheduleTriggerServer.d.ts +56 -7
  38. package/dist/scheduler/ScheduleTriggerServer.js +166 -1
  39. package/dist/scheduler/SlotBusinessStatus.d.ts +5 -1
  40. package/dist/scheduler/SlotBusinessStatus.js +6 -0
  41. package/dist/scheduler/SlotCoordinator.d.ts +49 -5
  42. package/dist/scheduler/SlotCoordinator.js +89 -22
  43. package/dist/scheduler/StallSweep.d.ts +65 -0
  44. package/dist/scheduler/StallSweep.js +105 -0
  45. package/dist/scheduler/TargetOutcome.d.ts +39 -1
  46. package/dist/scheduler/TargetOutcome.js +51 -1
  47. package/dist/scheduler/ledger-time.d.ts +23 -0
  48. package/dist/scheduler/ledger-time.js +37 -0
  49. package/dist/storage/DatabaseMigration.js +48 -0
  50. package/dist/storage/repositories/DeliveryRepository.d.ts +6 -0
  51. package/dist/storage/repositories/DeliveryRepository.js +13 -0
  52. package/dist/storage/repositories/OutboxRepository.d.ts +73 -1
  53. package/dist/storage/repositories/OutboxRepository.js +142 -0
  54. package/dist/storage/repositories/SlotRepository.d.ts +34 -0
  55. package/dist/storage/repositories/SlotRepository.js +61 -2
  56. package/dist/version.js +1 -1
  57. package/dist/webui/package.json +1 -1
  58. package/examples/gateway/README.md +4 -0
  59. package/examples/onebot-adapter/README.md +139 -0
  60. package/examples/onebot-adapter/server.mjs +612 -0
  61. package/package.json +2 -1
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ /**
3
+ * The durable job event stream (Workflow Protocol v1 §events).
4
+ *
5
+ * The stream is a PROJECTION of `delivery_events` — the log the delivery
6
+ * subsystem already writes — plus ONE durable ack cursor
7
+ * (`job_event_cursors`). There is no second event queue and no second table
8
+ * family: `slot_id` is the job id, `ts` is the protocol `at`, and `event` is the
9
+ * internal kind translated by the single mapping in `JobFacade`.
10
+ *
11
+ * Three properties this module owns, because they are protocol obligations:
12
+ *
13
+ * 1. **A terminal job always has a terminal event.** `SlotCoordinator`'s
14
+ * `execution.summary` hook only fires for success/partial/failed, and
15
+ * cancelled/expired slots would never produce one. Instead of widening the
16
+ * slot/business-status logic, the lifecycle events are *reconciled from the
17
+ * durable projection*: whatever the ledger already implies must exist, is
18
+ * materialised exactly once (mirroring `reconcileScheduleSummaries`). The
19
+ * page is therefore never empty for a job that exists.
20
+ *
21
+ * 2. **Ack is monotonic, idempotent and side-effect free.** It writes one row
22
+ * in `job_event_cursors` and nothing else. An unknown or older cursor is a
23
+ * no-op that reports the durable state — never an error.
24
+ *
25
+ * 3. **A declared `callback_url` is never silently dropped.** The event row and
26
+ * its callback intent are committed in ONE transaction, and a sweep over
27
+ * durable rows repairs any job whose terminal event is still missing (for
28
+ * example because the process died mid-flight). Retry/dead-lettering stay in
29
+ * the existing outbox worker.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.JobEventStream = exports.EVENT_CALLBACK_MAX_ATTEMPTS = exports.EVENTS_PAGE_LIMIT = void 0;
33
+ const logger_1 = require("../logger");
34
+ const JobFacade_1 = require("./JobFacade");
35
+ const JobView_1 = require("./JobView");
36
+ const ProtocolErrors_1 = require("./ProtocolErrors");
37
+ /** One page of events. Bounded so a long-running job cannot stream unbounded JSON. */
38
+ exports.EVENTS_PAGE_LIMIT = 200;
39
+ /** Bounded callback budget: a flaky third-party URL must converge, not retry forever. */
40
+ exports.EVENT_CALLBACK_MAX_ATTEMPTS = 8;
41
+ /** How many callback-bearing jobs one reconcile sweep repairs. */
42
+ const RECONCILE_SWEEP_LIMIT = 50;
43
+ class JobEventStream {
44
+ deps;
45
+ constructor(deps) {
46
+ this.deps = deps;
47
+ }
48
+ get db() {
49
+ return this.deps.database;
50
+ }
51
+ /**
52
+ * `$defs/EventPage`. Throws 404 for an unknown job, exactly like
53
+ * `GET /jobs/:jobId`.
54
+ */
55
+ page(jobId, query) {
56
+ const job = this.requireJob(jobId);
57
+ // The ledger, not a timer, decides which lifecycle events exist.
58
+ this.reconcile(jobId);
59
+ const kinds = (0, JobFacade_1.projectedInternalKinds)();
60
+ const cursor = this.db.outbox.jobEventCursor(jobId);
61
+ const unackedPosition = cursor ? { at: cursor.at, rowId: cursor.rowId } : null;
62
+ const after = query.unackedOnly
63
+ ? unackedPosition
64
+ : this.resolveAfter(jobId, query.after);
65
+ const rows = this.db.outbox.listSlotEvents(jobId, kinds, after, exports.EVENTS_PAGE_LIMIT + 1);
66
+ const hasMore = rows.length > exports.EVENTS_PAGE_LIMIT;
67
+ const pageRows = hasMore ? rows.slice(0, exports.EVENTS_PAGE_LIMIT) : rows;
68
+ return (0, JobFacade_1.buildEventPage)({
69
+ jobId,
70
+ sources: pageRows.map(toSource),
71
+ job,
72
+ correlationId: job.correlation_id ?? null,
73
+ hasMore,
74
+ // `unacked` always counts from the durable cursor, never from `after`:
75
+ // a client paging backwards must not see the count move.
76
+ unacked: this.db.outbox.countSlotEvents(jobId, kinds, unackedPosition),
77
+ serverTime: this.deps.now(),
78
+ });
79
+ }
80
+ /**
81
+ * `$defs/AckResult`. `ack_through` is monotonic and idempotent: replaying it,
82
+ * or sending an unknown/older cursor, is a no-op that reports what is durably
83
+ * stored. Acking never touches the slot ledger.
84
+ */
85
+ ack(jobId, body) {
86
+ const { ackThrough } = (0, JobFacade_1.parseAckBody)(body);
87
+ this.requireJob(jobId);
88
+ this.reconcile(jobId);
89
+ const kinds = (0, JobFacade_1.projectedInternalKinds)();
90
+ const position = this.resolveAckTarget(jobId, ackThrough);
91
+ if (position)
92
+ this.db.outbox.advanceJobEventCursor(jobId, position, this.deps.now());
93
+ const cursor = this.db.outbox.jobEventCursor(jobId);
94
+ const unackedPosition = cursor ? { at: cursor.at, rowId: cursor.rowId } : null;
95
+ const unacked = this.db.outbox.countSlotEvents(jobId, kinds, unackedPosition);
96
+ const total = this.db.outbox.countSlotEvents(jobId, kinds, null);
97
+ return (0, JobFacade_1.buildAckResult)({
98
+ jobId,
99
+ acked: total - unacked,
100
+ unacked,
101
+ serverTime: this.deps.now(),
102
+ });
103
+ }
104
+ /**
105
+ * Materialise every lifecycle event the durable ledger already implies, at
106
+ * most once each, and enqueue the matching callback in the same transaction.
107
+ * `callbackUrl` is only read when THIS call is the one that first records the
108
+ * job's request: a replay never rewrites the declared callback.
109
+ */
110
+ reconcile(jobId, callbackUrl = null) {
111
+ const job = this.requireJob(jobId);
112
+ this.ensure(job.job_id, job, 'job.requested', job.created_at, callbackUrl);
113
+ if (job.started_at !== undefined && job.started_at !== null) {
114
+ this.ensure(job.job_id, job, 'job.execution_started', job.started_at, null);
115
+ }
116
+ const terminal = (0, JobFacade_1.terminalJobEventKind)(job.status);
117
+ if (terminal)
118
+ this.ensure(job.job_id, job, terminal, job.updated_at, null);
119
+ }
120
+ /**
121
+ * Repair sweep for jobs that declared a callback and have no durable terminal
122
+ * event yet, so a process that died before the terminal reconcile still
123
+ * delivers its terminal callback without waiting for a poll. Only jobs that
124
+ * already own a durable `job.requested` row are in scope: that is exactly the
125
+ * set whose callback endpoint is known, and re-running it can never queue a
126
+ * second copy (the event_id is the outbox idempotency key). A job that never
127
+ * recorded its request event is repaired on the consumer's next read instead.
128
+ * Returns how many jobs it looked at, so a caller can assert it drains.
129
+ */
130
+ reconcileOutstanding(limit = RECONCILE_SWEEP_LIMIT) {
131
+ const slotIds = this.db.outbox.slotsAwaitingTerminalEvent(JobFacade_1.TERMINAL_JOB_EVENT_KINDS, limit);
132
+ for (const slotId of slotIds) {
133
+ try {
134
+ this.reconcile(slotId);
135
+ }
136
+ catch (error) {
137
+ // One broken job must never stop the outbox pump.
138
+ logger_1.logger.warn('Job event reconcile failed', { slotId, error });
139
+ }
140
+ }
141
+ return slotIds.length;
142
+ }
143
+ ensure(jobId, job, kind, at, callbackUrl) {
144
+ if (!job)
145
+ return;
146
+ // One commit: "the event is durable" and "the callback is owed" cannot
147
+ // diverge, so a crash can never lose a declared callback.
148
+ this.db.transaction(() => {
149
+ const write = this.db.outbox.recordSlotEventOnce({
150
+ slotId: jobId,
151
+ event: kind,
152
+ at,
153
+ deliveryTarget: callbackUrl,
154
+ });
155
+ if (!write.inserted)
156
+ return;
157
+ // Read the callback back from the durable declaration rather than from the
158
+ // argument, so the terminal event and the request event can never name
159
+ // different endpoints.
160
+ const declared = this.declaredCallbackUrl(jobId);
161
+ if (!declared)
162
+ return;
163
+ this.enqueueCallback(job, write.event, declared);
164
+ });
165
+ }
166
+ /** The `callback_url` a job declared, read from its durable request event. */
167
+ declaredCallbackUrl(jobId) {
168
+ return this.db.outbox.slotEvent(jobId, 'job.requested')?.deliveryTarget ?? null;
169
+ }
170
+ enqueueCallback(job, row, url) {
171
+ const payload = {
172
+ event: null,
173
+ job_id: job.job_id,
174
+ context: { slotId: job.job_id },
175
+ };
176
+ // The body is built by the SAME builder the endpoint serves, so a callback
177
+ // can never carry a shape the reader would not produce.
178
+ const body = this.eventBodyFor(job, row);
179
+ if (!body)
180
+ return;
181
+ payload.event = body;
182
+ this.db.outbox.enqueue({
183
+ kind: 'event_callback',
184
+ deliveryTarget: url,
185
+ // Reuses `idx_outbox_key`: replaying an admission, or re-running the
186
+ // sweep, can never queue a second copy of the same event.
187
+ idempotencyKey: `job-event:${job.job_id}:${body.event_id}`,
188
+ payload,
189
+ maxAttempts: exports.EVENT_CALLBACK_MAX_ATTEMPTS,
190
+ });
191
+ }
192
+ eventBodyFor(job, row) {
193
+ const page = (0, JobFacade_1.buildEventPage)({
194
+ jobId: job.job_id,
195
+ sources: [toSource(row)],
196
+ job: this.requireJob(job.job_id),
197
+ correlationId: job.correlation_id ?? null,
198
+ hasMore: false,
199
+ unacked: 0,
200
+ serverTime: this.deps.now(),
201
+ });
202
+ return page.events[0] ?? null;
203
+ }
204
+ requireJob(jobId) {
205
+ const job = (0, JobView_1.protocolJobBySlotId)(this.deps, jobId);
206
+ if (!job) {
207
+ throw new ProtocolErrors_1.ProtocolRequestError('invalid_params', 404, { message: 'unknown job' });
208
+ }
209
+ return job;
210
+ }
211
+ /**
212
+ * `?after=` is a "give me everything after this" hint. An unrecognised or
213
+ * foreign cursor cannot filter, so it is treated as absent and the consumer
214
+ * sees the whole stream again — `event_id` is the dedupe key, and at-least-once
215
+ * is the contract.
216
+ */
217
+ resolveAfter(jobId, after) {
218
+ if (!after)
219
+ return null;
220
+ const position = (0, JobFacade_1.parseProtocolEventId)(after);
221
+ if (!position)
222
+ return null;
223
+ return this.durableProjectedPosition(jobId, position);
224
+ }
225
+ /**
226
+ * Map an opaque `event_id` back to a durable position. Unknown ids, ids of
227
+ * other jobs and internal-only rows all resolve to null, which callers treat
228
+ * as "no-op" — never as an error.
229
+ */
230
+ resolveAckTarget(jobId, ackThrough) {
231
+ const position = (0, JobFacade_1.parseProtocolEventId)(ackThrough);
232
+ if (!position)
233
+ return null;
234
+ return this.durableProjectedPosition(jobId, position);
235
+ }
236
+ durableProjectedPosition(jobId, position) {
237
+ const row = this.db.outbox.slotEventById(jobId, position.rowId);
238
+ if (!row)
239
+ return null;
240
+ // Only an event the consumer can actually see may move a cursor: acking an
241
+ // internal-only row would silently mark projectable events as read.
242
+ if (!(0, JobFacade_1.isProjectedInternalKind)(row.event))
243
+ return null;
244
+ return { at: row.ts, rowId: row.id };
245
+ }
246
+ }
247
+ exports.JobEventStream = JobEventStream;
248
+ function toSource(row) {
249
+ return { rowId: row.id, at: row.ts, internalKind: row.event };
250
+ }
251
+ //# sourceMappingURL=JobEventStream.js.map
@@ -0,0 +1,298 @@
1
+ /**
2
+ * The producer-side projection layer of Workflow Protocol v1 (§11.1).
3
+ *
4
+ * Everything a consumer sees about a job is built here from the SAME durable
5
+ * ledger the legacy endpoints read: no second state store, no parallel status
6
+ * machine, no consumer vocabulary. The projection deliberately speaks only
7
+ * protocol words — `job_id`, `status`, `progress`, `error` — while the internal
8
+ * `TerminalReasonCode` is preserved inside `error.detail.internal_code` for
9
+ * diagnosis and mapped to the closed protocol code at this single site.
10
+ *
11
+ * `GET /capabilities` is likewise derived from the live configuration, so a
12
+ * config change (budget, deadline ceiling) is reflected without a code change.
13
+ */
14
+ import { StandaloneConfig } from '../config';
15
+ import { CandidateSearchParams } from './CandidateSearchParams';
16
+ import { JobStatusProjection } from './JobProjection';
17
+ import { ProtocolErrorBody } from './ProtocolErrors';
18
+ import { CandidateSupplyReport } from './TargetOutcome';
19
+ /** The only job type this producer serves today. */
20
+ export declare const JOB_TYPE_CANDIDATE_SEARCH = "candidate_search";
21
+ /** `$defs/ProtocolVersion` — this producer speaks exactly one version. */
22
+ export declare const PROTOCOL_VERSIONS: readonly string[];
23
+ /** `$defs/Job.status`. */
24
+ export type ProtocolJobStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'expired';
25
+ /** `$defs/Progress`: `stage` is required, everything else is optional. */
26
+ export interface ProtocolJobProgress {
27
+ stage: string;
28
+ message?: string;
29
+ at: number;
30
+ }
31
+ /**
32
+ * `$defs/Job`, as this producer emits it.
33
+ *
34
+ * Field names are the contract and are asserted against the vendored schema by
35
+ * `src/__tests__/protocol/job-facade.test.ts`. Note there is no `slot*`, no
36
+ * `refetch*` and no consumer-specific key here, by design (§8).
37
+ */
38
+ export interface ProtocolJob {
39
+ protocol_version: string;
40
+ job_id: string;
41
+ job_type: string;
42
+ status: ProtocolJobStatus;
43
+ idempotency_key?: string;
44
+ correlation_id?: string;
45
+ created_at: number;
46
+ updated_at: number;
47
+ started_at?: number | null;
48
+ heartbeat_at?: number | null;
49
+ deadline_at?: number | null;
50
+ lease_active: boolean;
51
+ lease_expires_at?: number | null;
52
+ attempt?: number;
53
+ progress: ProtocolJobProgress;
54
+ result?: Record<string, unknown>;
55
+ /** Always built by `protocolErrorBody`, so the code stays in the closed enum. */
56
+ error?: ProtocolErrorBody;
57
+ events_url?: string;
58
+ }
59
+ /** `$defs/JobTypeDeclaration`. */
60
+ export interface ProtocolJobTypeDeclaration {
61
+ name: string;
62
+ params_schema: string;
63
+ result_schema: string;
64
+ features?: string[];
65
+ queued_timeout_ms?: number;
66
+ stall_timeout_ms?: number;
67
+ heartbeat_interval_ms?: number;
68
+ default_deadline_ms?: number;
69
+ }
70
+ /** `$defs/Capabilities`. */
71
+ export interface ProtocolCapabilities {
72
+ protocol_versions: string[];
73
+ job_types: ProtocolJobTypeDeclaration[];
74
+ server_time?: number;
75
+ }
76
+ /** `$defs/Candidate`, built from the work this job actually delivered. */
77
+ export interface ProtocolDeliveredWork {
78
+ workId: string | null;
79
+ workType: string | null;
80
+ }
81
+ export interface ProtocolJobInput {
82
+ jobType?: string;
83
+ /** The shared ledger projection (`buildJobProjection`). */
84
+ projection: JobStatusProjection;
85
+ /** The execution ceiling this deployment enforces for the job, if known. */
86
+ deadlineAt?: number | null;
87
+ /** The single work item a succeeded job delivered. */
88
+ delivered?: ProtocolDeliveredWork | null;
89
+ /** The persisted candidate-supply funnel (counts only, by design). */
90
+ supply?: CandidateSupplyReport | null;
91
+ /** Injected clock, for tests. */
92
+ now?: number;
93
+ }
94
+ /**
95
+ * Classify the durable cell state.
96
+ *
97
+ * The CELL is the truth: it is the unit that executes and the unit the manual
98
+ * surface submits. Budget verdicts (`queued_too_long`, `stalled_no_heartbeat`,
99
+ * `execution_timeout`) end as protocol `expired` per §4 — they describe a lost
100
+ * liveness, not a Pixiv failure — while a consumer cancel is `cancelled`.
101
+ */
102
+ export declare function protocolJobStatus(projection: JobStatusProjection): ProtocolJobStatus;
103
+ /** The protocol-visible stage label for a job status (§2 `Progress.stage`). */
104
+ export declare function protocolProgressStage(status: ProtocolJobStatus, cellStatus: string): string;
105
+ /**
106
+ * Build the `$defs/Job` body for one job.
107
+ *
108
+ * A terminal job always carries `result` or `error` (§4). The result is built
109
+ * from what the ledger actually knows — the delivered work item and the
110
+ * persisted supply counts — never from a re-derived guestimate: `filtered` is
111
+ * omitted because per-work rejection reasons are not persisted (only counts).
112
+ */
113
+ export declare function buildProtocolJob(input: ProtocolJobInput): ProtocolJob;
114
+ /**
115
+ * The execution ceiling this deployment enforces for one schedule's work:
116
+ * the plan's configured `timeout`, else the scheduler-wide default.
117
+ */
118
+ export declare function resolveDeadlineMs(config: StandaloneConfig, scheduleId: string): number;
119
+ /**
120
+ * The declared `default_deadline_ms`: the widest ceiling any enabled plan
121
+ * grants, so the declaration never promises more time than config allows, and
122
+ * never less than a plan that does grant it.
123
+ */
124
+ export declare function resolveDefaultDeadlineMs(config: StandaloneConfig): number;
125
+ /** A `$defs/Task` this producer accepted, normalized for the admission. */
126
+ export interface ParsedTask {
127
+ idempotencyKey: string;
128
+ correlationId?: string;
129
+ /** `params.source.account` — the Pixiv resource identity to assert. */
130
+ account?: string;
131
+ /** `params` — the occurrence-scoped retrieval view. */
132
+ params: CandidateSearchParams;
133
+ /**
134
+ * Validated but not enforced in this phase: there is no durable column for a
135
+ * consumer-supplied ceiling yet, so `deadline_at` reports the ceiling the
136
+ * producer actually enforces (see `resolveDeadlineMs`) instead of promising
137
+ * one it would not honour.
138
+ */
139
+ deadlineMs?: number;
140
+ /**
141
+ * Validated and accepted. Carried durably by the `delivery_events` row for
142
+ * this Task's `job.requested` event and delivered by the outbox
143
+ * (`event_callback` rows) — `src/scheduler/JobEventStream.ts`.
144
+ */
145
+ callbackUrl?: string;
146
+ }
147
+ /**
148
+ * Parse and validate a `$defs/Task` body (§2/§3).
149
+ *
150
+ * Strict about the documented fields — a wrong type or an out-of-range value is
151
+ * a 400 `invalid_params`, never a silently coerced default — and deliberately
152
+ * tolerant about unknown keys, which §3 requires to be ignored so a newer
153
+ * consumer can talk to an older producer.
154
+ *
155
+ * `protocol_version` is judged by kind: absent is a malformed request
156
+ * (`invalid_params`), present-but-different is a version mismatch
157
+ * (`unsupported_protocol_version`). `job_type` uses `invalid_params`; the
158
+ * published error enum has no `unsupported_job_type` member and this producer
159
+ * does not invent protocol codes.
160
+ */
161
+ export declare function parseTaskBody(body: unknown): ParsedTask;
162
+ /**
163
+ * `$defs/Capabilities`, derived from the live configuration: change a budget in
164
+ * config and this declaration follows without a code change.
165
+ */
166
+ export declare function buildCapabilities(config: StandaloneConfig, now?: number): ProtocolCapabilities;
167
+ export declare const PROTOCOL_EVENT_TYPES: readonly ["job.accepted", "job.started", "job.progress", "job.succeeded", "job.failed", "job.expired", "job.cancelled"];
168
+ export type ProtocolEventType = (typeof PROTOCOL_EVENT_TYPES)[number];
169
+ /**
170
+ * The internal job-lifecycle kinds PixivFlow persists for a job. They are
171
+ * deliberately NOT spelled like the protocol enum: the translation is a
172
+ * decision made here, never a string passthrough.
173
+ */
174
+ export declare const JOB_EVENT_KINDS: readonly ["job.requested", "job.execution_started", "job.progressed", "job.outcome_succeeded", "job.outcome_failed", "job.outcome_expired", "job.outcome_cancelled"];
175
+ export type JobEventKind = (typeof JOB_EVENT_KINDS)[number];
176
+ export declare const TERMINAL_JOB_EVENT_KINDS: readonly JobEventKind[];
177
+ /** The internal kind that materialises the terminal event of a finished job. */
178
+ export declare function terminalJobEventKind(status: ProtocolJobStatus): JobEventKind | null;
179
+ /**
180
+ * The ONE mapping from durable internal kinds to protocol event types.
181
+ *
182
+ * `null` means internal-only: the row stays in `delivery_events` for operators
183
+ * and `runs show`, and never reaches a consumer. Any kind absent from this map
184
+ * is DROPPED with a warning — an internal kind can never become an unknown
185
+ * protocol type by accident. The intersection type below makes the mapping
186
+ * exhaustive over `JobEventKind` at compile time, so adding a new lifecycle kind
187
+ * without mapping it fails `tsc` instead of shipping.
188
+ */
189
+ export declare const PROTOCOL_EVENT_FOR_INTERNAL_KIND: Record<string, ProtocolEventType | null> & Record<JobEventKind, ProtocolEventType>;
190
+ /**
191
+ * The internal kinds a consumer is allowed to see. Callers use this as the SQL
192
+ * filter so `unacked` can never be pinned by rows that are not projectable.
193
+ */
194
+ export declare function projectedInternalKinds(): string[];
195
+ /** True when an internal kind is visible to consumers (has a protocol type). */
196
+ export declare function isProjectedInternalKind(internalKind: string): boolean;
197
+ /** Map one internal kind to its protocol type; null when internal-only. */
198
+ export declare function protocolEventTypeFor(internalKind: string): ProtocolEventType | null;
199
+ export interface ProtocolEventPayload {
200
+ job?: ProtocolJob | null;
201
+ result?: Record<string, unknown> | null;
202
+ error?: ProtocolErrorBody | null;
203
+ /**
204
+ * Non-normative producer detail. Consumers MUST ignore unknown keys; this is
205
+ * where internal vocabulary (`internal_kind`) is disclosed without polluting
206
+ * the protocol fields above.
207
+ */
208
+ detail?: Record<string, unknown> | null;
209
+ }
210
+ export interface ProtocolEvent {
211
+ protocol_version: string;
212
+ /** Consumers deduplicate on this. Also the ack cursor. */
213
+ event_id: string;
214
+ job_id: string;
215
+ type: ProtocolEventType;
216
+ at: number;
217
+ correlation_id?: string;
218
+ payload?: ProtocolEventPayload;
219
+ }
220
+ export interface ProtocolEventPage {
221
+ job_id: string;
222
+ events: ProtocolEvent[];
223
+ /** Present only when more events remain after this page. */
224
+ next_after?: string;
225
+ unacked?: number;
226
+ server_time?: number;
227
+ }
228
+ export interface ProtocolAckResult {
229
+ job_id: string;
230
+ acked: number;
231
+ unacked: number;
232
+ server_time?: number;
233
+ }
234
+ /**
235
+ * A durable event row as the stream sees it. Deliberately a plain shape so the
236
+ * facade stays independent of the storage layer's row type.
237
+ */
238
+ export interface ProtocolEventSource {
239
+ /** `delivery_events.id` — half of the ack cursor identity. */
240
+ rowId: number;
241
+ /** `delivery_events.ts` — the honest instant of the transition. */
242
+ at: number;
243
+ internalKind: string;
244
+ }
245
+ /** `evt-<at>-<row id>`: opaque, unique, and resolvable back to the row. */
246
+ export declare function protocolEventId(at: number, rowId: number): string;
247
+ /**
248
+ * A position in a job's durable event log. Structurally identical to the storage
249
+ * layer's cursor position; declared here so the facade does not depend on the
250
+ * storage layer's types.
251
+ */
252
+ export interface ProtocolEventPosition {
253
+ at: number;
254
+ rowId: number;
255
+ }
256
+ /** Resolve an opaque event_id to a durable position; null when unrecognised. */
257
+ export declare function parseProtocolEventId(value: string): ProtocolEventPosition | null;
258
+ /** Build one protocol event, or null when the internal kind is internal-only. */
259
+ export declare function buildProtocolEvent(input: {
260
+ jobId: string;
261
+ source: ProtocolEventSource;
262
+ job?: ProtocolJob | null;
263
+ correlationId?: string | null;
264
+ }): ProtocolEvent | null;
265
+ /**
266
+ * `$defs/EventPage` from durable rows. `events` is ordered by `at` (then by the
267
+ * durable row order, which is the same order because writers never move `at`
268
+ * backwards).
269
+ */
270
+ export declare function buildEventPage(input: {
271
+ jobId: string;
272
+ sources: readonly ProtocolEventSource[];
273
+ job?: ProtocolJob | null;
274
+ correlationId?: string | null;
275
+ hasMore: boolean;
276
+ unacked: number;
277
+ serverTime: number;
278
+ }): ProtocolEventPage;
279
+ /** `$defs/AckResult` from the durable cursor. */
280
+ export declare function buildAckResult(input: {
281
+ jobId: string;
282
+ acked: number;
283
+ unacked: number;
284
+ serverTime: number;
285
+ }): ProtocolAckResult;
286
+ /** `$defs/AckRequest`. */
287
+ export declare function parseAckBody(body: unknown): {
288
+ ackThrough: string;
289
+ };
290
+ /** Query parameters of `GET /jobs/:jobId/events`. */
291
+ export declare function parseEventQuery(query: {
292
+ after?: unknown;
293
+ unacked?: unknown;
294
+ }): {
295
+ after: string | null;
296
+ unackedOnly: boolean;
297
+ };
298
+ //# sourceMappingURL=JobFacade.d.ts.map