jazz-webhook 0.18.28
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 +4 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE.txt +19 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/successMap.d.ts +13 -0
- package/dist/successMap.d.ts.map +1 -0
- package/dist/successMap.js +32 -0
- package/dist/successMap.js.map +1 -0
- package/dist/test/http-server.d.ts +73 -0
- package/dist/test/http-server.d.ts.map +1 -0
- package/dist/test/http-server.js +177 -0
- package/dist/test/http-server.js.map +1 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +11 -0
- package/dist/test/setup.js.map +1 -0
- package/dist/test/successMap.test.d.ts +2 -0
- package/dist/test/successMap.test.d.ts.map +1 -0
- package/dist/test/successMap.test.js +172 -0
- package/dist/test/successMap.test.js.map +1 -0
- package/dist/test/webhook.test.d.ts +2 -0
- package/dist/test/webhook.test.d.ts.map +1 -0
- package/dist/test/webhook.test.js +356 -0
- package/dist/test/webhook.test.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook.d.ts +67 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +281 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +2 -0
- package/src/successMap.ts +55 -0
- package/src/test/http-server.ts +233 -0
- package/src/test/setup.ts +12 -0
- package/src/test/successMap.test.ts +215 -0
- package/src/test/webhook.test.ts +586 -0
- package/src/types.ts +106 -0
- package/src/webhook.ts +387 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +14 -0
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { co, z, Account } from "jazz-tools";
|
|
2
|
+
import { getTransactionsToTry, getTxIdKey, markSuccessful, SuccessMap, } from "./successMap.js";
|
|
3
|
+
export const WebhookRegistration = co.map({
|
|
4
|
+
webhookUrl: z.string(),
|
|
5
|
+
coValueId: z.string(),
|
|
6
|
+
active: z.boolean(),
|
|
7
|
+
successMap: SuccessMap,
|
|
8
|
+
});
|
|
9
|
+
export const RegistryState = co.record(z.string(), WebhookRegistration);
|
|
10
|
+
export const DEFAULT_JazzWebhookOptions = {
|
|
11
|
+
maxRetries: 3,
|
|
12
|
+
baseDelayMs: 20000,
|
|
13
|
+
};
|
|
14
|
+
export class WebhookRegistry {
|
|
15
|
+
constructor(registry, options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.activeSubscriptions = new Map();
|
|
18
|
+
this.rootUnsubscribe = null;
|
|
19
|
+
this.state = registry;
|
|
20
|
+
}
|
|
21
|
+
static createRegistry(owner) {
|
|
22
|
+
return RegistryState.create({}, owner);
|
|
23
|
+
}
|
|
24
|
+
static async loadAndStart(registryId, options = {}) {
|
|
25
|
+
const registry = await RegistryState.load(registryId);
|
|
26
|
+
if (!registry) {
|
|
27
|
+
throw new Error(`Webhook registry with ID ${registryId} not found`);
|
|
28
|
+
}
|
|
29
|
+
const webhook = new WebhookRegistry(registry, options);
|
|
30
|
+
webhook.start();
|
|
31
|
+
return webhook;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Registers a new webhook for a CoValue.
|
|
35
|
+
*
|
|
36
|
+
* @param webhookUrl - The HTTP URL to call when the CoValue changes
|
|
37
|
+
* @param coValueId - The ID of the CoValue to monitor (must start with "co_z")
|
|
38
|
+
* @returns The ID of the registered webhook
|
|
39
|
+
*/
|
|
40
|
+
async register(webhookUrl, coValueId) {
|
|
41
|
+
const registrationId = await registerWebhook({
|
|
42
|
+
registryId: this.state.$jazz.id,
|
|
43
|
+
webhookUrl,
|
|
44
|
+
coValueId,
|
|
45
|
+
});
|
|
46
|
+
// wait for registration to become visible in the registry
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
this.state.$jazz.subscribe((state, unsubscribe) => {
|
|
49
|
+
if (state.$jazz.refs[registrationId]) {
|
|
50
|
+
resolve(registrationId);
|
|
51
|
+
unsubscribe();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Unregisters a webhook and stops monitoring the CoValue.
|
|
58
|
+
*
|
|
59
|
+
* @param webhookId - The ID of the webhook to unregister
|
|
60
|
+
*/
|
|
61
|
+
unregister(webhookId) {
|
|
62
|
+
const webhook = this.state[webhookId];
|
|
63
|
+
if (!webhook) {
|
|
64
|
+
throw new Error(`Webhook with ID ${webhookId} not found`);
|
|
65
|
+
}
|
|
66
|
+
this.state.$jazz.delete(webhookId);
|
|
67
|
+
webhook.$jazz.set("active", false);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Starts monitoring a CoValue and emitting webhooks when it changes.
|
|
71
|
+
*
|
|
72
|
+
* @param webhookId - The ID of the webhook to start monitoring
|
|
73
|
+
*/
|
|
74
|
+
subscribe(webhook) {
|
|
75
|
+
const emitter = new WebhookEmitter(webhook, this.options);
|
|
76
|
+
this.activeSubscriptions.set(webhook.$jazz.id, () => {
|
|
77
|
+
emitter.stop();
|
|
78
|
+
this.activeSubscriptions.delete(webhook.$jazz.id);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Starts monitoring all active webhooks in the registry.
|
|
83
|
+
*
|
|
84
|
+
* This method iterates through all webhooks in the registry and starts
|
|
85
|
+
* subscriptions for any active webhooks that don't already have active
|
|
86
|
+
* subscriptions. This is useful for resuming webhook monitoring after
|
|
87
|
+
* a shutdown or when loading an existing registry.
|
|
88
|
+
*/
|
|
89
|
+
async start() {
|
|
90
|
+
if (this.rootUnsubscribe) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// TODO: this would be much more efficient with subscription diffs
|
|
94
|
+
const createAndDeleteSubscriptions = (registry) => {
|
|
95
|
+
for (const webhook of Object.values(registry)) {
|
|
96
|
+
const exists = this.activeSubscriptions.has(webhook.$jazz.id);
|
|
97
|
+
if (webhook.active && !exists) {
|
|
98
|
+
this.subscribe(webhook);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const [id, unsubscribe] of this.activeSubscriptions.entries()) {
|
|
102
|
+
if (!registry[id]) {
|
|
103
|
+
unsubscribe();
|
|
104
|
+
this.activeSubscriptions.delete(id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
this.rootUnsubscribe = this.state.$jazz.subscribe({
|
|
109
|
+
resolve: {
|
|
110
|
+
$each: true,
|
|
111
|
+
},
|
|
112
|
+
}, createAndDeleteSubscriptions);
|
|
113
|
+
}
|
|
114
|
+
shutdown() {
|
|
115
|
+
for (const unsubscribe of this.activeSubscriptions.values()) {
|
|
116
|
+
unsubscribe();
|
|
117
|
+
}
|
|
118
|
+
this.rootUnsubscribe?.();
|
|
119
|
+
this.rootUnsubscribe = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
class WebhookError extends Error {
|
|
123
|
+
constructor(message, retryAfterMs) {
|
|
124
|
+
super(message);
|
|
125
|
+
this.retryAfterMs = retryAfterMs;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Manages webhook emission with queuing, retry logic, and exponential backoff.
|
|
130
|
+
*
|
|
131
|
+
* The WebhookEmitter handles sending HTTP POST requests to webhook callbacks when
|
|
132
|
+
* a CoValue changes. It implements several key behaviors:
|
|
133
|
+
*
|
|
134
|
+
*/
|
|
135
|
+
class WebhookEmitter {
|
|
136
|
+
constructor(webhook, options = {}) {
|
|
137
|
+
this.webhook = webhook;
|
|
138
|
+
this.options = options;
|
|
139
|
+
this.pending = new Map();
|
|
140
|
+
const unsubscribeWebhook = this.webhook.$jazz.subscribe((webhook) => {
|
|
141
|
+
this.webhook = webhook;
|
|
142
|
+
});
|
|
143
|
+
const unsubscribeValue = this.webhook.$jazz.localNode.subscribe(this.webhook.coValueId, async (value) => {
|
|
144
|
+
if (value === "unavailable") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!this.webhook.active) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const todo = getTransactionsToTry(await this.loadSuccessMap(), value.core.knownState());
|
|
151
|
+
for (const txID of todo) {
|
|
152
|
+
if (!this.pending.has(getTxIdKey(txID))) {
|
|
153
|
+
this.makeAttempt(txID);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
this.unsubscribe = () => {
|
|
158
|
+
unsubscribeWebhook();
|
|
159
|
+
unsubscribeValue();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async loadSuccessMap() {
|
|
163
|
+
if (this.loadedSuccessMap) {
|
|
164
|
+
return this.loadedSuccessMap;
|
|
165
|
+
}
|
|
166
|
+
if (!this.successMap) {
|
|
167
|
+
this.successMap = this.webhook.$jazz
|
|
168
|
+
.ensureLoaded({ resolve: { successMap: true } })
|
|
169
|
+
.then((loaded) => loaded.successMap);
|
|
170
|
+
}
|
|
171
|
+
this.loadedSuccessMap = await this.successMap;
|
|
172
|
+
return this.loadedSuccessMap;
|
|
173
|
+
}
|
|
174
|
+
getRetryDelay(nRetries) {
|
|
175
|
+
const baseDelayMs = this.options.baseDelayMs || DEFAULT_JazzWebhookOptions.baseDelayMs;
|
|
176
|
+
return baseDelayMs * 2 ** nRetries;
|
|
177
|
+
}
|
|
178
|
+
getMaxRetries() {
|
|
179
|
+
return this.options.maxRetries || DEFAULT_JazzWebhookOptions.maxRetries;
|
|
180
|
+
}
|
|
181
|
+
stop() {
|
|
182
|
+
this.pending.forEach((entry) => {
|
|
183
|
+
clearTimeout(entry.timeout);
|
|
184
|
+
});
|
|
185
|
+
this.pending.clear();
|
|
186
|
+
this.unsubscribe();
|
|
187
|
+
}
|
|
188
|
+
makeAttempt(txID) {
|
|
189
|
+
const txIdKey = getTxIdKey(txID);
|
|
190
|
+
let entry = this.pending.get(txIdKey);
|
|
191
|
+
if (entry && entry.nRetries >= this.getMaxRetries()) {
|
|
192
|
+
// TODO: should we track failed transactions?
|
|
193
|
+
this.pending.delete(txIdKey);
|
|
194
|
+
clearTimeout(entry.timeout);
|
|
195
|
+
console.error(`Max retries reached for txID: ${txIdKey} on webhook: ${this.webhook.$jazz.id}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (entry) {
|
|
199
|
+
entry.nRetries++;
|
|
200
|
+
clearTimeout(entry.timeout);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
entry = { nRetries: 0 };
|
|
204
|
+
this.pending.set(txIdKey, entry);
|
|
205
|
+
}
|
|
206
|
+
const scheduleRetry = (delayMs) => {
|
|
207
|
+
const delay = delayMs ?? this.getRetryDelay(entry?.nRetries || 0);
|
|
208
|
+
entry.timeout = setTimeout(() => {
|
|
209
|
+
this.makeAttempt(txID);
|
|
210
|
+
}, delay);
|
|
211
|
+
};
|
|
212
|
+
fetch(this.webhook.webhookUrl, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: {
|
|
215
|
+
"Content-Type": "application/json",
|
|
216
|
+
},
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
coValueId: this.webhook.coValueId,
|
|
219
|
+
txID: txID,
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
.then(async (response) => {
|
|
223
|
+
if (response.ok) {
|
|
224
|
+
const successMap = await this.loadSuccessMap();
|
|
225
|
+
markSuccessful(successMap, txID);
|
|
226
|
+
this.pending.delete(txIdKey);
|
|
227
|
+
clearTimeout(entry.timeout);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let retryAfterMs;
|
|
231
|
+
if (response.headers.has("Retry-After")) {
|
|
232
|
+
try {
|
|
233
|
+
retryAfterMs =
|
|
234
|
+
parseFloat(response.headers.get("Retry-After")) * 1000;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
console.warn(`Invalid Retry-After header: ${response.headers.get("Retry-After")}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new WebhookError(`HTTP ${response.status}: ${response.statusText}`, retryAfterMs);
|
|
241
|
+
})
|
|
242
|
+
.catch((error) => {
|
|
243
|
+
scheduleRetry(error instanceof WebhookError ? error.retryAfterMs : undefined);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export const registerWebhook = async (options) => {
|
|
248
|
+
const { webhookUrl, coValueId, registryId } = {
|
|
249
|
+
registryId: process.env.JAZZ_WEBHOOK_REGISTRY_ID,
|
|
250
|
+
...options,
|
|
251
|
+
};
|
|
252
|
+
if (!registryId) {
|
|
253
|
+
throw new Error("Invalid webhook secret");
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
new URL(webhookUrl);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
throw new Error(`Invalid webhook URL: ${webhookUrl}`);
|
|
260
|
+
}
|
|
261
|
+
if (!registryId.startsWith("co_z")) {
|
|
262
|
+
throw new Error(`Invalid Registry ID format: ${coValueId}. Expected format: co_z...`);
|
|
263
|
+
}
|
|
264
|
+
if (!coValueId.startsWith("co_z")) {
|
|
265
|
+
throw new Error(`Invalid CoValue ID format: ${coValueId}. Expected format: co_z...`);
|
|
266
|
+
}
|
|
267
|
+
const registry = await RegistryState.load(registryId);
|
|
268
|
+
if (!registry) {
|
|
269
|
+
throw new Error(`Couldn't load registry with ID ${registryId}`);
|
|
270
|
+
}
|
|
271
|
+
const registration = WebhookRegistration.create({
|
|
272
|
+
webhookUrl,
|
|
273
|
+
coValueId,
|
|
274
|
+
active: true,
|
|
275
|
+
successMap: SuccessMap.create({}, registry.$jazz.owner),
|
|
276
|
+
}, registry.$jazz.owner);
|
|
277
|
+
registry.$jazz.set(registration.$jazz.id, registration);
|
|
278
|
+
await Account.getMe().$jazz.waitForAllCoValuesSync();
|
|
279
|
+
return registration.$jazz.id;
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jazz-webhook",
|
|
3
|
+
"module": "dist/index.js",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"version": "0.18.28",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cojson": "0.18.28",
|
|
17
|
+
"cojson-storage-sqlite": "0.18.28",
|
|
18
|
+
"cojson-transport-ws": "0.18.28",
|
|
19
|
+
"jazz-tools": "0.18.28"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/ws": "8.5.10",
|
|
23
|
+
"typescript": "5.6.2",
|
|
24
|
+
"vitest": "3.2.4"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"format-and-lint": "biome check .",
|
|
28
|
+
"format-and-lint:fix": "biome check . --write",
|
|
29
|
+
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist && chmod +x ./dist/index.js",
|
|
30
|
+
"test": "vitest --run --root ../../ --project jazz-webhook",
|
|
31
|
+
"dev": "tsc --watch --sourceMap --outDir dist",
|
|
32
|
+
"build-and-run": "pnpm turbo build && ./dist/index.js sync --in-memory"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CojsonInternalTypes, SessionID } from "cojson";
|
|
2
|
+
import { co, z } from "jazz-tools";
|
|
3
|
+
|
|
4
|
+
export const SuccessMap = co.record(
|
|
5
|
+
z.string(), // stringified transaction ID
|
|
6
|
+
z.boolean(),
|
|
7
|
+
);
|
|
8
|
+
export type SuccessMap = co.loaded<typeof SuccessMap>;
|
|
9
|
+
|
|
10
|
+
export type TxIdKey = `${SessionID}:${number}`;
|
|
11
|
+
|
|
12
|
+
export function getTxIdKey(txID: {
|
|
13
|
+
sessionID: SessionID;
|
|
14
|
+
txIndex: number;
|
|
15
|
+
}): TxIdKey {
|
|
16
|
+
return `${txID.sessionID}:${txID.txIndex}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function markSuccessful(
|
|
20
|
+
successMap: SuccessMap,
|
|
21
|
+
txID: CojsonInternalTypes.TransactionID,
|
|
22
|
+
) {
|
|
23
|
+
let success = successMap[getTxIdKey(txID)];
|
|
24
|
+
if (!success) {
|
|
25
|
+
successMap.$jazz.set(getTxIdKey(txID), true);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getTransactionsToTry(
|
|
30
|
+
successMap: SuccessMap,
|
|
31
|
+
knownState: CojsonInternalTypes.CoValueKnownState,
|
|
32
|
+
) {
|
|
33
|
+
const result: CojsonInternalTypes.TransactionID[] = [];
|
|
34
|
+
// TODO: optimisation: we can likely avoid even constructing a CoMap view
|
|
35
|
+
// and just get/set raw transactions from the CoValueCore of SuccessMap
|
|
36
|
+
for (const [sessionID, knownTxCount] of Object.entries(knownState.sessions)) {
|
|
37
|
+
for (let txIndex = 0; txIndex < knownTxCount; txIndex++) {
|
|
38
|
+
if (!successMap[`${sessionID}:${txIndex}`]) {
|
|
39
|
+
result.push({
|
|
40
|
+
sessionID: sessionID as SessionID,
|
|
41
|
+
txIndex: txIndex,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isTxSuccessful(
|
|
51
|
+
successMap: SuccessMap,
|
|
52
|
+
txID: CojsonInternalTypes.TransactionID,
|
|
53
|
+
) {
|
|
54
|
+
return successMap[getTxIdKey(txID)] ?? false;
|
|
55
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, ServerResponse, Server } from "http";
|
|
2
|
+
import { AddressInfo } from "net";
|
|
3
|
+
import { EventEmitter } from "events";
|
|
4
|
+
import { CojsonInternalTypes } from "cojson";
|
|
5
|
+
|
|
6
|
+
export interface WebhookRequest {
|
|
7
|
+
coValueId: string;
|
|
8
|
+
txID: CojsonInternalTypes.TransactionID;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WebhookResponse {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
body?: string;
|
|
15
|
+
delay?: number;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class WebhookTestServer {
|
|
20
|
+
private server: Server;
|
|
21
|
+
private port: number = 0;
|
|
22
|
+
private url: string = "";
|
|
23
|
+
private responseIndex: number = 0;
|
|
24
|
+
|
|
25
|
+
public readonly requests: WebhookRequest[] = [];
|
|
26
|
+
public readonly responses: WebhookResponse[] = [];
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
this.server = createServer(this.handleRequest.bind(this));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
listeners = new Set<() => void>();
|
|
33
|
+
addRequestListener(listener: () => void): void {
|
|
34
|
+
this.listeners.add(listener);
|
|
35
|
+
}
|
|
36
|
+
removeRequestListener(listener: () => void): void {
|
|
37
|
+
this.listeners.delete(listener);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
emitRequest(): void {
|
|
41
|
+
for (const listener of this.listeners) {
|
|
42
|
+
listener();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
|
47
|
+
if (req.method === "POST" && req.url === "/webhook") {
|
|
48
|
+
let body = "";
|
|
49
|
+
|
|
50
|
+
req.on("data", (chunk) => {
|
|
51
|
+
body += chunk.toString();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
req.on("end", async () => {
|
|
55
|
+
try {
|
|
56
|
+
const response = this.responses[this.responseIndex] || {
|
|
57
|
+
statusCode: 200,
|
|
58
|
+
};
|
|
59
|
+
this.responseIndex++;
|
|
60
|
+
|
|
61
|
+
// Add delay if specified
|
|
62
|
+
if (response.delay) {
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, response.delay));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
res.statusCode = response.statusCode;
|
|
67
|
+
res.setHeader("Content-Type", "application/json");
|
|
68
|
+
if (response.headers) {
|
|
69
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
70
|
+
res.setHeader(key, value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
res.end(response.body || JSON.stringify({ received: true }));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
res.statusCode = 400;
|
|
76
|
+
res.setHeader("Content-Type", "application/json");
|
|
77
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const webhookData: WebhookRequest = JSON.parse(body);
|
|
81
|
+
this.requests.push(webhookData);
|
|
82
|
+
|
|
83
|
+
console.log("request " + this.requests.length, webhookData);
|
|
84
|
+
|
|
85
|
+
this.emitRequest();
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
res.statusCode = 404;
|
|
89
|
+
res.end("Not Found");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Starts the server on an available port
|
|
95
|
+
*/
|
|
96
|
+
async start(): Promise<void> {
|
|
97
|
+
return new Promise<void>((resolve, reject) => {
|
|
98
|
+
this.server.listen(0, () => {
|
|
99
|
+
try {
|
|
100
|
+
const address = this.server.address() as AddressInfo;
|
|
101
|
+
this.port = address.port;
|
|
102
|
+
this.url = `http://localhost:${this.port}/webhook`;
|
|
103
|
+
resolve();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
reject(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.server.on("error", reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stops the server
|
|
115
|
+
*/
|
|
116
|
+
async close(): Promise<void> {
|
|
117
|
+
return new Promise<void>((resolve) => {
|
|
118
|
+
this.server.close(() => {
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resets all stored requests and responses
|
|
126
|
+
*/
|
|
127
|
+
reset(): void {
|
|
128
|
+
this.requests.length = 0;
|
|
129
|
+
this.responses.length = 0;
|
|
130
|
+
this.responseIndex = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sets a response for a specific request index
|
|
135
|
+
*/
|
|
136
|
+
setResponse(
|
|
137
|
+
index: number,
|
|
138
|
+
statusCode: number,
|
|
139
|
+
body?: string,
|
|
140
|
+
delay?: number,
|
|
141
|
+
headers?: Record<string, string>,
|
|
142
|
+
): void {
|
|
143
|
+
this.responses[index] = { statusCode, body, delay, headers };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Waits for a specific number of requests to be received
|
|
148
|
+
*/
|
|
149
|
+
async waitForRequests(
|
|
150
|
+
count: number,
|
|
151
|
+
timeout: number = 5000,
|
|
152
|
+
): Promise<WebhookRequest[]> {
|
|
153
|
+
return new Promise<WebhookRequest[]>((resolve, reject) => {
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
this.removeRequestListener(onRequest);
|
|
156
|
+
reject(
|
|
157
|
+
new Error(
|
|
158
|
+
`Timeout waiting for ${count} requests. Got ${this.requests.length}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}, timeout);
|
|
162
|
+
|
|
163
|
+
const onRequest = () => {
|
|
164
|
+
if (this.requests.length >= count) {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
this.removeRequestListener(onRequest);
|
|
167
|
+
resolve(this.requests);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
this.addRequestListener(onRequest);
|
|
172
|
+
onRequest();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Gets the server port
|
|
178
|
+
*/
|
|
179
|
+
getPort(): number {
|
|
180
|
+
return this.port;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gets the webhook URL
|
|
185
|
+
*/
|
|
186
|
+
getUrl(): string {
|
|
187
|
+
return this.url;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Gets the number of requests received
|
|
192
|
+
*/
|
|
193
|
+
getRequestCount(): number {
|
|
194
|
+
return this.requests.length;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
expectSingleRequest(): WebhookRequest {
|
|
198
|
+
if (this.requests.length !== 1) {
|
|
199
|
+
throw new Error("Expected 1 request, got " + this.requests.length);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return this.getLastRequest();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Gets the last request received
|
|
207
|
+
*/
|
|
208
|
+
getLastRequest(): WebhookRequest {
|
|
209
|
+
const lastRequest = this.requests.at(-1);
|
|
210
|
+
if (!lastRequest) {
|
|
211
|
+
throw new Error("No requests received");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return lastRequest;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Checks if the server is running
|
|
219
|
+
*/
|
|
220
|
+
isRunning(): boolean {
|
|
221
|
+
return this.server.listening;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Factory function to create and start a webhook test server
|
|
227
|
+
* @deprecated Use WebhookTestServer class directly
|
|
228
|
+
*/
|
|
229
|
+
export async function createWebhookTestServer(): Promise<WebhookTestServer> {
|
|
230
|
+
const server = new WebhookTestServer();
|
|
231
|
+
await server.start();
|
|
232
|
+
return server;
|
|
233
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { setupJazzTestSync } from "jazz-tools/testing";
|
|
3
|
+
|
|
4
|
+
// Global test setup
|
|
5
|
+
beforeEach(async () => {
|
|
6
|
+
await setupJazzTestSync();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Clean up after each test
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
// Clean up any global state if needed
|
|
12
|
+
});
|