@hotmeshio/hotmesh 0.16.4 → 0.16.5
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/build/package.json
CHANGED
|
@@ -72,13 +72,10 @@ async function processEvent(instance, status = stream_1.StreamStatus.SUCCESS, co
|
|
|
72
72
|
}
|
|
73
73
|
catch (error) {
|
|
74
74
|
if (error instanceof errors_1.CollationError) {
|
|
75
|
-
//FORBIDDEN: Leg1 not complete —
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//The GUID marker was already committed by notarizeLeg2Entry;
|
|
80
|
-
//on retry, collateLeg2Entry's SETNX is a no-op for the same
|
|
81
|
-
//GUID, and verifySyntheticInteger sees no steps done → allowed.
|
|
75
|
+
//FORBIDDEN: Leg1 not complete — should not occur after the fix
|
|
76
|
+
//that moved setHookSignal to post-commit. If seen, it indicates
|
|
77
|
+
//a new race window not covered by the fix. Rethrow so the inline
|
|
78
|
+
//retry in processWebHookEvent can attempt recovery.
|
|
82
79
|
if (error.fault === collator_1.CollationFaultType.FORBIDDEN) {
|
|
83
80
|
instance.logger.warn('process-event-forbidden-retry', {
|
|
84
81
|
jid: instance.context.metadata.jid,
|
|
@@ -160,6 +160,28 @@ declare class Hook extends Activity {
|
|
|
160
160
|
private redeliverPendingSignal;
|
|
161
161
|
doPassThrough(telemetry: TelemetryService): Promise<void>;
|
|
162
162
|
getHookRule(topic: string): Promise<HookRule | undefined>;
|
|
163
|
+
/**
|
|
164
|
+
* Register the time hook (sleep) inside the Leg1 transaction.
|
|
165
|
+
* Time hooks don't participate in the signal race — they're
|
|
166
|
+
* purely internal timeout registrations.
|
|
167
|
+
*/
|
|
168
|
+
registerTimeHook(transaction: ProviderTransaction): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Register the web hook signal AFTER the Leg1 transaction commits.
|
|
171
|
+
* This ensures the hook signal is never visible before Leg1
|
|
172
|
+
* completion, eliminating the FORBIDDEN window where Leg2 could
|
|
173
|
+
* find the hook but fail on the collation check.
|
|
174
|
+
*
|
|
175
|
+
* If a pending signal was stored by an early-arriving Leg2,
|
|
176
|
+
* setHookSignal atomically detects and returns it.
|
|
177
|
+
*/
|
|
178
|
+
registerWebHookSignal(): Promise<{
|
|
179
|
+
pending?: string;
|
|
180
|
+
} | void>;
|
|
181
|
+
/**
|
|
182
|
+
* @deprecated Use registerTimeHook + registerWebHookSignal instead.
|
|
183
|
+
* Kept for backward compatibility with tests that monkey-patch this method.
|
|
184
|
+
*/
|
|
163
185
|
registerHook(transaction?: ProviderTransaction): Promise<{
|
|
164
186
|
jobId?: string;
|
|
165
187
|
pending?: string;
|
|
@@ -171,6 +171,17 @@ class Hook extends activity_1.Activity {
|
|
|
171
171
|
if (!isResume) {
|
|
172
172
|
await this.doHook(telemetry);
|
|
173
173
|
}
|
|
174
|
+
else if (this.config.hook?.topic) {
|
|
175
|
+
//DUPLICATE: Leg1 completed previously but hook registration
|
|
176
|
+
//may not have happened (crash between transaction.exec and
|
|
177
|
+
//registerWebHookSignal). Attempt registration — setHookSignal
|
|
178
|
+
//is idempotent (returns success:false if hook already exists).
|
|
179
|
+
const hookResult = await this.registerWebHookSignal();
|
|
180
|
+
const pending = hookResult && hookResult.pending;
|
|
181
|
+
if (pending) {
|
|
182
|
+
await this.redeliverPendingSignal(pending);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
else {
|
|
176
187
|
//Category B: passthrough with crash-safe step protocol + GUID ledger
|
|
@@ -206,7 +217,9 @@ class Hook extends activity_1.Activity {
|
|
|
206
217
|
}
|
|
207
218
|
async doHook(telemetry) {
|
|
208
219
|
const transaction = this.store.transact();
|
|
209
|
-
|
|
220
|
+
//register time hooks (sleep) inside the transaction — these
|
|
221
|
+
//don't participate in the signal race
|
|
222
|
+
await this.registerTimeHook(transaction);
|
|
210
223
|
this.mapOutputData();
|
|
211
224
|
this.mapJobData();
|
|
212
225
|
await this.setState(transaction);
|
|
@@ -214,11 +227,15 @@ class Hook extends activity_1.Activity {
|
|
|
214
227
|
await this.setStatus(0, transaction);
|
|
215
228
|
await transaction.exec();
|
|
216
229
|
telemetry.mapActivityAttributes();
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
230
|
+
//register the web hook signal AFTER the transaction commits.
|
|
231
|
+
//this eliminates the FORBIDDEN window: the hook signal is never
|
|
232
|
+
//visible before Leg1 completion. If Leg2 arrives before this
|
|
233
|
+
//point, getHookSignal finds no hook and stores $pending, which
|
|
234
|
+
//setHookSignal will detect and return for redelivery.
|
|
235
|
+
const hookResult = await this.registerWebHookSignal();
|
|
236
|
+
const pending = hookResult && hookResult.pending;
|
|
237
|
+
if (pending) {
|
|
238
|
+
await this.redeliverPendingSignal(pending);
|
|
222
239
|
}
|
|
223
240
|
}
|
|
224
241
|
/**
|
|
@@ -259,12 +276,44 @@ class Hook extends activity_1.Activity {
|
|
|
259
276
|
const rules = await this.store.getHookRules();
|
|
260
277
|
return rules?.[topic]?.[0];
|
|
261
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Register the time hook (sleep) inside the Leg1 transaction.
|
|
281
|
+
* Time hooks don't participate in the signal race — they're
|
|
282
|
+
* purely internal timeout registrations.
|
|
283
|
+
*/
|
|
284
|
+
async registerTimeHook(transaction) {
|
|
285
|
+
if (this.config.sleep) {
|
|
286
|
+
const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
|
|
287
|
+
if (!isNaN(duration) && Number(duration) > 0) {
|
|
288
|
+
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '', transaction);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Register the web hook signal AFTER the Leg1 transaction commits.
|
|
294
|
+
* This ensures the hook signal is never visible before Leg1
|
|
295
|
+
* completion, eliminating the FORBIDDEN window where Leg2 could
|
|
296
|
+
* find the hook but fail on the collation check.
|
|
297
|
+
*
|
|
298
|
+
* If a pending signal was stored by an early-arriving Leg2,
|
|
299
|
+
* setHookSignal atomically detects and returns it.
|
|
300
|
+
*/
|
|
301
|
+
async registerWebHookSignal() {
|
|
302
|
+
if (this.config.hook?.topic) {
|
|
303
|
+
const hookResult = await this.engine.taskService.registerWebHook(this.config.hook.topic, this.context, this.resolveDad(), this.context.metadata.expire);
|
|
304
|
+
if (hookResult.pending) {
|
|
305
|
+
return { pending: hookResult.pending };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* @deprecated Use registerTimeHook + registerWebHookSignal instead.
|
|
311
|
+
* Kept for backward compatibility with tests that monkey-patch this method.
|
|
312
|
+
*/
|
|
262
313
|
async registerHook(transaction) {
|
|
263
314
|
let jobId;
|
|
264
315
|
let pending;
|
|
265
316
|
if (this.config.hook?.topic) {
|
|
266
|
-
//hook signal is set standalone (not in the transaction) so the
|
|
267
|
-
//single CTE query can atomically detect a pending signal collision
|
|
268
317
|
const hookResult = await this.engine.taskService.registerWebHook(this.config.hook.topic, this.context, this.resolveDad(), this.context.metadata.expire);
|
|
269
318
|
jobId = hookResult.jobId;
|
|
270
319
|
pending = hookResult.pending;
|