@objectstack/plugin-webhooks 5.0.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 -28
- 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/src/outbox.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook outbox contracts.
|
|
5
|
+
*
|
|
6
|
+
* The outbox stores webhook delivery rows that must be POSTed exactly once
|
|
7
|
+
* (modulo at-least-once + receiver-side idempotency). Implementations are
|
|
8
|
+
* pluggable so the same dispatcher can run against an in-memory test store
|
|
9
|
+
* or a SQL-backed production table.
|
|
10
|
+
*
|
|
11
|
+
* See `content/docs/concepts/webhook-delivery.mdx` §3.2 for the full schema.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type DeliveryStatus =
|
|
15
|
+
| 'pending'
|
|
16
|
+
| 'in_flight'
|
|
17
|
+
| 'success'
|
|
18
|
+
| 'failed'
|
|
19
|
+
| 'dead';
|
|
20
|
+
|
|
21
|
+
export interface WebhookDelivery {
|
|
22
|
+
/** UUID — also doubles as the receiver-side idempotency key. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** FK to sys_webhook.id — opaque to the dispatcher; only used for hashing. */
|
|
25
|
+
webhookId: string;
|
|
26
|
+
/** Origin event id. UNIQUE(event_id, webhook_id) prevents double-enqueue. */
|
|
27
|
+
eventId: string;
|
|
28
|
+
/** Origin event type, e.g. `data.record.created`. */
|
|
29
|
+
eventType: string;
|
|
30
|
+
/** Destination URL (snapshotted on enqueue — config edits don't rewrite live rows). */
|
|
31
|
+
url: string;
|
|
32
|
+
/** HTTP method — defaults to POST. */
|
|
33
|
+
method?: string;
|
|
34
|
+
/** Custom headers configured on the sink. */
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
/** HMAC-SHA256 secret. If present, signature is added. */
|
|
37
|
+
secret?: string;
|
|
38
|
+
/** Per-request timeout in ms. */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** JSON-serialisable body. */
|
|
41
|
+
payload: unknown;
|
|
42
|
+
|
|
43
|
+
/** Lifecycle state. */
|
|
44
|
+
status: DeliveryStatus;
|
|
45
|
+
/** Number of POST attempts made so far (0 before first attempt). */
|
|
46
|
+
attempts: number;
|
|
47
|
+
/** Node id currently working on this row, when `status = in_flight`. */
|
|
48
|
+
claimedBy?: string;
|
|
49
|
+
/** Wall-clock ms when the row was claimed. */
|
|
50
|
+
claimedAt?: number;
|
|
51
|
+
/** Earliest ms at which this row becomes eligible for the next attempt. */
|
|
52
|
+
nextRetryAt?: number;
|
|
53
|
+
/** Wall-clock ms of the last attempt (success or fail). */
|
|
54
|
+
lastAttemptedAt?: number;
|
|
55
|
+
/** HTTP status code from the most recent attempt. */
|
|
56
|
+
responseCode?: number;
|
|
57
|
+
/** Truncated response body for diagnostics. */
|
|
58
|
+
responseBody?: string;
|
|
59
|
+
/** Last transport / timeout error message. */
|
|
60
|
+
error?: string;
|
|
61
|
+
|
|
62
|
+
createdAt: number;
|
|
63
|
+
updatedAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface EnqueueInput {
|
|
67
|
+
webhookId: string;
|
|
68
|
+
eventId: string;
|
|
69
|
+
eventType: string;
|
|
70
|
+
url: string;
|
|
71
|
+
method?: string;
|
|
72
|
+
headers?: Record<string, string>;
|
|
73
|
+
secret?: string;
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
payload: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ClaimOptions {
|
|
79
|
+
/** Identifier of the node doing the claim (for `claimedBy`). */
|
|
80
|
+
nodeId: string;
|
|
81
|
+
/** Max rows to claim per call. */
|
|
82
|
+
limit: number;
|
|
83
|
+
/**
|
|
84
|
+
* Partition assignment for this worker. Only rows whose
|
|
85
|
+
* `hash(webhookId) mod count === index` are claimed. Omit to claim
|
|
86
|
+
* across all partitions (single-node mode).
|
|
87
|
+
*/
|
|
88
|
+
partition?: { index: number; count: number };
|
|
89
|
+
/** Visibility timeout — claimed rows revert to pending after this many ms. */
|
|
90
|
+
claimTtlMs: number;
|
|
91
|
+
/** "Now" reference, ms since epoch. Defaults to Date.now(). */
|
|
92
|
+
now?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface AckSuccess {
|
|
96
|
+
success: true;
|
|
97
|
+
httpStatus: number;
|
|
98
|
+
responseBody?: string;
|
|
99
|
+
durationMs: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AckFailure {
|
|
103
|
+
success: false;
|
|
104
|
+
httpStatus?: number;
|
|
105
|
+
responseBody?: string;
|
|
106
|
+
error?: string;
|
|
107
|
+
durationMs: number;
|
|
108
|
+
/** Computed by the dispatcher per the retry schedule, or undefined for dead. */
|
|
109
|
+
nextRetryAt?: number;
|
|
110
|
+
/** Marks the row terminal — no more attempts. */
|
|
111
|
+
dead?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type AckResult = AckSuccess | AckFailure;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pluggable storage backend for delivery rows. Implementations MUST make
|
|
118
|
+
* `claim()` atomic across concurrent callers — that property is the
|
|
119
|
+
* exactly-once guarantee.
|
|
120
|
+
*/
|
|
121
|
+
export interface IWebhookOutbox {
|
|
122
|
+
/**
|
|
123
|
+
* Insert a new delivery row. Implementations MUST treat
|
|
124
|
+
* `(eventId, webhookId)` as unique and silently drop duplicates.
|
|
125
|
+
* Returns the row id (existing or new).
|
|
126
|
+
*/
|
|
127
|
+
enqueue(input: EnqueueInput): Promise<string>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Atomically claim up to `limit` rows whose `nextRetryAt <= now` (or
|
|
131
|
+
* null) and matching the partition predicate. Claimed rows MUST be
|
|
132
|
+
* marked `in_flight` so concurrent claimers don't see them.
|
|
133
|
+
*/
|
|
134
|
+
claim(opts: ClaimOptions): Promise<WebhookDelivery[]>;
|
|
135
|
+
|
|
136
|
+
/** Record the outcome of an attempt. */
|
|
137
|
+
ack(id: string, result: AckResult): Promise<void>;
|
|
138
|
+
|
|
139
|
+
/** Snapshot accessor for tests / admin tooling. */
|
|
140
|
+
list(filter?: { status?: DeliveryStatus }): Promise<WebhookDelivery[]>;
|
|
141
|
+
}
|
package/src/partition.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable, framework-free partition hash. The dispatcher uses this to
|
|
5
|
+
* assign webhooks to partitions; the in-memory outbox uses the same hash
|
|
6
|
+
* to filter rows in `claim()`. Both call sites MUST agree, which is why
|
|
7
|
+
* this is a single shared helper.
|
|
8
|
+
*
|
|
9
|
+
* Uses a 32-bit FNV-1a variant — fast, no allocations, deterministic.
|
|
10
|
+
*/
|
|
11
|
+
export function hashPartition(key: string, count: number): number {
|
|
12
|
+
if (count <= 0) throw new Error('partition count must be > 0');
|
|
13
|
+
let h = 0x811c9dc5;
|
|
14
|
+
for (let i = 0; i < key.length; i++) {
|
|
15
|
+
h ^= key.charCodeAt(i);
|
|
16
|
+
h = Math.imul(h, 0x01000193);
|
|
17
|
+
}
|
|
18
|
+
return Math.abs(h | 0) % count;
|
|
19
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DeliveryRetentionSweeper test.
|
|
5
|
+
*
|
|
6
|
+
* Verifies the retention policy applied to `sys_webhook_delivery`:
|
|
7
|
+
* - success rows older than `successTtlMs` are deleted
|
|
8
|
+
* - dead rows older than `deadTtlMs` are deleted
|
|
9
|
+
* - pending / in_flight / failed rows are NEVER auto-pruned
|
|
10
|
+
* - rows newer than the TTL stay
|
|
11
|
+
* - successTtlMs=0 disables the success sweep
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import type { IDataEngine } from '@objectstack/spec/contracts';
|
|
16
|
+
import { DeliveryRetentionSweeper } from './retention.js';
|
|
17
|
+
|
|
18
|
+
class FakeEngine implements IDataEngine {
|
|
19
|
+
rows: any[] = [];
|
|
20
|
+
async find(): Promise<any[]> {
|
|
21
|
+
return this.rows;
|
|
22
|
+
}
|
|
23
|
+
async findOne(): Promise<any> {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
async insert(_n: string, data: any): Promise<any> {
|
|
27
|
+
const arr = Array.isArray(data) ? data : [data];
|
|
28
|
+
for (const r of arr) this.rows.push(r);
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
async update(): Promise<any> {
|
|
32
|
+
return { affected: 0 };
|
|
33
|
+
}
|
|
34
|
+
async delete(_name: string, opts?: any): Promise<any> {
|
|
35
|
+
const before = this.rows.length;
|
|
36
|
+
const where = opts?.where ?? {};
|
|
37
|
+
this.rows = this.rows.filter((r) => {
|
|
38
|
+
if (where.status && r.status !== where.status) return true;
|
|
39
|
+
if (where.updated_at?.$lt != null && !(r.updated_at < where.updated_at.$lt))
|
|
40
|
+
return true;
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
return { affected: before - this.rows.length };
|
|
44
|
+
}
|
|
45
|
+
async count(): Promise<number> {
|
|
46
|
+
return this.rows.length;
|
|
47
|
+
}
|
|
48
|
+
async aggregate(): Promise<any[]> {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const HOUR = 60 * 60 * 1000;
|
|
54
|
+
const DAY = 24 * HOUR;
|
|
55
|
+
|
|
56
|
+
describe('DeliveryRetentionSweeper', () => {
|
|
57
|
+
it('deletes old success rows past TTL', async () => {
|
|
58
|
+
const engine = new FakeEngine();
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
engine.rows.push(
|
|
61
|
+
{ id: 'a', status: 'success', updated_at: now - 8 * DAY },
|
|
62
|
+
{ id: 'b', status: 'success', updated_at: now - 6 * DAY }, // inside TTL
|
|
63
|
+
{ id: 'c', status: 'success', updated_at: now - 30 * DAY },
|
|
64
|
+
);
|
|
65
|
+
const sweeper = new DeliveryRetentionSweeper(engine, { successTtlMs: 7 * DAY });
|
|
66
|
+
const res = await sweeper.sweep(now);
|
|
67
|
+
expect(res.success).toBe(2);
|
|
68
|
+
expect(engine.rows.map((r) => r.id)).toEqual(['b']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('keeps pending / in_flight / failed rows regardless of age', async () => {
|
|
72
|
+
const engine = new FakeEngine();
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
engine.rows.push(
|
|
75
|
+
{ id: 'p', status: 'pending', updated_at: now - 100 * DAY },
|
|
76
|
+
{ id: 'i', status: 'in_flight', updated_at: now - 100 * DAY },
|
|
77
|
+
{ id: 'f', status: 'failed', updated_at: now - 100 * DAY },
|
|
78
|
+
);
|
|
79
|
+
const sweeper = new DeliveryRetentionSweeper(engine);
|
|
80
|
+
await sweeper.sweep(now);
|
|
81
|
+
expect(engine.rows).toHaveLength(3);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('deletes old dead rows past deadTtlMs', async () => {
|
|
85
|
+
const engine = new FakeEngine();
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
engine.rows.push(
|
|
88
|
+
{ id: 'd1', status: 'dead', updated_at: now - 31 * DAY },
|
|
89
|
+
{ id: 'd2', status: 'dead', updated_at: now - 29 * DAY }, // inside TTL
|
|
90
|
+
);
|
|
91
|
+
const sweeper = new DeliveryRetentionSweeper(engine, { deadTtlMs: 30 * DAY });
|
|
92
|
+
const res = await sweeper.sweep(now);
|
|
93
|
+
expect(res.dead).toBe(1);
|
|
94
|
+
expect(engine.rows.map((r) => r.id)).toEqual(['d2']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('successTtlMs=0 disables the success sweep', async () => {
|
|
98
|
+
const engine = new FakeEngine();
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
engine.rows.push({ id: 'a', status: 'success', updated_at: now - 100 * DAY });
|
|
101
|
+
const sweeper = new DeliveryRetentionSweeper(engine, { successTtlMs: 0 });
|
|
102
|
+
const res = await sweeper.sweep(now);
|
|
103
|
+
expect(res.success).toBe(0);
|
|
104
|
+
expect(engine.rows).toHaveLength(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('deadTtlMs=0 disables the dead sweep', async () => {
|
|
108
|
+
const engine = new FakeEngine();
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
engine.rows.push({ id: 'd', status: 'dead', updated_at: now - 100 * DAY });
|
|
111
|
+
const sweeper = new DeliveryRetentionSweeper(engine, { deadTtlMs: 0 });
|
|
112
|
+
const res = await sweeper.sweep(now);
|
|
113
|
+
expect(res.dead).toBe(0);
|
|
114
|
+
expect(engine.rows).toHaveLength(1);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/retention.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { IDataEngine } from '@objectstack/spec/contracts';
|
|
4
|
+
import { SYS_WEBHOOK_DELIVERY } from './schema.js';
|
|
5
|
+
|
|
6
|
+
interface OptionalLogger {
|
|
7
|
+
info?(msg: string, meta?: unknown): void;
|
|
8
|
+
warn?(msg: string, meta?: unknown): void;
|
|
9
|
+
debug?(msg: string, meta?: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DeliveryRetentionOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Object name backing the outbox. Defaults to `sys_webhook_delivery`.
|
|
15
|
+
*/
|
|
16
|
+
objectName?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How long to keep `success` rows. Default 7 days. Set to `0` to
|
|
20
|
+
* disable the success sweep (keep forever — not recommended in
|
|
21
|
+
* production).
|
|
22
|
+
*/
|
|
23
|
+
successTtlMs?: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How long to keep `dead` rows. Default 30 days. Set to `0` to
|
|
27
|
+
* keep forever.
|
|
28
|
+
*/
|
|
29
|
+
deadTtlMs?: number;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How often to run the sweep. Default 1h.
|
|
33
|
+
*/
|
|
34
|
+
sweepIntervalMs?: number;
|
|
35
|
+
|
|
36
|
+
logger?: OptionalLogger;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
successTtlMs: 7 * 24 * 60 * 60 * 1000,
|
|
41
|
+
deadTtlMs: 30 * 24 * 60 * 60 * 1000,
|
|
42
|
+
sweepIntervalMs: 60 * 60 * 1000,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Periodically prunes `sys_webhook_delivery` rows so the table doesn't
|
|
47
|
+
* grow unbounded.
|
|
48
|
+
*
|
|
49
|
+
* Without this every successful POST would leave a permanent row. At
|
|
50
|
+
* even moderate scale (10 events/s × 3 webhooks = 30 rows/s = ~2.6M
|
|
51
|
+
* rows/day) the table becomes a problem within a week.
|
|
52
|
+
*
|
|
53
|
+
* Retention defaults mirror Stripe/GitHub:
|
|
54
|
+
* - `success`: 7 days
|
|
55
|
+
* - `dead`: 30 days (kept longer for audit & manual re-delivery)
|
|
56
|
+
* - `pending`/`in_flight`/`failed`: never auto-pruned (they're
|
|
57
|
+
* either live work or signal something needs human attention)
|
|
58
|
+
*
|
|
59
|
+
* Runs on whichever node holds the sweeper interval — it doesn't need
|
|
60
|
+
* a cluster lock because DELETE WHERE created_at < threshold is
|
|
61
|
+
* idempotent; multiple nodes running concurrently is wasteful but
|
|
62
|
+
* safe.
|
|
63
|
+
*/
|
|
64
|
+
export class DeliveryRetentionSweeper {
|
|
65
|
+
private readonly objectName: string;
|
|
66
|
+
private readonly successTtlMs: number;
|
|
67
|
+
private readonly deadTtlMs: number;
|
|
68
|
+
private readonly sweepIntervalMs: number;
|
|
69
|
+
private readonly logger: OptionalLogger;
|
|
70
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
71
|
+
private running = false;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly engine: IDataEngine,
|
|
75
|
+
opts: DeliveryRetentionOptions = {},
|
|
76
|
+
) {
|
|
77
|
+
this.objectName = opts.objectName ?? SYS_WEBHOOK_DELIVERY;
|
|
78
|
+
this.successTtlMs = opts.successTtlMs ?? DEFAULTS.successTtlMs;
|
|
79
|
+
this.deadTtlMs = opts.deadTtlMs ?? DEFAULTS.deadTtlMs;
|
|
80
|
+
this.sweepIntervalMs = opts.sweepIntervalMs ?? DEFAULTS.sweepIntervalMs;
|
|
81
|
+
this.logger = opts.logger ?? {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
start(): void {
|
|
85
|
+
if (this.running) return;
|
|
86
|
+
this.running = true;
|
|
87
|
+
// First sweep deferred by one interval — let the system boot first.
|
|
88
|
+
this.timer = setInterval(() => {
|
|
89
|
+
this.sweep().catch((err) =>
|
|
90
|
+
this.logger.warn?.('[webhook-retention] sweep failed', err),
|
|
91
|
+
);
|
|
92
|
+
}, this.sweepIntervalMs);
|
|
93
|
+
this.timer.unref?.();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
stop(): void {
|
|
97
|
+
if (!this.running) return;
|
|
98
|
+
this.running = false;
|
|
99
|
+
if (this.timer) clearInterval(this.timer);
|
|
100
|
+
this.timer = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Run one sweep immediately. Returns the number of rows deleted. */
|
|
104
|
+
async sweep(now: number = Date.now()): Promise<{ success: number; dead: number }> {
|
|
105
|
+
let successDeleted = 0;
|
|
106
|
+
let deadDeleted = 0;
|
|
107
|
+
|
|
108
|
+
if (this.successTtlMs > 0) {
|
|
109
|
+
try {
|
|
110
|
+
const res = await this.engine.delete(this.objectName, {
|
|
111
|
+
where: {
|
|
112
|
+
status: 'success',
|
|
113
|
+
updated_at: { $lt: now - this.successTtlMs },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
successDeleted = res?.affected ?? 0;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
this.logger.warn?.('[webhook-retention] success sweep failed', err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.deadTtlMs > 0) {
|
|
123
|
+
try {
|
|
124
|
+
const res = await this.engine.delete(this.objectName, {
|
|
125
|
+
where: {
|
|
126
|
+
status: 'dead',
|
|
127
|
+
updated_at: { $lt: now - this.deadTtlMs },
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
deadDeleted = res?.affected ?? 0;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.logger.warn?.('[webhook-retention] dead sweep failed', err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (successDeleted + deadDeleted > 0) {
|
|
137
|
+
this.logger.info?.('[webhook-retention] sweep complete', {
|
|
138
|
+
success: successDeleted,
|
|
139
|
+
dead: deadDeleted,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return { success: successDeleted, dead: deadDeleted };
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public schema subpath: `@objectstack/plugin-webhooks/schema`.
|
|
5
|
+
*
|
|
6
|
+
* Thin re-export barrel kept stable across refactors. The actual object
|
|
7
|
+
* definition lives in `sys-webhook-delivery.object.ts` (matching the
|
|
8
|
+
* `*.object.ts` convention used everywhere else in the monorepo for
|
|
9
|
+
* `sys_*` schemas).
|
|
10
|
+
*
|
|
11
|
+
* Note: callers that just need the runtime should import from the
|
|
12
|
+
* package root (`@objectstack/plugin-webhooks`), which auto-registers
|
|
13
|
+
* `sys_webhook` + `sys_webhook_delivery` via the plugin manifest. This
|
|
14
|
+
* subpath exists for the rare case where you want the schema without
|
|
15
|
+
* installing the dispatcher plugin (e.g. read-only inspection from a
|
|
16
|
+
* different runtime).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
SysWebhookDelivery,
|
|
21
|
+
SYS_WEBHOOK_DELIVERY,
|
|
22
|
+
} from './sys-webhook-delivery.object.js';
|