@happyvertical/jobs 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/dist/adapters/bull.d.ts +103 -0
- package/dist/adapters/bull.d.ts.map +1 -0
- package/dist/adapters/bull.js +349 -0
- package/dist/adapters/bull.js.map +1 -0
- package/dist/adapters/bullmq.d.ts +85 -0
- package/dist/adapters/bullmq.d.ts.map +1 -0
- package/dist/adapters/bullmq.js +391 -0
- package/dist/adapters/bullmq.js.map +1 -0
- package/dist/adapters/cloud-tasks.d.ts +110 -0
- package/dist/adapters/cloud-tasks.d.ts.map +1 -0
- package/dist/adapters/cloud-tasks.js +336 -0
- package/dist/adapters/cloud-tasks.js.map +1 -0
- package/dist/adapters/postgres.d.ts +55 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +437 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +44 -0
- package/dist/adapters/sqlite.d.ts.map +1 -0
- package/dist/adapters/sqlite.js +323 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/adapters/sqs.d.ts +112 -0
- package/dist/adapters/sqs.d.ts.map +1 -0
- package/dist/adapters/sqs.js +411 -0
- package/dist/adapters/sqs.js.map +1 -0
- package/dist/base-store.d.ts +69 -0
- package/dist/base-store.d.ts.map +1 -0
- package/dist/chunks/base-store-DlNksWvQ.js +324 -0
- package/dist/chunks/base-store-DlNksWvQ.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/dist/retry.d.ts +84 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/types.d.ts +311 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/worker.d.ts +74 -0
- package/dist/worker.d.ts.map +1 -0
- package/metadata.json +34 -0
- package/package.json +114 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { getDatabase } from "@happyvertical/sql";
|
|
2
|
+
import { B as BaseJobStore, v as validateTableName } from "../chunks/base-store-DlNksWvQ.js";
|
|
3
|
+
class PostgresJobStore extends BaseJobStore {
|
|
4
|
+
db = null;
|
|
5
|
+
url;
|
|
6
|
+
externalDb;
|
|
7
|
+
tableName;
|
|
8
|
+
enableNotify;
|
|
9
|
+
notifyChannel;
|
|
10
|
+
notifyListeners = /* @__PURE__ */ new Set();
|
|
11
|
+
listening = false;
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
super();
|
|
14
|
+
this.url = config.url ?? process.env.DATABASE_URL ?? "";
|
|
15
|
+
this.externalDb = config.db ?? null;
|
|
16
|
+
this.tableName = validateTableName(config.tableName ?? "_jobs");
|
|
17
|
+
this.enableNotify = config.enableNotify ?? true;
|
|
18
|
+
this.notifyChannel = validateTableName(
|
|
19
|
+
config.notifyChannel ?? "job_events"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
if (this.initialized) return;
|
|
24
|
+
this.db = this.externalDb ?? await getDatabase({ type: "postgres", url: this.url });
|
|
25
|
+
await this.db.query(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
queue TEXT NOT NULL DEFAULT 'default',
|
|
29
|
+
payload JSONB NOT NULL,
|
|
30
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
31
|
+
priority INTEGER NOT NULL DEFAULT 50,
|
|
32
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
34
|
+
run_at TIMESTAMPTZ NOT NULL,
|
|
35
|
+
started_at TIMESTAMPTZ,
|
|
36
|
+
completed_at TIMESTAMPTZ,
|
|
37
|
+
timeout INTEGER NOT NULL DEFAULT 300000,
|
|
38
|
+
timeout_behavior TEXT NOT NULL DEFAULT 'fail',
|
|
39
|
+
last_error TEXT,
|
|
40
|
+
result_pointer TEXT,
|
|
41
|
+
retry_strategy JSONB NOT NULL,
|
|
42
|
+
worker_id TEXT,
|
|
43
|
+
worker_heartbeat TIMESTAMPTZ,
|
|
44
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
45
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
await this.db.query(`
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_dequeue
|
|
50
|
+
ON ${this.tableName} (status, queue, run_at, priority DESC)
|
|
51
|
+
WHERE status = 'pending'
|
|
52
|
+
`);
|
|
53
|
+
await this.db.query(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at
|
|
55
|
+
ON ${this.tableName} (created_at)
|
|
56
|
+
`);
|
|
57
|
+
await this.db.query(`
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_queue
|
|
59
|
+
ON ${this.tableName} (queue)
|
|
60
|
+
`);
|
|
61
|
+
if (this.enableNotify) {
|
|
62
|
+
await this.setupNotifyTriggers();
|
|
63
|
+
}
|
|
64
|
+
this.initialized = true;
|
|
65
|
+
}
|
|
66
|
+
async setupNotifyTriggers() {
|
|
67
|
+
if (!this.db) return;
|
|
68
|
+
await this.db.query(`
|
|
69
|
+
CREATE OR REPLACE FUNCTION ${this.tableName}_notify_created()
|
|
70
|
+
RETURNS TRIGGER AS $$
|
|
71
|
+
BEGIN
|
|
72
|
+
PERFORM pg_notify('${this.notifyChannel}', json_build_object(
|
|
73
|
+
'event', 'created',
|
|
74
|
+
'id', NEW.id,
|
|
75
|
+
'queue', NEW.queue,
|
|
76
|
+
'priority', NEW.priority,
|
|
77
|
+
'run_at', NEW.run_at
|
|
78
|
+
)::text);
|
|
79
|
+
RETURN NEW;
|
|
80
|
+
END;
|
|
81
|
+
$$ LANGUAGE plpgsql
|
|
82
|
+
`);
|
|
83
|
+
await this.db.query(
|
|
84
|
+
`DROP TRIGGER IF EXISTS ${this.tableName}_created_trigger ON ${this.tableName}`
|
|
85
|
+
);
|
|
86
|
+
await this.db.query(`
|
|
87
|
+
CREATE TRIGGER ${this.tableName}_created_trigger
|
|
88
|
+
AFTER INSERT ON ${this.tableName}
|
|
89
|
+
FOR EACH ROW EXECUTE FUNCTION ${this.tableName}_notify_created()
|
|
90
|
+
`);
|
|
91
|
+
await this.db.query(`
|
|
92
|
+
CREATE OR REPLACE FUNCTION ${this.tableName}_notify_ready()
|
|
93
|
+
RETURNS TRIGGER AS $$
|
|
94
|
+
BEGIN
|
|
95
|
+
IF NEW.status = 'pending' AND NEW.run_at <= NOW() AND
|
|
96
|
+
(OLD.run_at > NOW() OR OLD IS NULL) THEN
|
|
97
|
+
PERFORM pg_notify('${this.notifyChannel}', json_build_object(
|
|
98
|
+
'event', 'ready',
|
|
99
|
+
'id', NEW.id,
|
|
100
|
+
'queue', NEW.queue,
|
|
101
|
+
'priority', NEW.priority
|
|
102
|
+
)::text);
|
|
103
|
+
END IF;
|
|
104
|
+
RETURN NEW;
|
|
105
|
+
END;
|
|
106
|
+
$$ LANGUAGE plpgsql
|
|
107
|
+
`);
|
|
108
|
+
await this.db.query(
|
|
109
|
+
`DROP TRIGGER IF EXISTS ${this.tableName}_ready_trigger ON ${this.tableName}`
|
|
110
|
+
);
|
|
111
|
+
await this.db.query(`
|
|
112
|
+
CREATE TRIGGER ${this.tableName}_ready_trigger
|
|
113
|
+
AFTER UPDATE ON ${this.tableName}
|
|
114
|
+
FOR EACH ROW EXECUTE FUNCTION ${this.tableName}_notify_ready()
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Start listening for PostgreSQL notifications
|
|
119
|
+
* This enables push-based job retrieval
|
|
120
|
+
*/
|
|
121
|
+
async startListening() {
|
|
122
|
+
if (!this.db || !this.enableNotify || this.listening) return;
|
|
123
|
+
try {
|
|
124
|
+
await this.db.query(`LISTEN ${this.notifyChannel}`);
|
|
125
|
+
this.listening = true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn("Could not start LISTEN, falling back to polling:", error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async stopListening() {
|
|
131
|
+
if (!this.db || !this.listening) return;
|
|
132
|
+
try {
|
|
133
|
+
await this.db.query(`UNLISTEN ${this.notifyChannel}`);
|
|
134
|
+
this.listening = false;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async enqueue(options) {
|
|
139
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
140
|
+
const job = this.createJobRecord(options);
|
|
141
|
+
await this.db.insert(this.tableName, {
|
|
142
|
+
id: job.id,
|
|
143
|
+
queue: job.queue,
|
|
144
|
+
payload: JSON.stringify(job.payload),
|
|
145
|
+
status: job.status,
|
|
146
|
+
priority: job.priority,
|
|
147
|
+
attempts: job.attempts,
|
|
148
|
+
max_attempts: job.maxAttempts,
|
|
149
|
+
run_at: job.runAt.toISOString(),
|
|
150
|
+
started_at: job.startedAt?.toISOString() ?? null,
|
|
151
|
+
completed_at: job.completedAt?.toISOString() ?? null,
|
|
152
|
+
timeout: job.timeout,
|
|
153
|
+
timeout_behavior: job.timeoutBehavior,
|
|
154
|
+
last_error: job.lastError,
|
|
155
|
+
result_pointer: job.resultPointer,
|
|
156
|
+
retry_strategy: JSON.stringify(job.retryStrategy),
|
|
157
|
+
worker_id: job.workerId,
|
|
158
|
+
worker_heartbeat: job.workerHeartbeat?.toISOString() ?? null,
|
|
159
|
+
created_at: job.createdAt.toISOString(),
|
|
160
|
+
updated_at: job.updatedAt.toISOString()
|
|
161
|
+
});
|
|
162
|
+
await this.emitEvent("job.created", job);
|
|
163
|
+
if (job.runAt <= /* @__PURE__ */ new Date()) {
|
|
164
|
+
await this.emitEvent("job.ready", job);
|
|
165
|
+
}
|
|
166
|
+
return job;
|
|
167
|
+
}
|
|
168
|
+
async dequeue(queues, limit, workerId) {
|
|
169
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
170
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
171
|
+
const queuePlaceholders = queues.map((_, i) => `$${i + 1}`).join(", ");
|
|
172
|
+
const { rows } = await this.db.query(
|
|
173
|
+
`
|
|
174
|
+
UPDATE ${this.tableName}
|
|
175
|
+
SET status = 'running',
|
|
176
|
+
worker_id = $${queues.length + 1},
|
|
177
|
+
worker_heartbeat = $${queues.length + 2},
|
|
178
|
+
started_at = $${queues.length + 2},
|
|
179
|
+
attempts = attempts + 1,
|
|
180
|
+
updated_at = $${queues.length + 2}
|
|
181
|
+
WHERE id IN (
|
|
182
|
+
SELECT id FROM ${this.tableName}
|
|
183
|
+
WHERE status = 'pending'
|
|
184
|
+
AND queue IN (${queuePlaceholders})
|
|
185
|
+
AND run_at <= $${queues.length + 2}
|
|
186
|
+
ORDER BY priority DESC, run_at ASC
|
|
187
|
+
LIMIT $${queues.length + 3}
|
|
188
|
+
FOR UPDATE SKIP LOCKED
|
|
189
|
+
)
|
|
190
|
+
RETURNING *
|
|
191
|
+
`,
|
|
192
|
+
[...queues, workerId, now, limit]
|
|
193
|
+
);
|
|
194
|
+
const jobs = rows.map(
|
|
195
|
+
(row) => this.parseJobRow(row)
|
|
196
|
+
);
|
|
197
|
+
for (const job of jobs) {
|
|
198
|
+
await this.emitEvent("job.started", job);
|
|
199
|
+
}
|
|
200
|
+
return jobs;
|
|
201
|
+
}
|
|
202
|
+
async update(id, updates) {
|
|
203
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
204
|
+
const setClause = [];
|
|
205
|
+
const params = [];
|
|
206
|
+
let paramIndex = 1;
|
|
207
|
+
const fieldMap = {
|
|
208
|
+
queue: "queue",
|
|
209
|
+
payload: "payload",
|
|
210
|
+
status: "status",
|
|
211
|
+
priority: "priority",
|
|
212
|
+
attempts: "attempts",
|
|
213
|
+
maxAttempts: "max_attempts",
|
|
214
|
+
runAt: "run_at",
|
|
215
|
+
startedAt: "started_at",
|
|
216
|
+
completedAt: "completed_at",
|
|
217
|
+
timeout: "timeout",
|
|
218
|
+
timeoutBehavior: "timeout_behavior",
|
|
219
|
+
lastError: "last_error",
|
|
220
|
+
resultPointer: "result_pointer",
|
|
221
|
+
retryStrategy: "retry_strategy",
|
|
222
|
+
workerId: "worker_id",
|
|
223
|
+
workerHeartbeat: "worker_heartbeat"
|
|
224
|
+
};
|
|
225
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
226
|
+
const column = fieldMap[key];
|
|
227
|
+
if (!column) continue;
|
|
228
|
+
setClause.push(`${column} = $${paramIndex}`);
|
|
229
|
+
if (value instanceof Date) {
|
|
230
|
+
params.push(value.toISOString());
|
|
231
|
+
} else if (typeof value === "object" && value !== null) {
|
|
232
|
+
params.push(JSON.stringify(value));
|
|
233
|
+
} else {
|
|
234
|
+
params.push(value);
|
|
235
|
+
}
|
|
236
|
+
paramIndex++;
|
|
237
|
+
}
|
|
238
|
+
if (setClause.length === 0) {
|
|
239
|
+
const job2 = await this.get(id);
|
|
240
|
+
if (!job2) throw new Error(`Job not found: ${id}`);
|
|
241
|
+
return job2;
|
|
242
|
+
}
|
|
243
|
+
setClause.push(`updated_at = $${paramIndex}`);
|
|
244
|
+
params.push((/* @__PURE__ */ new Date()).toISOString());
|
|
245
|
+
paramIndex++;
|
|
246
|
+
params.push(id);
|
|
247
|
+
await this.db.query(
|
|
248
|
+
`UPDATE ${this.tableName} SET ${setClause.join(", ")} WHERE id = $${paramIndex}`,
|
|
249
|
+
params
|
|
250
|
+
);
|
|
251
|
+
const job = await this.get(id);
|
|
252
|
+
if (!job) throw new Error(`Job not found after update: ${id}`);
|
|
253
|
+
if (updates.status === "completed") {
|
|
254
|
+
await this.emitEvent("job.completed", job, {
|
|
255
|
+
resultPointer: job.resultPointer ?? void 0
|
|
256
|
+
});
|
|
257
|
+
} else if (updates.status === "failed") {
|
|
258
|
+
await this.emitEvent("job.failed", job, {
|
|
259
|
+
error: job.lastError ?? void 0
|
|
260
|
+
});
|
|
261
|
+
} else if (updates.status === "cancelled") {
|
|
262
|
+
await this.emitEvent("job.cancelled", job);
|
|
263
|
+
}
|
|
264
|
+
return job;
|
|
265
|
+
}
|
|
266
|
+
async get(id) {
|
|
267
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
268
|
+
const row = await this.db.get(this.tableName, { id });
|
|
269
|
+
if (!row) return null;
|
|
270
|
+
return this.parseJobRow(row);
|
|
271
|
+
}
|
|
272
|
+
async list(filter) {
|
|
273
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
274
|
+
const conditions = [];
|
|
275
|
+
const params = [];
|
|
276
|
+
let paramIndex = 1;
|
|
277
|
+
if (filter.queue) {
|
|
278
|
+
conditions.push(`queue = $${paramIndex++}`);
|
|
279
|
+
params.push(filter.queue);
|
|
280
|
+
}
|
|
281
|
+
if (filter.status) {
|
|
282
|
+
if (Array.isArray(filter.status)) {
|
|
283
|
+
const placeholders = filter.status.map(() => `$${paramIndex++}`).join(", ");
|
|
284
|
+
conditions.push(`status IN (${placeholders})`);
|
|
285
|
+
params.push(...filter.status);
|
|
286
|
+
} else {
|
|
287
|
+
conditions.push(`status = $${paramIndex++}`);
|
|
288
|
+
params.push(filter.status);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (filter.objectType) {
|
|
292
|
+
conditions.push(`payload->>'objectType' = $${paramIndex++}`);
|
|
293
|
+
params.push(filter.objectType);
|
|
294
|
+
}
|
|
295
|
+
if (filter.method) {
|
|
296
|
+
conditions.push(`payload->>'method' = $${paramIndex++}`);
|
|
297
|
+
params.push(filter.method);
|
|
298
|
+
}
|
|
299
|
+
if (filter.createdAfter) {
|
|
300
|
+
conditions.push(`created_at > $${paramIndex++}`);
|
|
301
|
+
params.push(filter.createdAfter.toISOString());
|
|
302
|
+
}
|
|
303
|
+
if (filter.createdBefore) {
|
|
304
|
+
conditions.push(`created_at < $${paramIndex++}`);
|
|
305
|
+
params.push(filter.createdBefore.toISOString());
|
|
306
|
+
}
|
|
307
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
308
|
+
const orderBy = this.buildOrderBy(filter);
|
|
309
|
+
let limitOffset = "";
|
|
310
|
+
if (filter.limit) {
|
|
311
|
+
limitOffset = `LIMIT $${paramIndex++}`;
|
|
312
|
+
params.push(filter.limit);
|
|
313
|
+
if (filter.offset) {
|
|
314
|
+
limitOffset += ` OFFSET $${paramIndex++}`;
|
|
315
|
+
params.push(filter.offset);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const { rows } = await this.db.query(
|
|
319
|
+
`SELECT * FROM ${this.tableName} ${where} ${orderBy} ${limitOffset}`,
|
|
320
|
+
params
|
|
321
|
+
);
|
|
322
|
+
return rows.map((row) => this.parseJobRow(row));
|
|
323
|
+
}
|
|
324
|
+
async cancel(id) {
|
|
325
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
326
|
+
const job = await this.get(id);
|
|
327
|
+
if (!job) throw new Error(`Job not found: ${id}`);
|
|
328
|
+
if (job.status === "completed" || job.status === "cancelled") {
|
|
329
|
+
throw new Error(`Cannot cancel job with status: ${job.status}`);
|
|
330
|
+
}
|
|
331
|
+
await this.update(id, {
|
|
332
|
+
status: "cancelled",
|
|
333
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async cleanup(options) {
|
|
337
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
338
|
+
const conditions = [];
|
|
339
|
+
const params = [];
|
|
340
|
+
let paramIndex = 1;
|
|
341
|
+
if (options.completedBefore) {
|
|
342
|
+
conditions.push(
|
|
343
|
+
`(status = 'completed' AND completed_at < $${paramIndex++})`
|
|
344
|
+
);
|
|
345
|
+
params.push(options.completedBefore.toISOString());
|
|
346
|
+
}
|
|
347
|
+
if (options.failedBefore) {
|
|
348
|
+
conditions.push(
|
|
349
|
+
`(status = 'failed' AND completed_at < $${paramIndex++})`
|
|
350
|
+
);
|
|
351
|
+
params.push(options.failedBefore.toISOString());
|
|
352
|
+
}
|
|
353
|
+
if (options.cancelledBefore) {
|
|
354
|
+
conditions.push(
|
|
355
|
+
`(status = 'cancelled' AND completed_at < $${paramIndex++})`
|
|
356
|
+
);
|
|
357
|
+
params.push(options.cancelledBefore.toISOString());
|
|
358
|
+
}
|
|
359
|
+
if (conditions.length === 0) return 0;
|
|
360
|
+
let query = `DELETE FROM ${this.tableName} WHERE (${conditions.join(" OR ")})`;
|
|
361
|
+
if (options.limit) {
|
|
362
|
+
query = `
|
|
363
|
+
DELETE FROM ${this.tableName}
|
|
364
|
+
WHERE id IN (
|
|
365
|
+
SELECT id FROM ${this.tableName}
|
|
366
|
+
WHERE (${conditions.join(" OR ")})
|
|
367
|
+
LIMIT $${paramIndex}
|
|
368
|
+
)
|
|
369
|
+
`;
|
|
370
|
+
params.push(options.limit);
|
|
371
|
+
}
|
|
372
|
+
const result = await this.db.query(query, params);
|
|
373
|
+
return result.rowCount ?? 0;
|
|
374
|
+
}
|
|
375
|
+
async heartbeat(jobId, workerId) {
|
|
376
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
377
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
378
|
+
await this.db.query(
|
|
379
|
+
`
|
|
380
|
+
UPDATE ${this.tableName}
|
|
381
|
+
SET worker_heartbeat = $1, updated_at = $1
|
|
382
|
+
WHERE id = $2 AND worker_id = $3 AND status = 'running'
|
|
383
|
+
`,
|
|
384
|
+
[now, jobId, workerId]
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
async stats(queue) {
|
|
388
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
389
|
+
const params = [];
|
|
390
|
+
let paramIndex = 1;
|
|
391
|
+
const queueFilter = queue ? `WHERE queue = $${paramIndex++}` : "";
|
|
392
|
+
if (queue) params.push(queue);
|
|
393
|
+
const { rows: countRows } = await this.db.query(
|
|
394
|
+
`
|
|
395
|
+
SELECT status, COUNT(*)::int as count
|
|
396
|
+
FROM ${this.tableName}
|
|
397
|
+
${queueFilter}
|
|
398
|
+
GROUP BY status
|
|
399
|
+
`,
|
|
400
|
+
params
|
|
401
|
+
);
|
|
402
|
+
const counts = {};
|
|
403
|
+
for (const row of countRows) {
|
|
404
|
+
counts[row.status] = row.count;
|
|
405
|
+
}
|
|
406
|
+
const { rows: durationRows } = await this.db.query(
|
|
407
|
+
`
|
|
408
|
+
SELECT AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000)::float as avg_duration
|
|
409
|
+
FROM ${this.tableName}
|
|
410
|
+
WHERE status = 'completed'
|
|
411
|
+
AND started_at IS NOT NULL
|
|
412
|
+
AND completed_at IS NOT NULL
|
|
413
|
+
${queue ? `AND queue = $1` : ""}
|
|
414
|
+
`,
|
|
415
|
+
queue ? [queue] : []
|
|
416
|
+
);
|
|
417
|
+
const avgDuration = durationRows[0]?.avg_duration ?? null;
|
|
418
|
+
return {
|
|
419
|
+
pending: counts["pending"] ?? 0,
|
|
420
|
+
running: counts["running"] ?? 0,
|
|
421
|
+
completed: counts["completed"] ?? 0,
|
|
422
|
+
failed: counts["failed"] ?? 0,
|
|
423
|
+
cancelled: counts["cancelled"] ?? 0,
|
|
424
|
+
avgDuration: avgDuration ? Math.round(avgDuration) : null
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async close() {
|
|
428
|
+
await this.stopListening();
|
|
429
|
+
this.db = null;
|
|
430
|
+
this.initialized = false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export {
|
|
434
|
+
PostgresJobStore,
|
|
435
|
+
PostgresJobStore as default
|
|
436
|
+
};
|
|
437
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sources":["../../src/adapters/postgres.ts"],"sourcesContent":["import { type DatabaseInterface, getDatabase } from '@happyvertical/sql';\nimport { BaseJobStore, validateTableName } from '../base-store.js';\nimport type {\n CleanupOptions,\n Job,\n JobCreateOptions,\n JobEventListener,\n JobFilter,\n QueueStats,\n} from '../types.js';\n\n/**\n * PostgreSQL job store configuration\n */\nexport interface PostgresJobStoreConfig {\n /** Database connection URL */\n url?: string;\n /** Existing database instance to use */\n db?: DatabaseInterface;\n /** Table name for jobs (default: '_jobs') */\n tableName?: string;\n /** Enable NOTIFY/LISTEN for push-based job retrieval */\n enableNotify?: boolean;\n /** Channel name for notifications (default: 'job_events') */\n notifyChannel?: string;\n}\n\n/**\n * PostgreSQL-based job store with NOTIFY/LISTEN support\n *\n * Uses PostgreSQL for job persistence with optional push-based\n * notifications via NOTIFY/LISTEN for efficient job retrieval.\n */\nexport class PostgresJobStore extends BaseJobStore {\n private db: DatabaseInterface | null = null;\n private readonly url: string;\n private readonly externalDb: DatabaseInterface | null;\n private readonly tableName: string;\n private readonly enableNotify: boolean;\n private readonly notifyChannel: string;\n private notifyListeners: Set<JobEventListener> = new Set();\n private listening = false;\n\n constructor(config: PostgresJobStoreConfig = {}) {\n super();\n this.url = config.url ?? process.env.DATABASE_URL ?? '';\n this.externalDb = config.db ?? null;\n this.tableName = validateTableName(config.tableName ?? '_jobs');\n this.enableNotify = config.enableNotify ?? true;\n this.notifyChannel = validateTableName(\n config.notifyChannel ?? 'job_events',\n );\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // Use existing database or create new one\n this.db =\n this.externalDb ??\n (await getDatabase({ type: 'postgres', url: this.url }));\n\n // Create jobs table using raw query\n await this.db.query(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n id TEXT PRIMARY KEY,\n queue TEXT NOT NULL DEFAULT 'default',\n payload JSONB NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n priority INTEGER NOT NULL DEFAULT 50,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n run_at TIMESTAMPTZ NOT NULL,\n started_at TIMESTAMPTZ,\n completed_at TIMESTAMPTZ,\n timeout INTEGER NOT NULL DEFAULT 300000,\n timeout_behavior TEXT NOT NULL DEFAULT 'fail',\n last_error TEXT,\n result_pointer TEXT,\n retry_strategy JSONB NOT NULL,\n worker_id TEXT,\n worker_heartbeat TIMESTAMPTZ,\n created_at TIMESTAMPTZ NOT NULL,\n updated_at TIMESTAMPTZ NOT NULL\n )\n `);\n\n // Create indexes\n await this.db.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_dequeue\n ON ${this.tableName} (status, queue, run_at, priority DESC)\n WHERE status = 'pending'\n `);\n\n await this.db.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at\n ON ${this.tableName} (created_at)\n `);\n\n await this.db.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_queue\n ON ${this.tableName} (queue)\n `);\n\n // Create notify functions and triggers if enabled\n if (this.enableNotify) {\n await this.setupNotifyTriggers();\n }\n\n this.initialized = true;\n }\n\n private async setupNotifyTriggers(): Promise<void> {\n if (!this.db) return;\n\n // Create notify function for job creation\n await this.db.query(`\n CREATE OR REPLACE FUNCTION ${this.tableName}_notify_created()\n RETURNS TRIGGER AS $$\n BEGIN\n PERFORM pg_notify('${this.notifyChannel}', json_build_object(\n 'event', 'created',\n 'id', NEW.id,\n 'queue', NEW.queue,\n 'priority', NEW.priority,\n 'run_at', NEW.run_at\n )::text);\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql\n `);\n\n // Create trigger for inserts\n await this.db.query(\n `DROP TRIGGER IF EXISTS ${this.tableName}_created_trigger ON ${this.tableName}`,\n );\n await this.db.query(`\n CREATE TRIGGER ${this.tableName}_created_trigger\n AFTER INSERT ON ${this.tableName}\n FOR EACH ROW EXECUTE FUNCTION ${this.tableName}_notify_created()\n `);\n\n // Create notify function for job ready (when run_at passes)\n await this.db.query(`\n CREATE OR REPLACE FUNCTION ${this.tableName}_notify_ready()\n RETURNS TRIGGER AS $$\n BEGIN\n IF NEW.status = 'pending' AND NEW.run_at <= NOW() AND\n (OLD.run_at > NOW() OR OLD IS NULL) THEN\n PERFORM pg_notify('${this.notifyChannel}', json_build_object(\n 'event', 'ready',\n 'id', NEW.id,\n 'queue', NEW.queue,\n 'priority', NEW.priority\n )::text);\n END IF;\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql\n `);\n\n // Create trigger for updates\n await this.db.query(\n `DROP TRIGGER IF EXISTS ${this.tableName}_ready_trigger ON ${this.tableName}`,\n );\n await this.db.query(`\n CREATE TRIGGER ${this.tableName}_ready_trigger\n AFTER UPDATE ON ${this.tableName}\n FOR EACH ROW EXECUTE FUNCTION ${this.tableName}_notify_ready()\n `);\n }\n\n /**\n * Start listening for PostgreSQL notifications\n * This enables push-based job retrieval\n */\n async startListening(): Promise<void> {\n if (!this.db || !this.enableNotify || this.listening) return;\n\n // Note: This requires the underlying pg client to support LISTEN\n // The @happyvertical/sql package may need to expose this functionality\n try {\n await this.db.query(`LISTEN ${this.notifyChannel}`);\n this.listening = true;\n } catch (error) {\n console.warn('Could not start LISTEN, falling back to polling:', error);\n }\n }\n\n async stopListening(): Promise<void> {\n if (!this.db || !this.listening) return;\n\n try {\n await this.db.query(`UNLISTEN ${this.notifyChannel}`);\n this.listening = false;\n } catch (error) {\n // Ignore errors when stopping\n }\n }\n\n async enqueue(options: JobCreateOptions): Promise<Job> {\n if (!this.db) throw new Error('Store not initialized');\n\n const job = this.createJobRecord(options);\n\n await this.db.insert(this.tableName, {\n id: job.id,\n queue: job.queue,\n payload: JSON.stringify(job.payload),\n status: job.status,\n priority: job.priority,\n attempts: job.attempts,\n max_attempts: job.maxAttempts,\n run_at: job.runAt.toISOString(),\n started_at: job.startedAt?.toISOString() ?? null,\n completed_at: job.completedAt?.toISOString() ?? null,\n timeout: job.timeout,\n timeout_behavior: job.timeoutBehavior,\n last_error: job.lastError,\n result_pointer: job.resultPointer,\n retry_strategy: JSON.stringify(job.retryStrategy),\n worker_id: job.workerId,\n worker_heartbeat: job.workerHeartbeat?.toISOString() ?? null,\n created_at: job.createdAt.toISOString(),\n updated_at: job.updatedAt.toISOString(),\n });\n\n await this.emitEvent('job.created', job);\n\n if (job.runAt <= new Date()) {\n await this.emitEvent('job.ready', job);\n }\n\n return job;\n }\n\n async dequeue(\n queues: string[],\n limit: number,\n workerId: string,\n ): Promise<Job[]> {\n if (!this.db) throw new Error('Store not initialized');\n\n const now = new Date().toISOString();\n\n // Use FOR UPDATE SKIP LOCKED for concurrent-safe dequeue\n // Build PostgreSQL parameterized query\n const queuePlaceholders = queues.map((_, i) => `$${i + 1}`).join(', ');\n\n const { rows } = await this.db.query(\n `\n UPDATE ${this.tableName}\n SET status = 'running',\n worker_id = $${queues.length + 1},\n worker_heartbeat = $${queues.length + 2},\n started_at = $${queues.length + 2},\n attempts = attempts + 1,\n updated_at = $${queues.length + 2}\n WHERE id IN (\n SELECT id FROM ${this.tableName}\n WHERE status = 'pending'\n AND queue IN (${queuePlaceholders})\n AND run_at <= $${queues.length + 2}\n ORDER BY priority DESC, run_at ASC\n LIMIT $${queues.length + 3}\n FOR UPDATE SKIP LOCKED\n )\n RETURNING *\n `,\n [...queues, workerId, now, limit],\n );\n\n const jobs = rows.map((row) =>\n this.parseJobRow(row as Record<string, unknown>),\n );\n\n for (const job of jobs) {\n await this.emitEvent('job.started', job);\n }\n\n return jobs;\n }\n\n async update(id: string, updates: Partial<Job>): Promise<Job> {\n if (!this.db) throw new Error('Store not initialized');\n\n const setClause: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n const fieldMap: Record<string, string> = {\n queue: 'queue',\n payload: 'payload',\n status: 'status',\n priority: 'priority',\n attempts: 'attempts',\n maxAttempts: 'max_attempts',\n runAt: 'run_at',\n startedAt: 'started_at',\n completedAt: 'completed_at',\n timeout: 'timeout',\n timeoutBehavior: 'timeout_behavior',\n lastError: 'last_error',\n resultPointer: 'result_pointer',\n retryStrategy: 'retry_strategy',\n workerId: 'worker_id',\n workerHeartbeat: 'worker_heartbeat',\n };\n\n for (const [key, value] of Object.entries(updates)) {\n const column = fieldMap[key];\n if (!column) continue;\n\n setClause.push(`${column} = $${paramIndex}`);\n\n if (value instanceof Date) {\n params.push(value.toISOString());\n } else if (typeof value === 'object' && value !== null) {\n params.push(JSON.stringify(value));\n } else {\n params.push(value);\n }\n paramIndex++;\n }\n\n if (setClause.length === 0) {\n const job = await this.get(id);\n if (!job) throw new Error(`Job not found: ${id}`);\n return job;\n }\n\n setClause.push(`updated_at = $${paramIndex}`);\n params.push(new Date().toISOString());\n paramIndex++;\n\n params.push(id);\n\n await this.db.query(\n `UPDATE ${this.tableName} SET ${setClause.join(', ')} WHERE id = $${paramIndex}`,\n params,\n );\n\n const job = await this.get(id);\n if (!job) throw new Error(`Job not found after update: ${id}`);\n\n if (updates.status === 'completed') {\n await this.emitEvent('job.completed', job, {\n resultPointer: job.resultPointer ?? undefined,\n });\n } else if (updates.status === 'failed') {\n await this.emitEvent('job.failed', job, {\n error: job.lastError ?? undefined,\n });\n } else if (updates.status === 'cancelled') {\n await this.emitEvent('job.cancelled', job);\n }\n\n return job;\n }\n\n async get(id: string): Promise<Job | null> {\n if (!this.db) throw new Error('Store not initialized');\n\n const row = await this.db.get(this.tableName, { id });\n if (!row) return null;\n\n return this.parseJobRow(row as Record<string, unknown>);\n }\n\n async list(filter: JobFilter): Promise<Job[]> {\n if (!this.db) throw new Error('Store not initialized');\n\n // Build query with PostgreSQL parameter syntax\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (filter.queue) {\n conditions.push(`queue = $${paramIndex++}`);\n params.push(filter.queue);\n }\n\n if (filter.status) {\n if (Array.isArray(filter.status)) {\n const placeholders = filter.status\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`status IN (${placeholders})`);\n params.push(...filter.status);\n } else {\n conditions.push(`status = $${paramIndex++}`);\n params.push(filter.status);\n }\n }\n\n if (filter.objectType) {\n conditions.push(`payload->>'objectType' = $${paramIndex++}`);\n params.push(filter.objectType);\n }\n\n if (filter.method) {\n conditions.push(`payload->>'method' = $${paramIndex++}`);\n params.push(filter.method);\n }\n\n if (filter.createdAfter) {\n conditions.push(`created_at > $${paramIndex++}`);\n params.push(filter.createdAfter.toISOString());\n }\n\n if (filter.createdBefore) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(filter.createdBefore.toISOString());\n }\n\n const where =\n conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const orderBy = this.buildOrderBy(filter);\n\n let limitOffset = '';\n if (filter.limit) {\n limitOffset = `LIMIT $${paramIndex++}`;\n params.push(filter.limit);\n\n if (filter.offset) {\n limitOffset += ` OFFSET $${paramIndex++}`;\n params.push(filter.offset);\n }\n }\n\n const { rows } = await this.db.query(\n `SELECT * FROM ${this.tableName} ${where} ${orderBy} ${limitOffset}`,\n params,\n );\n\n return rows.map((row) => this.parseJobRow(row as Record<string, unknown>));\n }\n\n async cancel(id: string): Promise<void> {\n if (!this.db) throw new Error('Store not initialized');\n\n const job = await this.get(id);\n if (!job) throw new Error(`Job not found: ${id}`);\n\n if (job.status === 'completed' || job.status === 'cancelled') {\n throw new Error(`Cannot cancel job with status: ${job.status}`);\n }\n\n await this.update(id, {\n status: 'cancelled',\n completedAt: new Date(),\n });\n }\n\n async cleanup(options: CleanupOptions): Promise<number> {\n if (!this.db) throw new Error('Store not initialized');\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.completedBefore) {\n conditions.push(\n `(status = 'completed' AND completed_at < $${paramIndex++})`,\n );\n params.push(options.completedBefore.toISOString());\n }\n\n if (options.failedBefore) {\n conditions.push(\n `(status = 'failed' AND completed_at < $${paramIndex++})`,\n );\n params.push(options.failedBefore.toISOString());\n }\n\n if (options.cancelledBefore) {\n conditions.push(\n `(status = 'cancelled' AND completed_at < $${paramIndex++})`,\n );\n params.push(options.cancelledBefore.toISOString());\n }\n\n if (conditions.length === 0) return 0;\n\n let query = `DELETE FROM ${this.tableName} WHERE (${conditions.join(' OR ')})`;\n\n if (options.limit) {\n query = `\n DELETE FROM ${this.tableName}\n WHERE id IN (\n SELECT id FROM ${this.tableName}\n WHERE (${conditions.join(' OR ')})\n LIMIT $${paramIndex}\n )\n `;\n params.push(options.limit);\n }\n\n const result = await this.db.query(query, params);\n\n return result.rowCount ?? 0;\n }\n\n async heartbeat(jobId: string, workerId: string): Promise<void> {\n if (!this.db) throw new Error('Store not initialized');\n\n const now = new Date().toISOString();\n await this.db.query(\n `\n UPDATE ${this.tableName}\n SET worker_heartbeat = $1, updated_at = $1\n WHERE id = $2 AND worker_id = $3 AND status = 'running'\n `,\n [now, jobId, workerId],\n );\n }\n\n async stats(queue?: string): Promise<QueueStats> {\n if (!this.db) throw new Error('Store not initialized');\n\n const params: unknown[] = [];\n let paramIndex = 1;\n const queueFilter = queue ? `WHERE queue = $${paramIndex++}` : '';\n if (queue) params.push(queue);\n\n const { rows: countRows } = await this.db.query(\n `\n SELECT status, COUNT(*)::int as count\n FROM ${this.tableName}\n ${queueFilter}\n GROUP BY status\n `,\n params,\n );\n\n const counts: Record<string, number> = {};\n for (const row of countRows) {\n counts[row.status as string] = row.count as number;\n }\n\n const { rows: durationRows } = await this.db.query(\n `\n SELECT AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000)::float as avg_duration\n FROM ${this.tableName}\n WHERE status = 'completed'\n AND started_at IS NOT NULL\n AND completed_at IS NOT NULL\n ${queue ? `AND queue = $1` : ''}\n `,\n queue ? [queue] : [],\n );\n\n const avgDuration =\n (durationRows[0] as { avg_duration: number | null })?.avg_duration ??\n null;\n\n return {\n pending: counts['pending'] ?? 0,\n running: counts['running'] ?? 0,\n completed: counts['completed'] ?? 0,\n failed: counts['failed'] ?? 0,\n cancelled: counts['cancelled'] ?? 0,\n avgDuration: avgDuration ? Math.round(avgDuration) : null,\n };\n }\n\n async close(): Promise<void> {\n await this.stopListening();\n // The SDK's DatabaseInterface doesn't have a close method\n this.db = null;\n this.initialized = false;\n }\n}\n\nexport default PostgresJobStore;\n"],"names":["job"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DatabaseInterface, SqliteCapabilitiesOptions } from '@happyvertical/sql';
|
|
2
|
+
import { BaseJobStore } from '../base-store.js';
|
|
3
|
+
import { CleanupOptions, Job, JobCreateOptions, JobFilter, QueueStats } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* SQLite job store configuration
|
|
6
|
+
*/
|
|
7
|
+
export interface SqliteJobStoreConfig {
|
|
8
|
+
/** Database URL or path (default: ':memory:') */
|
|
9
|
+
url?: string;
|
|
10
|
+
/** Existing database instance to use */
|
|
11
|
+
db?: DatabaseInterface;
|
|
12
|
+
/** Table name for jobs (default: '_jobs') */
|
|
13
|
+
tableName?: string;
|
|
14
|
+
/** Optional SQLite native capabilities for development/test modes */
|
|
15
|
+
capabilities?: SqliteCapabilitiesOptions;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* SQLite-based job store
|
|
19
|
+
*
|
|
20
|
+
* Uses SQLite for job persistence. Supports polling-based job retrieval.
|
|
21
|
+
* Good for single-instance deployments or development.
|
|
22
|
+
*/
|
|
23
|
+
export declare class SqliteJobStore extends BaseJobStore {
|
|
24
|
+
private db;
|
|
25
|
+
private readonly url;
|
|
26
|
+
private readonly externalDb;
|
|
27
|
+
private readonly tableName;
|
|
28
|
+
private readonly capabilities;
|
|
29
|
+
constructor(config?: SqliteJobStoreConfig);
|
|
30
|
+
initialize(): Promise<void>;
|
|
31
|
+
enqueue(options: JobCreateOptions): Promise<Job>;
|
|
32
|
+
dequeue(queues: string[], limit: number, workerId: string): Promise<Job[]>;
|
|
33
|
+
update(id: string, updates: Partial<Job>): Promise<Job>;
|
|
34
|
+
get(id: string): Promise<Job | null>;
|
|
35
|
+
list(filter: JobFilter): Promise<Job[]>;
|
|
36
|
+
cancel(id: string): Promise<void>;
|
|
37
|
+
cleanup(options: CleanupOptions): Promise<number>;
|
|
38
|
+
heartbeat(jobId: string, workerId: string): Promise<void>;
|
|
39
|
+
stats(queue?: string): Promise<QueueStats>;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
waitForUpdate(timeoutMs?: number): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
export default SqliteJobStore;
|
|
44
|
+
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,yBAAyB,EAE/B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAqB,MAAM,kBAAkB,CAAC;AACnE,OAAO,KAAK,EACV,cAAc,EACd,GAAG,EACH,gBAAgB,EAChB,SAAS,EACT,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,CAAC,EAAE,iBAAiB,CAAC;IACvB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,YAAY,CAAC,EAAE,yBAAyB,CAAC;CAC1C;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,EAAE,CAAkC;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;gBAEzD,MAAM,GAAE,oBAAyB;IAQvC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA8C3B,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC;IAqChD,OAAO,CACX,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,EAAE,CAAC;IA2DX,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IA6EvD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IASpC,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAgBvC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CjD,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAazD,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAmD1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQtB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAU1D;AAED,eAAe,cAAc,CAAC"}
|