@objectstack/plugin-webhooks 5.1.0 → 5.2.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/.turbo/turbo-build.log +35 -13
- package/CHANGELOG.md +9 -37
- package/dist/chunk-JN76ZRWN.js +164 -0
- package/dist/chunk-JN76ZRWN.js.map +1 -0
- package/dist/chunk-M4M5FWIH.cjs +15 -0
- package/dist/chunk-M4M5FWIH.cjs.map +1 -0
- package/dist/chunk-NYSUNT6X.js +15 -0
- package/dist/chunk-NYSUNT6X.js.map +1 -0
- package/dist/chunk-OW7ESXOK.cjs +164 -0
- package/dist/chunk-OW7ESXOK.cjs.map +1 -0
- package/dist/index.cjs +747 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +455 -0
- package/dist/index.d.ts +425 -74
- package/dist/index.js +712 -218
- package/dist/index.js.map +1 -1
- package/dist/outbox-bPQmKYPN.d.cts +128 -0
- package/dist/outbox-bPQmKYPN.d.ts +128 -0
- package/dist/schema.cjs +9 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +4772 -0
- package/dist/schema.d.ts +4772 -0
- package/dist/schema.js +9 -0
- package/dist/schema.js.map +1 -0
- package/dist/sql-outbox.cjs +184 -0
- package/dist/sql-outbox.cjs.map +1 -0
- package/dist/sql-outbox.d.cts +54 -0
- package/dist/sql-outbox.d.ts +54 -0
- package/dist/sql-outbox.js +184 -0
- package/dist/sql-outbox.js.map +1 -0
- package/package.json +30 -10
- package/src/auto-enqueuer.test.ts +391 -0
- package/src/auto-enqueuer.ts +335 -0
- package/src/dispatcher.test.ts +324 -0
- package/src/dispatcher.ts +218 -0
- package/src/http-sender.ts +187 -0
- package/src/index.ts +48 -12
- package/src/memory-outbox.ts +127 -0
- package/src/outbox.ts +141 -0
- package/src/partition.ts +19 -0
- package/src/retention.test.ts +116 -0
- package/src/retention.ts +144 -0
- package/src/schema.ts +22 -0
- package/src/sql-outbox.test.ts +410 -0
- package/src/sql-outbox.ts +282 -0
- package/src/sys-webhook-delivery.object.ts +202 -0
- package/src/webhook-outbox-plugin.ts +280 -0
- package/tsconfig.json +5 -13
- package/tsup.config.ts +14 -0
- package/dist/index.d.mts +0 -104
- package/dist/index.mjs +0 -216
- package/dist/index.mjs.map +0 -1
- package/src/webhooks-plugin.test.ts +0 -218
- package/src/webhooks-plugin.ts +0 -294
package/dist/schema.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkM4M5FWIHcjs = require('./chunk-M4M5FWIH.cjs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunkOW7ESXOKcjs = require('./chunk-OW7ESXOK.cjs');
|
|
7
|
+
|
|
8
|
+
// src/sql-outbox.ts
|
|
9
|
+
var _crypto = require('crypto');
|
|
10
|
+
var SqlWebhookOutbox = class {
|
|
11
|
+
constructor(engine, opts) {
|
|
12
|
+
this.engine = engine;
|
|
13
|
+
if (opts.partitionCount <= 0) {
|
|
14
|
+
throw new Error("SqlWebhookOutbox: partitionCount must be > 0");
|
|
15
|
+
}
|
|
16
|
+
this.objectName = _nullishCoalesce(opts.objectName, () => ( _chunkOW7ESXOKcjs.SYS_WEBHOOK_DELIVERY));
|
|
17
|
+
this.partitionCount = opts.partitionCount;
|
|
18
|
+
}
|
|
19
|
+
async enqueue(input) {
|
|
20
|
+
const existing = await this.engine.findOne(this.objectName, {
|
|
21
|
+
where: { event_id: input.eventId, webhook_id: input.webhookId },
|
|
22
|
+
fields: ["id"]
|
|
23
|
+
});
|
|
24
|
+
if (_optionalChain([existing, 'optionalAccess', _ => _.id])) return existing.id;
|
|
25
|
+
const id = _crypto.randomUUID.call(void 0, );
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const row = {
|
|
28
|
+
id,
|
|
29
|
+
webhook_id: input.webhookId,
|
|
30
|
+
event_id: input.eventId,
|
|
31
|
+
event_type: input.eventType,
|
|
32
|
+
url: input.url,
|
|
33
|
+
method: _nullishCoalesce(input.method, () => ( "POST")),
|
|
34
|
+
headers_json: input.headers ? JSON.stringify(input.headers) : void 0,
|
|
35
|
+
secret: input.secret,
|
|
36
|
+
timeout_ms: input.timeoutMs,
|
|
37
|
+
payload_json: JSON.stringify(_nullishCoalesce(input.payload, () => ( null))),
|
|
38
|
+
partition_key: _chunkM4M5FWIHcjs.hashPartition.call(void 0, input.webhookId, this.partitionCount),
|
|
39
|
+
status: "pending",
|
|
40
|
+
attempts: 0,
|
|
41
|
+
created_at: now,
|
|
42
|
+
updated_at: now
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
await this.engine.insert(this.objectName, row);
|
|
46
|
+
return id;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const winner = await this.engine.findOne(this.objectName, {
|
|
49
|
+
where: { event_id: input.eventId, webhook_id: input.webhookId },
|
|
50
|
+
fields: ["id"]
|
|
51
|
+
});
|
|
52
|
+
if (_optionalChain([winner, 'optionalAccess', _2 => _2.id])) return winner.id;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async claim(opts) {
|
|
57
|
+
const now = _nullishCoalesce(opts.now, () => ( Date.now()));
|
|
58
|
+
await this.engine.update(
|
|
59
|
+
this.objectName,
|
|
60
|
+
{ status: "pending", claimed_by: null, claimed_at: null, updated_at: now },
|
|
61
|
+
{
|
|
62
|
+
where: {
|
|
63
|
+
status: "in_flight",
|
|
64
|
+
claimed_at: { $lt: now - opts.claimTtlMs }
|
|
65
|
+
},
|
|
66
|
+
multi: true
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
const partitionFilter = opts.partition ? { partition_key: opts.partition.index } : {};
|
|
70
|
+
const candidates = await this.engine.find(this.objectName, {
|
|
71
|
+
where: {
|
|
72
|
+
status: "pending",
|
|
73
|
+
...partitionFilter,
|
|
74
|
+
// next_retry_at <= now OR null
|
|
75
|
+
$or: [
|
|
76
|
+
{ next_retry_at: null },
|
|
77
|
+
{ next_retry_at: { $lte: now } }
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
fields: ["id"],
|
|
81
|
+
// No orderBy for portability — drivers handle the natural insert order.
|
|
82
|
+
limit: opts.limit
|
|
83
|
+
});
|
|
84
|
+
if (candidates.length === 0) return [];
|
|
85
|
+
const ids = candidates.map((c) => c.id);
|
|
86
|
+
await this.engine.update(
|
|
87
|
+
this.objectName,
|
|
88
|
+
{
|
|
89
|
+
status: "in_flight",
|
|
90
|
+
claimed_by: opts.nodeId,
|
|
91
|
+
claimed_at: now,
|
|
92
|
+
updated_at: now
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
where: { id: { $in: ids }, status: "pending" },
|
|
96
|
+
multi: true
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
const claimed = await this.engine.find(this.objectName, {
|
|
100
|
+
where: {
|
|
101
|
+
id: { $in: ids },
|
|
102
|
+
claimed_by: opts.nodeId,
|
|
103
|
+
claimed_at: now,
|
|
104
|
+
status: "in_flight"
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return claimed.map((r) => this.toDelivery(r));
|
|
108
|
+
}
|
|
109
|
+
async ack(id, result) {
|
|
110
|
+
const current = await this.engine.findOne(this.objectName, {
|
|
111
|
+
where: { id },
|
|
112
|
+
fields: ["attempts"]
|
|
113
|
+
});
|
|
114
|
+
if (!current) return;
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
let status;
|
|
117
|
+
let nextRetryAt;
|
|
118
|
+
let error;
|
|
119
|
+
if (result.success) {
|
|
120
|
+
status = "success";
|
|
121
|
+
nextRetryAt = null;
|
|
122
|
+
error = null;
|
|
123
|
+
} else if (result.dead) {
|
|
124
|
+
status = "dead";
|
|
125
|
+
nextRetryAt = null;
|
|
126
|
+
error = _nullishCoalesce(result.error, () => ( null));
|
|
127
|
+
} else {
|
|
128
|
+
status = "pending";
|
|
129
|
+
nextRetryAt = _nullishCoalesce(result.nextRetryAt, () => ( null));
|
|
130
|
+
error = _nullishCoalesce(result.error, () => ( null));
|
|
131
|
+
}
|
|
132
|
+
await this.engine.update(
|
|
133
|
+
this.objectName,
|
|
134
|
+
{
|
|
135
|
+
status,
|
|
136
|
+
attempts: (_nullishCoalesce(current.attempts, () => ( 0))) + 1,
|
|
137
|
+
last_attempted_at: now,
|
|
138
|
+
claimed_by: null,
|
|
139
|
+
claimed_at: null,
|
|
140
|
+
response_code: _nullishCoalesce(result.httpStatus, () => ( null)),
|
|
141
|
+
response_body: _nullishCoalesce(result.responseBody, () => ( null)),
|
|
142
|
+
next_retry_at: nextRetryAt,
|
|
143
|
+
error,
|
|
144
|
+
updated_at: now
|
|
145
|
+
},
|
|
146
|
+
{ where: { id }, multi: false }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
async list(filter) {
|
|
150
|
+
const rows = await this.engine.find(this.objectName, {
|
|
151
|
+
where: _optionalChain([filter, 'optionalAccess', _3 => _3.status]) ? { status: filter.status } : {}
|
|
152
|
+
});
|
|
153
|
+
return rows.map((r) => this.toDelivery(r));
|
|
154
|
+
}
|
|
155
|
+
toDelivery(r) {
|
|
156
|
+
return {
|
|
157
|
+
id: r.id,
|
|
158
|
+
webhookId: r.webhook_id,
|
|
159
|
+
eventId: r.event_id,
|
|
160
|
+
eventType: r.event_type,
|
|
161
|
+
url: r.url,
|
|
162
|
+
method: _nullishCoalesce(r.method, () => ( void 0)),
|
|
163
|
+
headers: r.headers_json ? JSON.parse(r.headers_json) : void 0,
|
|
164
|
+
secret: _nullishCoalesce(r.secret, () => ( void 0)),
|
|
165
|
+
timeoutMs: _nullishCoalesce(r.timeout_ms, () => ( void 0)),
|
|
166
|
+
payload: JSON.parse(r.payload_json),
|
|
167
|
+
status: r.status,
|
|
168
|
+
attempts: r.attempts,
|
|
169
|
+
claimedBy: _nullishCoalesce(r.claimed_by, () => ( void 0)),
|
|
170
|
+
claimedAt: _nullishCoalesce(r.claimed_at, () => ( void 0)),
|
|
171
|
+
nextRetryAt: _nullishCoalesce(r.next_retry_at, () => ( void 0)),
|
|
172
|
+
lastAttemptedAt: _nullishCoalesce(r.last_attempted_at, () => ( void 0)),
|
|
173
|
+
responseCode: _nullishCoalesce(r.response_code, () => ( void 0)),
|
|
174
|
+
responseBody: _nullishCoalesce(r.response_body, () => ( void 0)),
|
|
175
|
+
error: _nullishCoalesce(r.error, () => ( void 0)),
|
|
176
|
+
createdAt: r.created_at,
|
|
177
|
+
updatedAt: r.updated_at
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
exports.SqlWebhookOutbox = SqlWebhookOutbox;
|
|
184
|
+
//# sourceMappingURL=sql-outbox.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/sql-outbox.cjs","../src/sql-outbox.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/sql-outbox.cjs","sourcesContent":[null,"// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { randomUUID } from 'node:crypto';\nimport type { IDataEngine } from '@objectstack/spec/contracts';\nimport type {\n AckResult,\n ClaimOptions,\n DeliveryStatus,\n EnqueueInput,\n IWebhookOutbox,\n WebhookDelivery,\n} from './outbox.js';\nimport { hashPartition } from './partition.js';\nimport { SYS_WEBHOOK_DELIVERY } from './schema.js';\n\nexport interface SqlWebhookOutboxOptions {\n /**\n * Total partition count — MUST match the dispatcher's `partitionCount`.\n * Used at enqueue time to precompute `partition_key`.\n */\n partitionCount: number;\n /**\n * Object name to read/write. Defaults to `sys_webhook_delivery`. Override\n * only if you've registered the schema under a different name.\n */\n objectName?: string;\n}\n\ninterface DeliveryRow {\n id: string;\n webhook_id: string;\n event_id: string;\n event_type: string;\n url: string;\n method?: string | null;\n headers_json?: string | null;\n secret?: string | null;\n timeout_ms?: number | null;\n payload_json: string;\n partition_key: number;\n status: DeliveryStatus;\n attempts: number;\n claimed_by?: string | null;\n claimed_at?: number | null;\n next_retry_at?: number | null;\n last_attempted_at?: number | null;\n response_code?: number | null;\n response_body?: string | null;\n error?: string | null;\n created_at: number;\n updated_at: number;\n}\n\n/**\n * Durable `IWebhookOutbox` backed by ObjectQL — the production storage\n * impl. Works against any registered driver (SQL, Turso, Mongo, in-memory)\n * because everything goes through the driver-agnostic `IDataEngine` API.\n *\n * **Why no `FOR UPDATE SKIP LOCKED`?** ObjectQL is driver-agnostic — that\n * SQL feature is Postgres-only. We get equivalent safety from two layers:\n *\n * 1. `cluster.lock` held per partition by the dispatcher (the primary\n * mutex). One node owns one partition at a time → no two claimers.\n * 2. Atomic `UPDATE WHERE status='pending'` (the backup). Even if two\n * claimers slip through (e.g. admin reschedule + dispatcher), only\n * the first UPDATE matches each row.\n *\n * **Why precompute `partition_key` on enqueue?** ObjectQL has no\n * cross-driver `hash()` function in WHERE clauses. Storing the partition\n * as a column makes the claim query a plain indexed lookup.\n *\n * **Dedup race**: SELECT-then-INSERT has a tiny window where two\n * concurrent producers both miss the SELECT and both INSERT. The unique\n * index `(event_id, webhook_id)` on the table catches it — the second\n * INSERT errors, the producer ignores it. Receivers MUST be idempotent\n * on the `X-Objectstack-Delivery` header anyway.\n */\nexport class SqlWebhookOutbox implements IWebhookOutbox {\n private readonly objectName: string;\n private readonly partitionCount: number;\n\n constructor(\n private readonly engine: IDataEngine,\n opts: SqlWebhookOutboxOptions,\n ) {\n if (opts.partitionCount <= 0) {\n throw new Error('SqlWebhookOutbox: partitionCount must be > 0');\n }\n this.objectName = opts.objectName ?? SYS_WEBHOOK_DELIVERY;\n this.partitionCount = opts.partitionCount;\n }\n\n async enqueue(input: EnqueueInput): Promise<string> {\n // Cheap pre-check to absorb most duplicates without hitting the\n // unique-index error path. Race window with the INSERT below is\n // intentional and documented.\n const existing = await this.engine.findOne(this.objectName, {\n where: { event_id: input.eventId, webhook_id: input.webhookId },\n fields: ['id'],\n });\n if (existing?.id) return existing.id as string;\n\n const id = randomUUID();\n const now = Date.now();\n const row: Omit<DeliveryRow, 'response_body' | 'error'> = {\n id,\n webhook_id: input.webhookId,\n event_id: input.eventId,\n event_type: input.eventType,\n url: input.url,\n method: input.method ?? 'POST',\n headers_json: input.headers ? JSON.stringify(input.headers) : undefined,\n secret: input.secret,\n timeout_ms: input.timeoutMs,\n payload_json: JSON.stringify(input.payload ?? null),\n partition_key: hashPartition(input.webhookId, this.partitionCount),\n status: 'pending',\n attempts: 0,\n created_at: now,\n updated_at: now,\n };\n try {\n await this.engine.insert(this.objectName, row);\n return id;\n } catch (err) {\n // Unique-index collision (dedup race) → look up the winner and\n // return its id. Any other error propagates.\n const winner = await this.engine.findOne(this.objectName, {\n where: { event_id: input.eventId, webhook_id: input.webhookId },\n fields: ['id'],\n });\n if (winner?.id) return winner.id as string;\n throw err;\n }\n }\n\n async claim(opts: ClaimOptions): Promise<WebhookDelivery[]> {\n const now = opts.now ?? Date.now();\n\n // 1. Reap stale in_flight rows — visibility-timeout recovery.\n await this.engine.update(\n this.objectName,\n { status: 'pending', claimed_by: null, claimed_at: null, updated_at: now },\n {\n where: {\n status: 'in_flight',\n claimed_at: { $lt: now - opts.claimTtlMs },\n },\n multi: true,\n },\n );\n\n // 2. Pick candidate ids.\n const partitionFilter = opts.partition\n ? { partition_key: opts.partition.index }\n : {};\n const candidates = await this.engine.find(this.objectName, {\n where: {\n status: 'pending',\n ...partitionFilter,\n // next_retry_at <= now OR null\n $or: [\n { next_retry_at: null },\n { next_retry_at: { $lte: now } },\n ],\n },\n fields: ['id'],\n // No orderBy for portability — drivers handle the natural insert order.\n limit: opts.limit,\n });\n if (candidates.length === 0) return [];\n\n const ids = (candidates as Array<{ id: string }>).map((c) => c.id);\n\n // 3. Atomic claim. WHERE status='pending' rejects any rows another\n // worker swept up between steps 2 and 3.\n await this.engine.update(\n this.objectName,\n {\n status: 'in_flight',\n claimed_by: opts.nodeId,\n claimed_at: now,\n updated_at: now,\n },\n {\n where: { id: { $in: ids }, status: 'pending' },\n multi: true,\n },\n );\n\n // 4. Read back the rows we actually own.\n const claimed = (await this.engine.find(this.objectName, {\n where: {\n id: { $in: ids },\n claimed_by: opts.nodeId,\n claimed_at: now,\n status: 'in_flight',\n },\n })) as DeliveryRow[];\n\n return claimed.map((r) => this.toDelivery(r));\n }\n\n async ack(id: string, result: AckResult): Promise<void> {\n // ObjectQL has no atomic $inc across drivers, so read-then-write.\n // Safe enough: ack is single-writer per row (only the claimer acks).\n const current = (await this.engine.findOne(this.objectName, {\n where: { id },\n fields: ['attempts'],\n })) as { attempts?: number } | null;\n if (!current) return;\n\n const now = Date.now();\n let status: DeliveryStatus;\n let nextRetryAt: number | null;\n let error: string | null;\n\n if (result.success) {\n status = 'success';\n nextRetryAt = null;\n error = null;\n } else if (result.dead) {\n status = 'dead';\n nextRetryAt = null;\n error = result.error ?? null;\n } else {\n status = 'pending';\n nextRetryAt = result.nextRetryAt ?? null;\n error = result.error ?? null;\n }\n\n await this.engine.update(\n this.objectName,\n {\n status,\n attempts: (current.attempts ?? 0) + 1,\n last_attempted_at: now,\n claimed_by: null,\n claimed_at: null,\n response_code: result.httpStatus ?? null,\n response_body: result.responseBody ?? null,\n next_retry_at: nextRetryAt,\n error,\n updated_at: now,\n },\n { where: { id }, multi: false },\n );\n }\n\n async list(filter?: { status?: DeliveryStatus }): Promise<WebhookDelivery[]> {\n const rows = (await this.engine.find(this.objectName, {\n where: filter?.status ? { status: filter.status } : {},\n })) as DeliveryRow[];\n return rows.map((r) => this.toDelivery(r));\n }\n\n private toDelivery(r: DeliveryRow): WebhookDelivery {\n return {\n id: r.id,\n webhookId: r.webhook_id,\n eventId: r.event_id,\n eventType: r.event_type,\n url: r.url,\n method: r.method ?? undefined,\n headers: r.headers_json ? JSON.parse(r.headers_json) : undefined,\n secret: r.secret ?? undefined,\n timeoutMs: r.timeout_ms ?? undefined,\n payload: JSON.parse(r.payload_json),\n status: r.status,\n attempts: r.attempts,\n claimedBy: r.claimed_by ?? undefined,\n claimedAt: r.claimed_at ?? undefined,\n nextRetryAt: r.next_retry_at ?? undefined,\n lastAttemptedAt: r.last_attempted_at ?? undefined,\n responseCode: r.response_code ?? undefined,\n responseBody: r.response_body ?? undefined,\n error: r.error ?? undefined,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { IDataEngine } from '@objectstack/spec/contracts';
|
|
2
|
+
import { I as IWebhookOutbox, E as EnqueueInput, C as ClaimOptions, W as WebhookDelivery, a as AckResult, D as DeliveryStatus } from './outbox-bPQmKYPN.cjs';
|
|
3
|
+
|
|
4
|
+
interface SqlWebhookOutboxOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Total partition count — MUST match the dispatcher's `partitionCount`.
|
|
7
|
+
* Used at enqueue time to precompute `partition_key`.
|
|
8
|
+
*/
|
|
9
|
+
partitionCount: number;
|
|
10
|
+
/**
|
|
11
|
+
* Object name to read/write. Defaults to `sys_webhook_delivery`. Override
|
|
12
|
+
* only if you've registered the schema under a different name.
|
|
13
|
+
*/
|
|
14
|
+
objectName?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Durable `IWebhookOutbox` backed by ObjectQL — the production storage
|
|
18
|
+
* impl. Works against any registered driver (SQL, Turso, Mongo, in-memory)
|
|
19
|
+
* because everything goes through the driver-agnostic `IDataEngine` API.
|
|
20
|
+
*
|
|
21
|
+
* **Why no `FOR UPDATE SKIP LOCKED`?** ObjectQL is driver-agnostic — that
|
|
22
|
+
* SQL feature is Postgres-only. We get equivalent safety from two layers:
|
|
23
|
+
*
|
|
24
|
+
* 1. `cluster.lock` held per partition by the dispatcher (the primary
|
|
25
|
+
* mutex). One node owns one partition at a time → no two claimers.
|
|
26
|
+
* 2. Atomic `UPDATE WHERE status='pending'` (the backup). Even if two
|
|
27
|
+
* claimers slip through (e.g. admin reschedule + dispatcher), only
|
|
28
|
+
* the first UPDATE matches each row.
|
|
29
|
+
*
|
|
30
|
+
* **Why precompute `partition_key` on enqueue?** ObjectQL has no
|
|
31
|
+
* cross-driver `hash()` function in WHERE clauses. Storing the partition
|
|
32
|
+
* as a column makes the claim query a plain indexed lookup.
|
|
33
|
+
*
|
|
34
|
+
* **Dedup race**: SELECT-then-INSERT has a tiny window where two
|
|
35
|
+
* concurrent producers both miss the SELECT and both INSERT. The unique
|
|
36
|
+
* index `(event_id, webhook_id)` on the table catches it — the second
|
|
37
|
+
* INSERT errors, the producer ignores it. Receivers MUST be idempotent
|
|
38
|
+
* on the `X-Objectstack-Delivery` header anyway.
|
|
39
|
+
*/
|
|
40
|
+
declare class SqlWebhookOutbox implements IWebhookOutbox {
|
|
41
|
+
private readonly engine;
|
|
42
|
+
private readonly objectName;
|
|
43
|
+
private readonly partitionCount;
|
|
44
|
+
constructor(engine: IDataEngine, opts: SqlWebhookOutboxOptions);
|
|
45
|
+
enqueue(input: EnqueueInput): Promise<string>;
|
|
46
|
+
claim(opts: ClaimOptions): Promise<WebhookDelivery[]>;
|
|
47
|
+
ack(id: string, result: AckResult): Promise<void>;
|
|
48
|
+
list(filter?: {
|
|
49
|
+
status?: DeliveryStatus;
|
|
50
|
+
}): Promise<WebhookDelivery[]>;
|
|
51
|
+
private toDelivery;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { SqlWebhookOutbox, type SqlWebhookOutboxOptions };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { IDataEngine } from '@objectstack/spec/contracts';
|
|
2
|
+
import { I as IWebhookOutbox, E as EnqueueInput, C as ClaimOptions, W as WebhookDelivery, a as AckResult, D as DeliveryStatus } from './outbox-bPQmKYPN.js';
|
|
3
|
+
|
|
4
|
+
interface SqlWebhookOutboxOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Total partition count — MUST match the dispatcher's `partitionCount`.
|
|
7
|
+
* Used at enqueue time to precompute `partition_key`.
|
|
8
|
+
*/
|
|
9
|
+
partitionCount: number;
|
|
10
|
+
/**
|
|
11
|
+
* Object name to read/write. Defaults to `sys_webhook_delivery`. Override
|
|
12
|
+
* only if you've registered the schema under a different name.
|
|
13
|
+
*/
|
|
14
|
+
objectName?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Durable `IWebhookOutbox` backed by ObjectQL — the production storage
|
|
18
|
+
* impl. Works against any registered driver (SQL, Turso, Mongo, in-memory)
|
|
19
|
+
* because everything goes through the driver-agnostic `IDataEngine` API.
|
|
20
|
+
*
|
|
21
|
+
* **Why no `FOR UPDATE SKIP LOCKED`?** ObjectQL is driver-agnostic — that
|
|
22
|
+
* SQL feature is Postgres-only. We get equivalent safety from two layers:
|
|
23
|
+
*
|
|
24
|
+
* 1. `cluster.lock` held per partition by the dispatcher (the primary
|
|
25
|
+
* mutex). One node owns one partition at a time → no two claimers.
|
|
26
|
+
* 2. Atomic `UPDATE WHERE status='pending'` (the backup). Even if two
|
|
27
|
+
* claimers slip through (e.g. admin reschedule + dispatcher), only
|
|
28
|
+
* the first UPDATE matches each row.
|
|
29
|
+
*
|
|
30
|
+
* **Why precompute `partition_key` on enqueue?** ObjectQL has no
|
|
31
|
+
* cross-driver `hash()` function in WHERE clauses. Storing the partition
|
|
32
|
+
* as a column makes the claim query a plain indexed lookup.
|
|
33
|
+
*
|
|
34
|
+
* **Dedup race**: SELECT-then-INSERT has a tiny window where two
|
|
35
|
+
* concurrent producers both miss the SELECT and both INSERT. The unique
|
|
36
|
+
* index `(event_id, webhook_id)` on the table catches it — the second
|
|
37
|
+
* INSERT errors, the producer ignores it. Receivers MUST be idempotent
|
|
38
|
+
* on the `X-Objectstack-Delivery` header anyway.
|
|
39
|
+
*/
|
|
40
|
+
declare class SqlWebhookOutbox implements IWebhookOutbox {
|
|
41
|
+
private readonly engine;
|
|
42
|
+
private readonly objectName;
|
|
43
|
+
private readonly partitionCount;
|
|
44
|
+
constructor(engine: IDataEngine, opts: SqlWebhookOutboxOptions);
|
|
45
|
+
enqueue(input: EnqueueInput): Promise<string>;
|
|
46
|
+
claim(opts: ClaimOptions): Promise<WebhookDelivery[]>;
|
|
47
|
+
ack(id: string, result: AckResult): Promise<void>;
|
|
48
|
+
list(filter?: {
|
|
49
|
+
status?: DeliveryStatus;
|
|
50
|
+
}): Promise<WebhookDelivery[]>;
|
|
51
|
+
private toDelivery;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { SqlWebhookOutbox, type SqlWebhookOutboxOptions };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashPartition
|
|
3
|
+
} from "./chunk-NYSUNT6X.js";
|
|
4
|
+
import {
|
|
5
|
+
SYS_WEBHOOK_DELIVERY
|
|
6
|
+
} from "./chunk-JN76ZRWN.js";
|
|
7
|
+
|
|
8
|
+
// src/sql-outbox.ts
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
var SqlWebhookOutbox = class {
|
|
11
|
+
constructor(engine, opts) {
|
|
12
|
+
this.engine = engine;
|
|
13
|
+
if (opts.partitionCount <= 0) {
|
|
14
|
+
throw new Error("SqlWebhookOutbox: partitionCount must be > 0");
|
|
15
|
+
}
|
|
16
|
+
this.objectName = opts.objectName ?? SYS_WEBHOOK_DELIVERY;
|
|
17
|
+
this.partitionCount = opts.partitionCount;
|
|
18
|
+
}
|
|
19
|
+
async enqueue(input) {
|
|
20
|
+
const existing = await this.engine.findOne(this.objectName, {
|
|
21
|
+
where: { event_id: input.eventId, webhook_id: input.webhookId },
|
|
22
|
+
fields: ["id"]
|
|
23
|
+
});
|
|
24
|
+
if (existing?.id) return existing.id;
|
|
25
|
+
const id = randomUUID();
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const row = {
|
|
28
|
+
id,
|
|
29
|
+
webhook_id: input.webhookId,
|
|
30
|
+
event_id: input.eventId,
|
|
31
|
+
event_type: input.eventType,
|
|
32
|
+
url: input.url,
|
|
33
|
+
method: input.method ?? "POST",
|
|
34
|
+
headers_json: input.headers ? JSON.stringify(input.headers) : void 0,
|
|
35
|
+
secret: input.secret,
|
|
36
|
+
timeout_ms: input.timeoutMs,
|
|
37
|
+
payload_json: JSON.stringify(input.payload ?? null),
|
|
38
|
+
partition_key: hashPartition(input.webhookId, this.partitionCount),
|
|
39
|
+
status: "pending",
|
|
40
|
+
attempts: 0,
|
|
41
|
+
created_at: now,
|
|
42
|
+
updated_at: now
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
await this.engine.insert(this.objectName, row);
|
|
46
|
+
return id;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const winner = await this.engine.findOne(this.objectName, {
|
|
49
|
+
where: { event_id: input.eventId, webhook_id: input.webhookId },
|
|
50
|
+
fields: ["id"]
|
|
51
|
+
});
|
|
52
|
+
if (winner?.id) return winner.id;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async claim(opts) {
|
|
57
|
+
const now = opts.now ?? Date.now();
|
|
58
|
+
await this.engine.update(
|
|
59
|
+
this.objectName,
|
|
60
|
+
{ status: "pending", claimed_by: null, claimed_at: null, updated_at: now },
|
|
61
|
+
{
|
|
62
|
+
where: {
|
|
63
|
+
status: "in_flight",
|
|
64
|
+
claimed_at: { $lt: now - opts.claimTtlMs }
|
|
65
|
+
},
|
|
66
|
+
multi: true
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
const partitionFilter = opts.partition ? { partition_key: opts.partition.index } : {};
|
|
70
|
+
const candidates = await this.engine.find(this.objectName, {
|
|
71
|
+
where: {
|
|
72
|
+
status: "pending",
|
|
73
|
+
...partitionFilter,
|
|
74
|
+
// next_retry_at <= now OR null
|
|
75
|
+
$or: [
|
|
76
|
+
{ next_retry_at: null },
|
|
77
|
+
{ next_retry_at: { $lte: now } }
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
fields: ["id"],
|
|
81
|
+
// No orderBy for portability — drivers handle the natural insert order.
|
|
82
|
+
limit: opts.limit
|
|
83
|
+
});
|
|
84
|
+
if (candidates.length === 0) return [];
|
|
85
|
+
const ids = candidates.map((c) => c.id);
|
|
86
|
+
await this.engine.update(
|
|
87
|
+
this.objectName,
|
|
88
|
+
{
|
|
89
|
+
status: "in_flight",
|
|
90
|
+
claimed_by: opts.nodeId,
|
|
91
|
+
claimed_at: now,
|
|
92
|
+
updated_at: now
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
where: { id: { $in: ids }, status: "pending" },
|
|
96
|
+
multi: true
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
const claimed = await this.engine.find(this.objectName, {
|
|
100
|
+
where: {
|
|
101
|
+
id: { $in: ids },
|
|
102
|
+
claimed_by: opts.nodeId,
|
|
103
|
+
claimed_at: now,
|
|
104
|
+
status: "in_flight"
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return claimed.map((r) => this.toDelivery(r));
|
|
108
|
+
}
|
|
109
|
+
async ack(id, result) {
|
|
110
|
+
const current = await this.engine.findOne(this.objectName, {
|
|
111
|
+
where: { id },
|
|
112
|
+
fields: ["attempts"]
|
|
113
|
+
});
|
|
114
|
+
if (!current) return;
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
let status;
|
|
117
|
+
let nextRetryAt;
|
|
118
|
+
let error;
|
|
119
|
+
if (result.success) {
|
|
120
|
+
status = "success";
|
|
121
|
+
nextRetryAt = null;
|
|
122
|
+
error = null;
|
|
123
|
+
} else if (result.dead) {
|
|
124
|
+
status = "dead";
|
|
125
|
+
nextRetryAt = null;
|
|
126
|
+
error = result.error ?? null;
|
|
127
|
+
} else {
|
|
128
|
+
status = "pending";
|
|
129
|
+
nextRetryAt = result.nextRetryAt ?? null;
|
|
130
|
+
error = result.error ?? null;
|
|
131
|
+
}
|
|
132
|
+
await this.engine.update(
|
|
133
|
+
this.objectName,
|
|
134
|
+
{
|
|
135
|
+
status,
|
|
136
|
+
attempts: (current.attempts ?? 0) + 1,
|
|
137
|
+
last_attempted_at: now,
|
|
138
|
+
claimed_by: null,
|
|
139
|
+
claimed_at: null,
|
|
140
|
+
response_code: result.httpStatus ?? null,
|
|
141
|
+
response_body: result.responseBody ?? null,
|
|
142
|
+
next_retry_at: nextRetryAt,
|
|
143
|
+
error,
|
|
144
|
+
updated_at: now
|
|
145
|
+
},
|
|
146
|
+
{ where: { id }, multi: false }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
async list(filter) {
|
|
150
|
+
const rows = await this.engine.find(this.objectName, {
|
|
151
|
+
where: filter?.status ? { status: filter.status } : {}
|
|
152
|
+
});
|
|
153
|
+
return rows.map((r) => this.toDelivery(r));
|
|
154
|
+
}
|
|
155
|
+
toDelivery(r) {
|
|
156
|
+
return {
|
|
157
|
+
id: r.id,
|
|
158
|
+
webhookId: r.webhook_id,
|
|
159
|
+
eventId: r.event_id,
|
|
160
|
+
eventType: r.event_type,
|
|
161
|
+
url: r.url,
|
|
162
|
+
method: r.method ?? void 0,
|
|
163
|
+
headers: r.headers_json ? JSON.parse(r.headers_json) : void 0,
|
|
164
|
+
secret: r.secret ?? void 0,
|
|
165
|
+
timeoutMs: r.timeout_ms ?? void 0,
|
|
166
|
+
payload: JSON.parse(r.payload_json),
|
|
167
|
+
status: r.status,
|
|
168
|
+
attempts: r.attempts,
|
|
169
|
+
claimedBy: r.claimed_by ?? void 0,
|
|
170
|
+
claimedAt: r.claimed_at ?? void 0,
|
|
171
|
+
nextRetryAt: r.next_retry_at ?? void 0,
|
|
172
|
+
lastAttemptedAt: r.last_attempted_at ?? void 0,
|
|
173
|
+
responseCode: r.response_code ?? void 0,
|
|
174
|
+
responseBody: r.response_body ?? void 0,
|
|
175
|
+
error: r.error ?? void 0,
|
|
176
|
+
createdAt: r.created_at,
|
|
177
|
+
updatedAt: r.updated_at
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
export {
|
|
182
|
+
SqlWebhookOutbox
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=sql-outbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sql-outbox.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { randomUUID } from 'node:crypto';\nimport type { IDataEngine } from '@objectstack/spec/contracts';\nimport type {\n AckResult,\n ClaimOptions,\n DeliveryStatus,\n EnqueueInput,\n IWebhookOutbox,\n WebhookDelivery,\n} from './outbox.js';\nimport { hashPartition } from './partition.js';\nimport { SYS_WEBHOOK_DELIVERY } from './schema.js';\n\nexport interface SqlWebhookOutboxOptions {\n /**\n * Total partition count — MUST match the dispatcher's `partitionCount`.\n * Used at enqueue time to precompute `partition_key`.\n */\n partitionCount: number;\n /**\n * Object name to read/write. Defaults to `sys_webhook_delivery`. Override\n * only if you've registered the schema under a different name.\n */\n objectName?: string;\n}\n\ninterface DeliveryRow {\n id: string;\n webhook_id: string;\n event_id: string;\n event_type: string;\n url: string;\n method?: string | null;\n headers_json?: string | null;\n secret?: string | null;\n timeout_ms?: number | null;\n payload_json: string;\n partition_key: number;\n status: DeliveryStatus;\n attempts: number;\n claimed_by?: string | null;\n claimed_at?: number | null;\n next_retry_at?: number | null;\n last_attempted_at?: number | null;\n response_code?: number | null;\n response_body?: string | null;\n error?: string | null;\n created_at: number;\n updated_at: number;\n}\n\n/**\n * Durable `IWebhookOutbox` backed by ObjectQL — the production storage\n * impl. Works against any registered driver (SQL, Turso, Mongo, in-memory)\n * because everything goes through the driver-agnostic `IDataEngine` API.\n *\n * **Why no `FOR UPDATE SKIP LOCKED`?** ObjectQL is driver-agnostic — that\n * SQL feature is Postgres-only. We get equivalent safety from two layers:\n *\n * 1. `cluster.lock` held per partition by the dispatcher (the primary\n * mutex). One node owns one partition at a time → no two claimers.\n * 2. Atomic `UPDATE WHERE status='pending'` (the backup). Even if two\n * claimers slip through (e.g. admin reschedule + dispatcher), only\n * the first UPDATE matches each row.\n *\n * **Why precompute `partition_key` on enqueue?** ObjectQL has no\n * cross-driver `hash()` function in WHERE clauses. Storing the partition\n * as a column makes the claim query a plain indexed lookup.\n *\n * **Dedup race**: SELECT-then-INSERT has a tiny window where two\n * concurrent producers both miss the SELECT and both INSERT. The unique\n * index `(event_id, webhook_id)` on the table catches it — the second\n * INSERT errors, the producer ignores it. Receivers MUST be idempotent\n * on the `X-Objectstack-Delivery` header anyway.\n */\nexport class SqlWebhookOutbox implements IWebhookOutbox {\n private readonly objectName: string;\n private readonly partitionCount: number;\n\n constructor(\n private readonly engine: IDataEngine,\n opts: SqlWebhookOutboxOptions,\n ) {\n if (opts.partitionCount <= 0) {\n throw new Error('SqlWebhookOutbox: partitionCount must be > 0');\n }\n this.objectName = opts.objectName ?? SYS_WEBHOOK_DELIVERY;\n this.partitionCount = opts.partitionCount;\n }\n\n async enqueue(input: EnqueueInput): Promise<string> {\n // Cheap pre-check to absorb most duplicates without hitting the\n // unique-index error path. Race window with the INSERT below is\n // intentional and documented.\n const existing = await this.engine.findOne(this.objectName, {\n where: { event_id: input.eventId, webhook_id: input.webhookId },\n fields: ['id'],\n });\n if (existing?.id) return existing.id as string;\n\n const id = randomUUID();\n const now = Date.now();\n const row: Omit<DeliveryRow, 'response_body' | 'error'> = {\n id,\n webhook_id: input.webhookId,\n event_id: input.eventId,\n event_type: input.eventType,\n url: input.url,\n method: input.method ?? 'POST',\n headers_json: input.headers ? JSON.stringify(input.headers) : undefined,\n secret: input.secret,\n timeout_ms: input.timeoutMs,\n payload_json: JSON.stringify(input.payload ?? null),\n partition_key: hashPartition(input.webhookId, this.partitionCount),\n status: 'pending',\n attempts: 0,\n created_at: now,\n updated_at: now,\n };\n try {\n await this.engine.insert(this.objectName, row);\n return id;\n } catch (err) {\n // Unique-index collision (dedup race) → look up the winner and\n // return its id. Any other error propagates.\n const winner = await this.engine.findOne(this.objectName, {\n where: { event_id: input.eventId, webhook_id: input.webhookId },\n fields: ['id'],\n });\n if (winner?.id) return winner.id as string;\n throw err;\n }\n }\n\n async claim(opts: ClaimOptions): Promise<WebhookDelivery[]> {\n const now = opts.now ?? Date.now();\n\n // 1. Reap stale in_flight rows — visibility-timeout recovery.\n await this.engine.update(\n this.objectName,\n { status: 'pending', claimed_by: null, claimed_at: null, updated_at: now },\n {\n where: {\n status: 'in_flight',\n claimed_at: { $lt: now - opts.claimTtlMs },\n },\n multi: true,\n },\n );\n\n // 2. Pick candidate ids.\n const partitionFilter = opts.partition\n ? { partition_key: opts.partition.index }\n : {};\n const candidates = await this.engine.find(this.objectName, {\n where: {\n status: 'pending',\n ...partitionFilter,\n // next_retry_at <= now OR null\n $or: [\n { next_retry_at: null },\n { next_retry_at: { $lte: now } },\n ],\n },\n fields: ['id'],\n // No orderBy for portability — drivers handle the natural insert order.\n limit: opts.limit,\n });\n if (candidates.length === 0) return [];\n\n const ids = (candidates as Array<{ id: string }>).map((c) => c.id);\n\n // 3. Atomic claim. WHERE status='pending' rejects any rows another\n // worker swept up between steps 2 and 3.\n await this.engine.update(\n this.objectName,\n {\n status: 'in_flight',\n claimed_by: opts.nodeId,\n claimed_at: now,\n updated_at: now,\n },\n {\n where: { id: { $in: ids }, status: 'pending' },\n multi: true,\n },\n );\n\n // 4. Read back the rows we actually own.\n const claimed = (await this.engine.find(this.objectName, {\n where: {\n id: { $in: ids },\n claimed_by: opts.nodeId,\n claimed_at: now,\n status: 'in_flight',\n },\n })) as DeliveryRow[];\n\n return claimed.map((r) => this.toDelivery(r));\n }\n\n async ack(id: string, result: AckResult): Promise<void> {\n // ObjectQL has no atomic $inc across drivers, so read-then-write.\n // Safe enough: ack is single-writer per row (only the claimer acks).\n const current = (await this.engine.findOne(this.objectName, {\n where: { id },\n fields: ['attempts'],\n })) as { attempts?: number } | null;\n if (!current) return;\n\n const now = Date.now();\n let status: DeliveryStatus;\n let nextRetryAt: number | null;\n let error: string | null;\n\n if (result.success) {\n status = 'success';\n nextRetryAt = null;\n error = null;\n } else if (result.dead) {\n status = 'dead';\n nextRetryAt = null;\n error = result.error ?? null;\n } else {\n status = 'pending';\n nextRetryAt = result.nextRetryAt ?? null;\n error = result.error ?? null;\n }\n\n await this.engine.update(\n this.objectName,\n {\n status,\n attempts: (current.attempts ?? 0) + 1,\n last_attempted_at: now,\n claimed_by: null,\n claimed_at: null,\n response_code: result.httpStatus ?? null,\n response_body: result.responseBody ?? null,\n next_retry_at: nextRetryAt,\n error,\n updated_at: now,\n },\n { where: { id }, multi: false },\n );\n }\n\n async list(filter?: { status?: DeliveryStatus }): Promise<WebhookDelivery[]> {\n const rows = (await this.engine.find(this.objectName, {\n where: filter?.status ? { status: filter.status } : {},\n })) as DeliveryRow[];\n return rows.map((r) => this.toDelivery(r));\n }\n\n private toDelivery(r: DeliveryRow): WebhookDelivery {\n return {\n id: r.id,\n webhookId: r.webhook_id,\n eventId: r.event_id,\n eventType: r.event_type,\n url: r.url,\n method: r.method ?? undefined,\n headers: r.headers_json ? JSON.parse(r.headers_json) : undefined,\n secret: r.secret ?? undefined,\n timeoutMs: r.timeout_ms ?? undefined,\n payload: JSON.parse(r.payload_json),\n status: r.status,\n attempts: r.attempts,\n claimedBy: r.claimed_by ?? undefined,\n claimedAt: r.claimed_at ?? undefined,\n nextRetryAt: r.next_retry_at ?? undefined,\n lastAttemptedAt: r.last_attempted_at ?? undefined,\n responseCode: r.response_code ?? undefined,\n responseBody: r.response_body ?? undefined,\n error: r.error ?? undefined,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-webhooks",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
11
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.
|
|
12
|
-
"require": "./dist/index.
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./sql": {
|
|
16
|
+
"types": "./dist/sql-outbox.d.ts",
|
|
17
|
+
"import": "./dist/sql-outbox.js",
|
|
18
|
+
"require": "./dist/sql-outbox.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./schema": {
|
|
21
|
+
"types": "./dist/schema.d.ts",
|
|
22
|
+
"import": "./dist/schema.js",
|
|
23
|
+
"require": "./dist/schema.cjs"
|
|
13
24
|
}
|
|
14
25
|
},
|
|
15
26
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "5.
|
|
17
|
-
"@objectstack/
|
|
27
|
+
"@objectstack/core": "5.2.0",
|
|
28
|
+
"@objectstack/platform-objects": "5.2.0",
|
|
29
|
+
"@objectstack/spec": "5.2.0",
|
|
30
|
+
"@objectstack/service-cluster": "5.1.1"
|
|
18
31
|
},
|
|
19
32
|
"devDependencies": {
|
|
20
33
|
"@types/node": "^25.9.1",
|
|
@@ -24,11 +37,18 @@
|
|
|
24
37
|
"keywords": [
|
|
25
38
|
"objectstack",
|
|
26
39
|
"plugin",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
40
|
+
"webhook",
|
|
41
|
+
"outbox",
|
|
42
|
+
"cluster"
|
|
29
43
|
],
|
|
44
|
+
"author": "ObjectStack",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/objectstack-ai/framework.git",
|
|
48
|
+
"directory": "packages/plugins/plugin-webhooks"
|
|
49
|
+
},
|
|
30
50
|
"scripts": {
|
|
31
|
-
"build": "tsup
|
|
32
|
-
"test": "vitest run
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"test": "vitest run"
|
|
33
53
|
}
|
|
34
54
|
}
|