@triggery/core 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +846 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
// src/check.ts
|
|
2
|
+
function createCheck(conditions) {
|
|
3
|
+
return {
|
|
4
|
+
is(key, predicate) {
|
|
5
|
+
const value = conditions[key];
|
|
6
|
+
if (value === void 0 || value === null) return false;
|
|
7
|
+
return predicate(value);
|
|
8
|
+
},
|
|
9
|
+
all(map) {
|
|
10
|
+
for (const key of Object.keys(map)) {
|
|
11
|
+
const predicate = map[key];
|
|
12
|
+
if (!predicate) continue;
|
|
13
|
+
const value = conditions[key];
|
|
14
|
+
if (value === void 0 || value === null) return false;
|
|
15
|
+
if (!predicate(value)) return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
any(map) {
|
|
20
|
+
for (const key of Object.keys(map)) {
|
|
21
|
+
const predicate = map[key];
|
|
22
|
+
if (!predicate) continue;
|
|
23
|
+
const value = conditions[key];
|
|
24
|
+
if (value === void 0 || value === null) continue;
|
|
25
|
+
if (predicate(value)) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/cascadeContext.ts
|
|
33
|
+
var current = null;
|
|
34
|
+
function getCurrentDispatch() {
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
function withDispatch(ctx, fn) {
|
|
38
|
+
const prev = current;
|
|
39
|
+
current = ctx;
|
|
40
|
+
try {
|
|
41
|
+
return fn();
|
|
42
|
+
} finally {
|
|
43
|
+
current = prev;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function chainHas(ctx, triggerId) {
|
|
47
|
+
let cur = ctx ?? null;
|
|
48
|
+
while (cur) {
|
|
49
|
+
if (cur.triggerId === triggerId) return true;
|
|
50
|
+
cur = cur.parent;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/dispatch.ts
|
|
56
|
+
var runIdCounter = 0;
|
|
57
|
+
var genRunId = () => `run_${(++runIdCounter).toString(36)}`;
|
|
58
|
+
function needsTiming(middleware) {
|
|
59
|
+
for (const mw of middleware) {
|
|
60
|
+
if (mw.onActionEnd || mw.onError || mw.onActionStart) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
var nowMs = () => Date.now();
|
|
65
|
+
function cancelAllTimers(trigger) {
|
|
66
|
+
for (const entry of trigger.timers.values()) {
|
|
67
|
+
if (entry.kind === "debounce" || entry.kind === "defer") {
|
|
68
|
+
clearTimeout(entry.tid);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
trigger.timers.clear();
|
|
72
|
+
}
|
|
73
|
+
function invokeAction(handler, ctx, middleware, trackTiming) {
|
|
74
|
+
const startedAt = trackTiming ? performance.now() : 0;
|
|
75
|
+
for (const mw of middleware) mw.onActionStart?.(ctx);
|
|
76
|
+
try {
|
|
77
|
+
const result = handler(ctx.payload);
|
|
78
|
+
if (result && typeof result.then === "function") {
|
|
79
|
+
result.then(
|
|
80
|
+
(value) => {
|
|
81
|
+
for (const mw of middleware) {
|
|
82
|
+
mw.onActionEnd?.({
|
|
83
|
+
...ctx,
|
|
84
|
+
durationMs: trackTiming ? performance.now() - startedAt : 0,
|
|
85
|
+
result: value
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
(error) => {
|
|
90
|
+
for (const mw of middleware) mw.onError?.({ ...ctx, error });
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
for (const mw of middleware) {
|
|
95
|
+
mw.onActionEnd?.({
|
|
96
|
+
...ctx,
|
|
97
|
+
durationMs: trackTiming ? performance.now() - startedAt : 0,
|
|
98
|
+
result
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
for (const mw of middleware) mw.onError?.({ ...ctx, error });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function executeTrigger(deps) {
|
|
107
|
+
const { trigger } = deps;
|
|
108
|
+
if (!trigger.enabled) return;
|
|
109
|
+
const concurrency = trigger.config.concurrency ?? "take-latest";
|
|
110
|
+
if (concurrency === "queue") {
|
|
111
|
+
trigger.queueTail = trigger.queueTail.then(() => runHandler(deps, concurrency)).catch(() => {
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
return runHandler(deps, concurrency);
|
|
116
|
+
}
|
|
117
|
+
var EMPTY_STRING_ARRAY = Object.freeze([]);
|
|
118
|
+
function runHandler(deps, concurrency) {
|
|
119
|
+
const {
|
|
120
|
+
trigger,
|
|
121
|
+
fireCtx,
|
|
122
|
+
inspector,
|
|
123
|
+
inspectorEnabled,
|
|
124
|
+
middleware,
|
|
125
|
+
hasMiddleware,
|
|
126
|
+
trackTiming,
|
|
127
|
+
runtimeId
|
|
128
|
+
} = deps;
|
|
129
|
+
if (!trigger.enabled) return;
|
|
130
|
+
if (hasMiddleware) {
|
|
131
|
+
const matchCtx = {
|
|
132
|
+
triggerId: trigger.config.id,
|
|
133
|
+
eventName: fireCtx.eventName,
|
|
134
|
+
payload: fireCtx.payload,
|
|
135
|
+
cascadeDepth: fireCtx.cascadeDepth
|
|
136
|
+
};
|
|
137
|
+
for (const mw of middleware) mw.onBeforeMatch?.(matchCtx);
|
|
138
|
+
}
|
|
139
|
+
if (concurrency === "take-first" || concurrency === "exhaust") {
|
|
140
|
+
if (trigger.inFlight.size > 0) {
|
|
141
|
+
const reason = `concurrency-${concurrency}`;
|
|
142
|
+
if (hasMiddleware) {
|
|
143
|
+
const skipCtx = {
|
|
144
|
+
triggerId: trigger.config.id,
|
|
145
|
+
eventName: fireCtx.eventName,
|
|
146
|
+
reason
|
|
147
|
+
};
|
|
148
|
+
for (const mw of middleware) mw.onSkip?.(skipCtx);
|
|
149
|
+
}
|
|
150
|
+
if (inspectorEnabled) {
|
|
151
|
+
inspector.record({
|
|
152
|
+
triggerId: trigger.config.id,
|
|
153
|
+
runId: genRunId(),
|
|
154
|
+
eventName: fireCtx.eventName,
|
|
155
|
+
status: "skipped",
|
|
156
|
+
reason,
|
|
157
|
+
durationMs: 0,
|
|
158
|
+
executedActions: EMPTY_STRING_ARRAY,
|
|
159
|
+
snapshotKeys: EMPTY_STRING_ARRAY
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (concurrency === "take-latest" && trigger.inFlight.size > 0) {
|
|
166
|
+
for (const prev of trigger.inFlight) prev.abort("superseded-by-latest");
|
|
167
|
+
trigger.inFlight.clear();
|
|
168
|
+
}
|
|
169
|
+
const runId = genRunId();
|
|
170
|
+
const needDuration = inspectorEnabled || trackTiming;
|
|
171
|
+
const startedAt = needDuration ? performance.now() : 0;
|
|
172
|
+
const required = trigger.config.required;
|
|
173
|
+
if (required.length > 0) {
|
|
174
|
+
for (let i = 0; i < required.length; i++) {
|
|
175
|
+
const requiredName = required[i];
|
|
176
|
+
if (!trigger.conditions.has(requiredName)) {
|
|
177
|
+
const reason = `missing-required-condition:${requiredName}`;
|
|
178
|
+
if (hasMiddleware) {
|
|
179
|
+
const skipCtx = {
|
|
180
|
+
triggerId: trigger.config.id,
|
|
181
|
+
eventName: fireCtx.eventName,
|
|
182
|
+
reason
|
|
183
|
+
};
|
|
184
|
+
for (const mw of middleware) mw.onSkip?.(skipCtx);
|
|
185
|
+
}
|
|
186
|
+
if (inspectorEnabled) {
|
|
187
|
+
inspector.record({
|
|
188
|
+
triggerId: trigger.config.id,
|
|
189
|
+
runId,
|
|
190
|
+
eventName: fireCtx.eventName,
|
|
191
|
+
status: "skipped",
|
|
192
|
+
reason,
|
|
193
|
+
durationMs: needDuration ? performance.now() - startedAt : 0,
|
|
194
|
+
executedActions: EMPTY_STRING_ARRAY,
|
|
195
|
+
snapshotKeys: EMPTY_STRING_ARRAY
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const executedActions = inspectorEnabled ? [] : EMPTY_STRING_ARRAY;
|
|
203
|
+
const snapshotKeys = inspectorEnabled ? [] : EMPTY_STRING_ARRAY;
|
|
204
|
+
const snapshotCache = /* @__PURE__ */ new Map();
|
|
205
|
+
const conditionsProxy = new Proxy({}, {
|
|
206
|
+
get(_target, prop) {
|
|
207
|
+
if (typeof prop !== "string") return void 0;
|
|
208
|
+
if (snapshotCache.has(prop)) return snapshotCache.get(prop);
|
|
209
|
+
const getter = trigger.conditions.get(prop);
|
|
210
|
+
if (!getter) {
|
|
211
|
+
snapshotCache.set(prop, void 0);
|
|
212
|
+
return void 0;
|
|
213
|
+
}
|
|
214
|
+
const value = getter();
|
|
215
|
+
snapshotCache.set(prop, value);
|
|
216
|
+
if (inspectorEnabled) snapshotKeys.push(prop);
|
|
217
|
+
return value;
|
|
218
|
+
},
|
|
219
|
+
has(_target, prop) {
|
|
220
|
+
return typeof prop === "string" && trigger.conditions.has(prop);
|
|
221
|
+
},
|
|
222
|
+
ownKeys() {
|
|
223
|
+
return Array.from(trigger.conditions.keys());
|
|
224
|
+
},
|
|
225
|
+
getOwnPropertyDescriptor() {
|
|
226
|
+
return { enumerable: true, configurable: true };
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
const callActionImmediate = (name, payload) => {
|
|
230
|
+
const handler = trigger.actions.get(name);
|
|
231
|
+
if (!handler) return;
|
|
232
|
+
if (inspectorEnabled) executedActions.push(name);
|
|
233
|
+
invokeAction(
|
|
234
|
+
handler,
|
|
235
|
+
{ triggerId: trigger.config.id, runId, actionName: name, payload },
|
|
236
|
+
middleware,
|
|
237
|
+
trackTiming
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
const callActionDeferred = (name, payload) => {
|
|
241
|
+
if (!trigger.enabled) return;
|
|
242
|
+
const handler = trigger.actions.get(name);
|
|
243
|
+
if (!handler) return;
|
|
244
|
+
invokeAction(
|
|
245
|
+
handler,
|
|
246
|
+
{ triggerId: trigger.config.id, runId, actionName: name, payload },
|
|
247
|
+
middleware,
|
|
248
|
+
trackTiming
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
const buildTimedProxy = (kind, ms) => new Proxy({}, {
|
|
252
|
+
get(_target, prop) {
|
|
253
|
+
if (typeof prop !== "string") return void 0;
|
|
254
|
+
if (!trigger.actions.has(prop)) return void 0;
|
|
255
|
+
return (payload) => {
|
|
256
|
+
if (kind === "debounce") {
|
|
257
|
+
const key = `debounce:${prop}:${ms}`;
|
|
258
|
+
const existing = trigger.timers.get(key);
|
|
259
|
+
if (existing?.kind === "debounce") clearTimeout(existing.tid);
|
|
260
|
+
const tid = setTimeout(() => {
|
|
261
|
+
trigger.timers.delete(key);
|
|
262
|
+
callActionDeferred(prop, payload);
|
|
263
|
+
}, ms);
|
|
264
|
+
trigger.timers.set(key, { kind: "debounce", tid });
|
|
265
|
+
} else if (kind === "throttle") {
|
|
266
|
+
const key = `throttle:${prop}:${ms}`;
|
|
267
|
+
const existing = trigger.timers.get(key);
|
|
268
|
+
const now = nowMs();
|
|
269
|
+
if (existing?.kind === "throttle" && now - existing.lastFiredAt < ms) return;
|
|
270
|
+
trigger.timers.set(key, { kind: "throttle", lastFiredAt: now });
|
|
271
|
+
callActionDeferred(prop, payload);
|
|
272
|
+
} else {
|
|
273
|
+
const key = `defer:${prop}:${++trigger.deferCounter}`;
|
|
274
|
+
const tid = setTimeout(() => {
|
|
275
|
+
trigger.timers.delete(key);
|
|
276
|
+
callActionDeferred(prop, payload);
|
|
277
|
+
}, ms);
|
|
278
|
+
trigger.timers.set(key, { kind: "defer", tid });
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
has(_target, prop) {
|
|
283
|
+
return typeof prop === "string" && trigger.actions.has(prop);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
const actionsProxy = new Proxy({}, {
|
|
287
|
+
get(_target, prop) {
|
|
288
|
+
if (prop === "debounce") return (ms) => buildTimedProxy("debounce", ms);
|
|
289
|
+
if (prop === "throttle") return (ms) => buildTimedProxy("throttle", ms);
|
|
290
|
+
if (prop === "defer") return (ms) => buildTimedProxy("defer", ms);
|
|
291
|
+
if (typeof prop !== "string") return void 0;
|
|
292
|
+
if (!trigger.actions.has(prop)) return void 0;
|
|
293
|
+
return (payload) => callActionImmediate(prop, payload);
|
|
294
|
+
},
|
|
295
|
+
has(_target, prop) {
|
|
296
|
+
return typeof prop === "string" && trigger.actions.has(prop);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const controller = new AbortController();
|
|
300
|
+
trigger.inFlight.add(controller);
|
|
301
|
+
const handlerCtx = {
|
|
302
|
+
event: { name: fireCtx.eventName, payload: fireCtx.payload },
|
|
303
|
+
conditions: conditionsProxy,
|
|
304
|
+
actions: actionsProxy,
|
|
305
|
+
check: createCheck(conditionsProxy),
|
|
306
|
+
meta: {
|
|
307
|
+
runId,
|
|
308
|
+
triggerId: trigger.config.id,
|
|
309
|
+
scheduledAt: startedAt,
|
|
310
|
+
cascadeDepth: fireCtx.cascadeDepth,
|
|
311
|
+
...fireCtx.parentRunId !== void 0 && { parentRunId: fireCtx.parentRunId },
|
|
312
|
+
...fireCtx.parentTriggerId !== void 0 && { parentTriggerId: fireCtx.parentTriggerId }
|
|
313
|
+
},
|
|
314
|
+
signal: controller.signal
|
|
315
|
+
};
|
|
316
|
+
const finalize = (status, reason) => {
|
|
317
|
+
trigger.inFlight.delete(controller);
|
|
318
|
+
if (!inspectorEnabled) return;
|
|
319
|
+
const snapshot = {
|
|
320
|
+
triggerId: trigger.config.id,
|
|
321
|
+
runId,
|
|
322
|
+
eventName: fireCtx.eventName,
|
|
323
|
+
status,
|
|
324
|
+
durationMs: needDuration ? performance.now() - startedAt : 0,
|
|
325
|
+
executedActions,
|
|
326
|
+
snapshotKeys
|
|
327
|
+
};
|
|
328
|
+
if (reason !== void 0) snapshot.reason = reason;
|
|
329
|
+
inspector.record(snapshot);
|
|
330
|
+
};
|
|
331
|
+
try {
|
|
332
|
+
const result = withDispatch(
|
|
333
|
+
{
|
|
334
|
+
runtimeId,
|
|
335
|
+
triggerId: trigger.config.id,
|
|
336
|
+
runId,
|
|
337
|
+
cascadeDepth: fireCtx.cascadeDepth,
|
|
338
|
+
parent: fireCtx.parentContext ?? null
|
|
339
|
+
},
|
|
340
|
+
() => trigger.config.handler(handlerCtx)
|
|
341
|
+
);
|
|
342
|
+
if (result && typeof result.then === "function") {
|
|
343
|
+
return result.then(
|
|
344
|
+
() => finalize("fired"),
|
|
345
|
+
(error) => {
|
|
346
|
+
if (controller.signal.aborted) {
|
|
347
|
+
finalize("aborted", String(controller.signal.reason ?? "aborted"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.error(`[triggery] handler "${trigger.config.id}" failed:`, error);
|
|
351
|
+
finalize("errored", String(error));
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
finalize("fired");
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error(`[triggery] handler "${trigger.config.id}" failed:`, error);
|
|
358
|
+
finalize("errored", String(error));
|
|
359
|
+
}
|
|
360
|
+
return void 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/inspector.ts
|
|
364
|
+
var EMPTY_BUFFER = Object.freeze(
|
|
365
|
+
[]
|
|
366
|
+
);
|
|
367
|
+
var NOOP_UNSUBSCRIBE = () => {
|
|
368
|
+
};
|
|
369
|
+
var NOOP_INSPECTOR = {
|
|
370
|
+
record() {
|
|
371
|
+
},
|
|
372
|
+
getBuffer() {
|
|
373
|
+
return EMPTY_BUFFER;
|
|
374
|
+
},
|
|
375
|
+
getLastForTrigger() {
|
|
376
|
+
return void 0;
|
|
377
|
+
},
|
|
378
|
+
subscribe() {
|
|
379
|
+
return NOOP_UNSUBSCRIBE;
|
|
380
|
+
},
|
|
381
|
+
clear() {
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
function createNoopInspector() {
|
|
385
|
+
return NOOP_INSPECTOR;
|
|
386
|
+
}
|
|
387
|
+
function createInspector(bufferSize) {
|
|
388
|
+
const size = Math.max(1, bufferSize);
|
|
389
|
+
const slots = new Array(size);
|
|
390
|
+
let head = 0;
|
|
391
|
+
let count = 0;
|
|
392
|
+
const lastByTrigger = /* @__PURE__ */ new Map();
|
|
393
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
394
|
+
return {
|
|
395
|
+
record(snapshot) {
|
|
396
|
+
slots[head] = snapshot;
|
|
397
|
+
head = head + 1 === size ? 0 : head + 1;
|
|
398
|
+
if (count < size) count++;
|
|
399
|
+
lastByTrigger.set(snapshot.triggerId, snapshot);
|
|
400
|
+
if (listeners.size > 0) {
|
|
401
|
+
for (const listener of listeners) {
|
|
402
|
+
try {
|
|
403
|
+
listener(snapshot);
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
getBuffer() {
|
|
410
|
+
if (count === 0) return [];
|
|
411
|
+
const out = new Array(count);
|
|
412
|
+
let idx = head === 0 ? size - 1 : head - 1;
|
|
413
|
+
for (let i = 0; i < count; i++) {
|
|
414
|
+
out[i] = slots[idx];
|
|
415
|
+
idx = idx === 0 ? size - 1 : idx - 1;
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
},
|
|
419
|
+
getLastForTrigger(triggerId) {
|
|
420
|
+
return lastByTrigger.get(triggerId);
|
|
421
|
+
},
|
|
422
|
+
subscribe(listener) {
|
|
423
|
+
listeners.add(listener);
|
|
424
|
+
return () => listeners.delete(listener);
|
|
425
|
+
},
|
|
426
|
+
clear() {
|
|
427
|
+
for (let i = 0; i < size; i++) slots[i] = void 0;
|
|
428
|
+
head = 0;
|
|
429
|
+
count = 0;
|
|
430
|
+
lastByTrigger.clear();
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/scheduler.ts
|
|
436
|
+
function createMicrotaskScheduler() {
|
|
437
|
+
let queue = [];
|
|
438
|
+
let scheduled = false;
|
|
439
|
+
const flush = () => {
|
|
440
|
+
scheduled = false;
|
|
441
|
+
if (queue.length === 0) return;
|
|
442
|
+
const tasks = queue;
|
|
443
|
+
queue = [];
|
|
444
|
+
for (const task of tasks) {
|
|
445
|
+
try {
|
|
446
|
+
task();
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error("[triggery] scheduler task failed:", error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
enqueue(task) {
|
|
454
|
+
queue.push(task);
|
|
455
|
+
if (!scheduled) {
|
|
456
|
+
scheduled = true;
|
|
457
|
+
queueMicrotask(flush);
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
flush
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function createSyncScheduler() {
|
|
464
|
+
return {
|
|
465
|
+
enqueue(task) {
|
|
466
|
+
task();
|
|
467
|
+
},
|
|
468
|
+
flush() {
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function createScheduler(strategy) {
|
|
473
|
+
return strategy === "sync" ? createSyncScheduler() : createMicrotaskScheduler();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/runtime.ts
|
|
477
|
+
var runtimeIdCounter = 0;
|
|
478
|
+
var genRuntimeId = () => `runtime_${(++runtimeIdCounter).toString(36)}`;
|
|
479
|
+
var isDev = () => {
|
|
480
|
+
const env = globalThis.process?.env?.NODE_ENV;
|
|
481
|
+
return env !== "production";
|
|
482
|
+
};
|
|
483
|
+
function resolveInspectorEnabled(option) {
|
|
484
|
+
if (option === true) return true;
|
|
485
|
+
if (option === false) return false;
|
|
486
|
+
const dev = isDev();
|
|
487
|
+
if (option === void 0) return dev;
|
|
488
|
+
if (dev) return option.dev ?? true;
|
|
489
|
+
return option.prod ?? false;
|
|
490
|
+
}
|
|
491
|
+
function createRuntime(options = {}) {
|
|
492
|
+
const id = genRuntimeId();
|
|
493
|
+
const triggers = /* @__PURE__ */ new Map();
|
|
494
|
+
const eventIndex = /* @__PURE__ */ new Map();
|
|
495
|
+
const middleware = options.middleware ?? [];
|
|
496
|
+
const hasMiddleware = middleware.length > 0;
|
|
497
|
+
const trackTiming = needsTiming(middleware);
|
|
498
|
+
const maxCascadeDepth = options.maxCascadeDepth ?? 3;
|
|
499
|
+
const inspectorEnabled = resolveInspectorEnabled(options.inspector);
|
|
500
|
+
const inspector = inspectorEnabled ? createInspector(options.inspectorBufferSize ?? 50) : createNoopInspector();
|
|
501
|
+
const microtaskScheduler = createScheduler("microtask");
|
|
502
|
+
const syncScheduler = createScheduler("sync");
|
|
503
|
+
const warnedCollisions = /* @__PURE__ */ new Set();
|
|
504
|
+
const indexEvent = (eventName, trigger) => {
|
|
505
|
+
let set = eventIndex.get(eventName);
|
|
506
|
+
if (!set) {
|
|
507
|
+
set = /* @__PURE__ */ new Set();
|
|
508
|
+
eventIndex.set(eventName, set);
|
|
509
|
+
}
|
|
510
|
+
set.add(trigger);
|
|
511
|
+
};
|
|
512
|
+
const deindexEvent = (eventName, trigger) => {
|
|
513
|
+
const set = eventIndex.get(eventName);
|
|
514
|
+
if (!set) return;
|
|
515
|
+
set.delete(trigger);
|
|
516
|
+
if (set.size === 0) eventIndex.delete(eventName);
|
|
517
|
+
};
|
|
518
|
+
const registerTrigger = (config) => {
|
|
519
|
+
if (triggers.has(config.id)) {
|
|
520
|
+
const existing = triggers.get(config.id);
|
|
521
|
+
if (existing) {
|
|
522
|
+
for (const eventName of existing.config.events) deindexEvent(eventName, existing);
|
|
523
|
+
cancelAllTimers(existing);
|
|
524
|
+
for (const prev of existing.inFlight) prev.abort("trigger-replaced");
|
|
525
|
+
existing.inFlight.clear();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const registered = {
|
|
529
|
+
config,
|
|
530
|
+
conditions: /* @__PURE__ */ new Map(),
|
|
531
|
+
actions: /* @__PURE__ */ new Map(),
|
|
532
|
+
conditionStacks: /* @__PURE__ */ new Map(),
|
|
533
|
+
actionStacks: /* @__PURE__ */ new Map(),
|
|
534
|
+
enabled: true,
|
|
535
|
+
inFlight: /* @__PURE__ */ new Set(),
|
|
536
|
+
queueTail: Promise.resolve(),
|
|
537
|
+
timers: /* @__PURE__ */ new Map(),
|
|
538
|
+
deferCounter: 0
|
|
539
|
+
};
|
|
540
|
+
triggers.set(config.id, registered);
|
|
541
|
+
for (const eventName of config.events) indexEvent(eventName, registered);
|
|
542
|
+
let unregistered = false;
|
|
543
|
+
return {
|
|
544
|
+
unregister() {
|
|
545
|
+
if (unregistered) return;
|
|
546
|
+
unregistered = true;
|
|
547
|
+
const current2 = triggers.get(config.id);
|
|
548
|
+
if (current2 !== registered) return;
|
|
549
|
+
for (const eventName of registered.config.events) deindexEvent(eventName, registered);
|
|
550
|
+
cancelAllTimers(registered);
|
|
551
|
+
for (const ctl of registered.inFlight) ctl.abort("trigger-disposed");
|
|
552
|
+
registered.inFlight.clear();
|
|
553
|
+
triggers.delete(config.id);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
const registerStacked = (triggerId, name, fn, label, scope) => {
|
|
558
|
+
const trigger = triggers.get(triggerId);
|
|
559
|
+
if (!trigger) {
|
|
560
|
+
if (isDev()) {
|
|
561
|
+
console.warn(
|
|
562
|
+
`[triggery] register${label === "condition" ? "Condition" : "Action"}: trigger "${triggerId}" not found`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return { unregister() {
|
|
566
|
+
} };
|
|
567
|
+
}
|
|
568
|
+
if (trigger.config.scope !== scope) {
|
|
569
|
+
if (isDev()) {
|
|
570
|
+
const collisionKey = `scope-mismatch:${label}:${triggerId}:${scope}:${name}`;
|
|
571
|
+
if (!warnedCollisions.has(collisionKey)) {
|
|
572
|
+
warnedCollisions.add(collisionKey);
|
|
573
|
+
console.warn(
|
|
574
|
+
`[triggery] register${label === "condition" ? "Condition" : "Action"}: scope mismatch \u2014 trigger "${triggerId}" has scope "${trigger.config.scope || "(global)"}" but the registration came from scope "${scope || "(global)"}". The registration is ignored.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return { unregister() {
|
|
579
|
+
} };
|
|
580
|
+
}
|
|
581
|
+
const stacks = label === "condition" ? trigger.conditionStacks : trigger.actionStacks;
|
|
582
|
+
const mirror = label === "condition" ? trigger.conditions : trigger.actions;
|
|
583
|
+
let stack = stacks.get(name);
|
|
584
|
+
if (!stack) {
|
|
585
|
+
stack = [];
|
|
586
|
+
stacks.set(name, stack);
|
|
587
|
+
}
|
|
588
|
+
if (isDev() && stack.length > 0) {
|
|
589
|
+
const collisionKey = `${label}:${triggerId}:${name}`;
|
|
590
|
+
if (!warnedCollisions.has(collisionKey)) {
|
|
591
|
+
warnedCollisions.add(collisionKey);
|
|
592
|
+
console.warn(
|
|
593
|
+
`[triggery] multiple ${label} registrations for "${name}" on trigger "${triggerId}" \u2014 last-mount-wins. To compose values from several sources, register through a single hook.`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
stack.push(fn);
|
|
598
|
+
mirror.set(name, fn);
|
|
599
|
+
let unregistered = false;
|
|
600
|
+
return {
|
|
601
|
+
unregister() {
|
|
602
|
+
if (unregistered) return;
|
|
603
|
+
unregistered = true;
|
|
604
|
+
const t = triggers.get(triggerId);
|
|
605
|
+
if (!t) return;
|
|
606
|
+
const liveStacks = label === "condition" ? t.conditionStacks : t.actionStacks;
|
|
607
|
+
const liveMirror = label === "condition" ? t.conditions : t.actions;
|
|
608
|
+
const liveStack = liveStacks.get(name);
|
|
609
|
+
if (!liveStack) return;
|
|
610
|
+
for (let i = liveStack.length - 1; i >= 0; i--) {
|
|
611
|
+
if (liveStack[i] === fn) {
|
|
612
|
+
liveStack.splice(i, 1);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (liveStack.length === 0) {
|
|
617
|
+
liveStacks.delete(name);
|
|
618
|
+
liveMirror.delete(name);
|
|
619
|
+
} else {
|
|
620
|
+
liveMirror.set(name, liveStack[liveStack.length - 1]);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
};
|
|
625
|
+
const registerCondition = (triggerId, name, getter, options2) => registerStacked(triggerId, name, getter, "condition", options2?.scope ?? "");
|
|
626
|
+
const registerAction = (triggerId, name, handler, options2) => registerStacked(triggerId, name, handler, "action", options2?.scope ?? "");
|
|
627
|
+
const emitCascade = (info) => {
|
|
628
|
+
for (const mw of middleware) mw.onCascade?.(info);
|
|
629
|
+
};
|
|
630
|
+
const buildFireContext = (eventName, payload) => {
|
|
631
|
+
const parent = getCurrentDispatch();
|
|
632
|
+
if (parent && parent.runtimeId === id) {
|
|
633
|
+
return {
|
|
634
|
+
eventName,
|
|
635
|
+
payload,
|
|
636
|
+
cascadeDepth: parent.cascadeDepth + 1,
|
|
637
|
+
parentRunId: parent.runId,
|
|
638
|
+
parentTriggerId: parent.triggerId,
|
|
639
|
+
parentContext: parent
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return { eventName, payload, cascadeDepth: 0 };
|
|
643
|
+
};
|
|
644
|
+
const dispatch = (fireCtx, opts) => {
|
|
645
|
+
if (hasMiddleware) {
|
|
646
|
+
for (const mw of middleware) {
|
|
647
|
+
const result = mw.onFire?.(fireCtx);
|
|
648
|
+
if (result?.cancel) return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (fireCtx.cascadeDepth > maxCascadeDepth) {
|
|
652
|
+
emitCascade({
|
|
653
|
+
parentTriggerId: fireCtx.parentTriggerId ?? "",
|
|
654
|
+
parentRunId: fireCtx.parentRunId ?? "",
|
|
655
|
+
newEventName: fireCtx.eventName,
|
|
656
|
+
cascadeDepth: fireCtx.cascadeDepth,
|
|
657
|
+
kind: "overflow"
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const set = eventIndex.get(fireCtx.eventName);
|
|
662
|
+
if (!set || set.size === 0) return;
|
|
663
|
+
const triggersForEvent = set.size === 1 ? [set.values().next().value] : Array.from(set);
|
|
664
|
+
const parentCtx = fireCtx.parentContext;
|
|
665
|
+
for (const trigger of triggersForEvent) {
|
|
666
|
+
if (!trigger.enabled) continue;
|
|
667
|
+
if (parentCtx !== void 0 && chainHas(parentCtx, trigger.config.id)) {
|
|
668
|
+
emitCascade({
|
|
669
|
+
parentTriggerId: fireCtx.parentTriggerId ?? "",
|
|
670
|
+
parentRunId: fireCtx.parentRunId ?? "",
|
|
671
|
+
newEventName: fireCtx.eventName,
|
|
672
|
+
cascadeDepth: fireCtx.cascadeDepth,
|
|
673
|
+
kind: "cycle"
|
|
674
|
+
});
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const run = () => {
|
|
678
|
+
executeTrigger({
|
|
679
|
+
trigger,
|
|
680
|
+
fireCtx,
|
|
681
|
+
inspector,
|
|
682
|
+
inspectorEnabled,
|
|
683
|
+
middleware,
|
|
684
|
+
hasMiddleware,
|
|
685
|
+
trackTiming,
|
|
686
|
+
runtimeId: id
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
if (opts.forceSync) {
|
|
690
|
+
run();
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const scheduler = trigger.config.schedule === "sync" ? syncScheduler : microtaskScheduler;
|
|
694
|
+
scheduler.enqueue(run);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const fire = (eventName, payload) => {
|
|
698
|
+
dispatch(buildFireContext(eventName, payload), { forceSync: false });
|
|
699
|
+
};
|
|
700
|
+
const fireSync = (eventName, payload) => {
|
|
701
|
+
dispatch(buildFireContext(eventName, payload), { forceSync: true });
|
|
702
|
+
};
|
|
703
|
+
const subscribe = (listener) => {
|
|
704
|
+
const off = inspector.subscribe(listener);
|
|
705
|
+
let unregistered = false;
|
|
706
|
+
return {
|
|
707
|
+
unregister() {
|
|
708
|
+
if (unregistered) return;
|
|
709
|
+
unregistered = true;
|
|
710
|
+
off();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
};
|
|
714
|
+
const getInspectorBuffer = () => inspector.getBuffer();
|
|
715
|
+
const getTrigger = (triggerId) => {
|
|
716
|
+
const trigger = triggers.get(triggerId);
|
|
717
|
+
if (!trigger) return void 0;
|
|
718
|
+
return buildPublicTrigger(trigger, inspector);
|
|
719
|
+
};
|
|
720
|
+
const graph = () => {
|
|
721
|
+
const nodes = [];
|
|
722
|
+
for (const t of triggers.values()) {
|
|
723
|
+
nodes.push({
|
|
724
|
+
id: t.config.id,
|
|
725
|
+
scope: t.config.scope,
|
|
726
|
+
events: t.config.events,
|
|
727
|
+
required: t.config.required,
|
|
728
|
+
schedule: t.config.schedule,
|
|
729
|
+
concurrency: t.config.concurrency,
|
|
730
|
+
enabled: t.enabled
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const idx = {};
|
|
734
|
+
for (const [eventName, triggerSet] of eventIndex) {
|
|
735
|
+
idx[eventName] = Array.from(triggerSet).map((t) => t.config.id);
|
|
736
|
+
}
|
|
737
|
+
return { triggers: nodes, eventIndex: idx };
|
|
738
|
+
};
|
|
739
|
+
const dispose = () => {
|
|
740
|
+
for (const trigger of triggers.values()) {
|
|
741
|
+
for (const ctl of trigger.inFlight) ctl.abort("runtime-disposed");
|
|
742
|
+
trigger.inFlight.clear();
|
|
743
|
+
cancelAllTimers(trigger);
|
|
744
|
+
trigger.enabled = false;
|
|
745
|
+
}
|
|
746
|
+
triggers.clear();
|
|
747
|
+
eventIndex.clear();
|
|
748
|
+
inspector.clear();
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
id,
|
|
752
|
+
inspectorEnabled,
|
|
753
|
+
registerTrigger,
|
|
754
|
+
registerCondition,
|
|
755
|
+
registerAction,
|
|
756
|
+
fire,
|
|
757
|
+
fireSync,
|
|
758
|
+
subscribe,
|
|
759
|
+
getInspectorBuffer,
|
|
760
|
+
getTrigger,
|
|
761
|
+
graph,
|
|
762
|
+
dispose
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function buildPublicTrigger(internal, inspector) {
|
|
766
|
+
return {
|
|
767
|
+
id: internal.config.id,
|
|
768
|
+
schedule: internal.config.schedule,
|
|
769
|
+
enable() {
|
|
770
|
+
internal.enabled = true;
|
|
771
|
+
},
|
|
772
|
+
disable() {
|
|
773
|
+
internal.enabled = false;
|
|
774
|
+
},
|
|
775
|
+
isEnabled() {
|
|
776
|
+
return internal.enabled;
|
|
777
|
+
},
|
|
778
|
+
namedHooks() {
|
|
779
|
+
throw new Error("[triggery] namedHooks() can only be called on the original trigger object");
|
|
780
|
+
},
|
|
781
|
+
inspect() {
|
|
782
|
+
return inspector.getLastForTrigger(internal.config.id);
|
|
783
|
+
},
|
|
784
|
+
dispose() {
|
|
785
|
+
internal.enabled = false;
|
|
786
|
+
for (const ctl of internal.inFlight) ctl.abort("disposed");
|
|
787
|
+
internal.inFlight.clear();
|
|
788
|
+
cancelAllTimers(internal);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
var _defaultRuntime;
|
|
793
|
+
function getDefaultRuntime() {
|
|
794
|
+
if (!_defaultRuntime) _defaultRuntime = createRuntime();
|
|
795
|
+
return _defaultRuntime;
|
|
796
|
+
}
|
|
797
|
+
function setDefaultRuntime(runtime) {
|
|
798
|
+
_defaultRuntime = runtime;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/createTrigger.ts
|
|
802
|
+
function createTrigger(config, runtime = getDefaultRuntime()) {
|
|
803
|
+
const internalHandler = (ctx) => config.handler(ctx);
|
|
804
|
+
const internalConfig = {
|
|
805
|
+
id: config.id,
|
|
806
|
+
schedule: config.schedule ?? "microtask",
|
|
807
|
+
concurrency: config.concurrency ?? "take-latest",
|
|
808
|
+
required: config.required ?? [],
|
|
809
|
+
events: config.events,
|
|
810
|
+
scope: config.scope ?? "",
|
|
811
|
+
handler: internalHandler
|
|
812
|
+
};
|
|
813
|
+
const token = runtime.registerTrigger(internalConfig);
|
|
814
|
+
return {
|
|
815
|
+
id: config.id,
|
|
816
|
+
schedule: internalConfig.schedule,
|
|
817
|
+
enable() {
|
|
818
|
+
runtime.getTrigger(config.id)?.enable();
|
|
819
|
+
},
|
|
820
|
+
disable() {
|
|
821
|
+
runtime.getTrigger(config.id)?.disable();
|
|
822
|
+
},
|
|
823
|
+
isEnabled() {
|
|
824
|
+
return runtime.getTrigger(config.id)?.isEnabled() ?? false;
|
|
825
|
+
},
|
|
826
|
+
inspect() {
|
|
827
|
+
return runtime.getTrigger(config.id)?.inspect();
|
|
828
|
+
},
|
|
829
|
+
dispose() {
|
|
830
|
+
token.unregister();
|
|
831
|
+
},
|
|
832
|
+
namedHooks() {
|
|
833
|
+
return new Proxy({}, {
|
|
834
|
+
get(_target, prop) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
`[triggery] namedHooks().${String(prop)} requires @triggery/react. Use createNamedHooks(trigger) from '@triggery/react' instead.`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export { createCheck, createInspector, createRuntime, createScheduler, createTrigger, getDefaultRuntime, setDefaultRuntime };
|
|
845
|
+
//# sourceMappingURL=index.js.map
|
|
846
|
+
//# sourceMappingURL=index.js.map
|