hazo_notify 5.0.0 → 5.2.1
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/README.md +5 -5
- package/dist/components/notification_bell/index.d.ts +11 -1
- package/dist/components/notification_bell/index.d.ts.map +1 -1
- package/dist/components/notification_bell/index.js +33 -1
- package/dist/components/notification_bell/index.js.map +1 -1
- package/dist/lib/adapters/email/adapter.d.ts +2 -0
- package/dist/lib/adapters/email/adapter.d.ts.map +1 -1
- package/dist/lib/adapters/email/adapter.js +53 -22
- package/dist/lib/adapters/email/adapter.js.map +1 -1
- package/dist/lib/adapters/webhook/adapter.d.ts +10 -0
- package/dist/lib/adapters/webhook/adapter.d.ts.map +1 -0
- package/dist/lib/adapters/webhook/adapter.js +90 -0
- package/dist/lib/adapters/webhook/adapter.js.map +1 -0
- package/dist/lib/adapters/webhook/index.d.ts +8 -0
- package/dist/lib/adapters/webhook/index.d.ts.map +1 -0
- package/dist/lib/adapters/webhook/index.js +3 -0
- package/dist/lib/adapters/webhook/index.js.map +1 -0
- package/dist/lib/adapters/webhook/types.d.ts +45 -0
- package/dist/lib/adapters/webhook/types.d.ts.map +1 -0
- package/dist/lib/adapters/webhook/types.js +12 -0
- package/dist/lib/adapters/webhook/types.js.map +1 -0
- package/dist/lib/api/inbox.d.ts +32 -0
- package/dist/lib/api/inbox.d.ts.map +1 -1
- package/dist/lib/api/inbox.js +68 -0
- package/dist/lib/api/inbox.js.map +1 -1
- package/dist/lib/dispatcher/index.d.ts +2 -2
- package/dist/lib/dispatcher/index.d.ts.map +1 -1
- package/dist/lib/dispatcher/index.js +28 -8
- package/dist/lib/dispatcher/index.js.map +1 -1
- package/dist/lib/inbox/broadcaster.d.ts +44 -0
- package/dist/lib/inbox/broadcaster.d.ts.map +1 -0
- package/dist/lib/inbox/broadcaster.js +62 -0
- package/dist/lib/inbox/broadcaster.js.map +1 -0
- package/dist/lib/inbox/index.d.ts +2 -0
- package/dist/lib/inbox/index.d.ts.map +1 -1
- package/dist/lib/inbox/index.js +1 -0
- package/dist/lib/inbox/index.js.map +1 -1
- package/dist/lib/inbox/storage.d.ts +11 -32
- package/dist/lib/inbox/storage.d.ts.map +1 -1
- package/dist/lib/inbox/storage.js +22 -38
- package/dist/lib/inbox/storage.js.map +1 -1
- package/dist/lib/inbox/types.d.ts +7 -0
- package/dist/lib/inbox/types.d.ts.map +1 -1
- package/dist/lib/jobs/submit.d.ts.map +1 -1
- package/dist/lib/jobs/submit.js +1 -0
- package/dist/lib/jobs/submit.js.map +1 -1
- package/dist/lib/jobs/types.d.ts +1 -0
- package/dist/lib/jobs/types.d.ts.map +1 -1
- package/dist/lib/lifecycle/default_templates.d.ts +9 -0
- package/dist/lib/lifecycle/default_templates.d.ts.map +1 -0
- package/dist/lib/lifecycle/default_templates.js +160 -0
- package/dist/lib/lifecycle/default_templates.js.map +1 -0
- package/dist/lib/lifecycle/dispatch.d.ts +37 -0
- package/dist/lib/lifecycle/dispatch.d.ts.map +1 -0
- package/dist/lib/lifecycle/dispatch.js +32 -0
- package/dist/lib/lifecycle/dispatch.js.map +1 -0
- package/dist/lib/lifecycle/events.d.ts +34 -0
- package/dist/lib/lifecycle/events.d.ts.map +1 -0
- package/dist/lib/lifecycle/events.js +118 -0
- package/dist/lib/lifecycle/events.js.map +1 -0
- package/dist/lib/lifecycle/handler.d.ts +29 -0
- package/dist/lib/lifecycle/handler.d.ts.map +1 -0
- package/dist/lib/lifecycle/handler.js +145 -0
- package/dist/lib/lifecycle/handler.js.map +1 -0
- package/dist/lib/lifecycle/index.d.ts +23 -0
- package/dist/lib/lifecycle/index.d.ts.map +1 -0
- package/dist/lib/lifecycle/index.js +21 -0
- package/dist/lib/lifecycle/index.js.map +1 -0
- package/dist/lib/lifecycle/register.d.ts +29 -0
- package/dist/lib/lifecycle/register.d.ts.map +1 -0
- package/dist/lib/lifecycle/register.js +29 -0
- package/dist/lib/lifecycle/register.js.map +1 -0
- package/dist/lib/lifecycle/resolver.d.ts +22 -0
- package/dist/lib/lifecycle/resolver.d.ts.map +1 -0
- package/dist/lib/lifecycle/resolver.js +105 -0
- package/dist/lib/lifecycle/resolver.js.map +1 -0
- package/dist/lib/lifecycle/scheduler.d.ts +27 -0
- package/dist/lib/lifecycle/scheduler.d.ts.map +1 -0
- package/dist/lib/lifecycle/scheduler.js +108 -0
- package/dist/lib/lifecycle/scheduler.js.map +1 -0
- package/dist/lib/lifecycle/status.d.ts +39 -0
- package/dist/lib/lifecycle/status.d.ts.map +1 -0
- package/dist/lib/lifecycle/status.js +67 -0
- package/dist/lib/lifecycle/status.js.map +1 -0
- package/dist/lib/lifecycle/types.d.ts +165 -0
- package/dist/lib/lifecycle/types.d.ts.map +1 -0
- package/dist/lib/lifecycle/types.js +11 -0
- package/dist/lib/lifecycle/types.js.map +1 -0
- package/dist/lib/preferences/index.d.ts +3 -0
- package/dist/lib/preferences/index.d.ts.map +1 -0
- package/dist/lib/preferences/index.js +3 -0
- package/dist/lib/preferences/index.js.map +1 -0
- package/dist/lib/preferences/storage.d.ts +33 -0
- package/dist/lib/preferences/storage.d.ts.map +1 -0
- package/dist/lib/preferences/storage.js +125 -0
- package/dist/lib/preferences/storage.js.map +1 -0
- package/dist/lib/preferences/types.d.ts +59 -0
- package/dist/lib/preferences/types.d.ts.map +1 -0
- package/dist/lib/preferences/types.js +20 -0
- package/dist/lib/preferences/types.js.map +1 -0
- package/dist/lib/template_manager/db/template_repository.d.ts +4 -1
- package/dist/lib/template_manager/db/template_repository.d.ts.map +1 -1
- package/dist/lib/template_manager/db/template_repository.js +17 -1
- package/dist/lib/template_manager/db/template_repository.js.map +1 -1
- package/dist/lib/template_manager/types.d.ts +1 -0
- package/dist/lib/template_manager/types.d.ts.map +1 -1
- package/migrations/008_lifecycle_status.pg.sql +27 -0
- package/migrations/008_lifecycle_status.sqlite.sql +24 -0
- package/migrations/009_templates_locale.pg.sql +6 -0
- package/migrations/009_templates_locale.sqlite.sql +4 -0
- package/migrations/010_preferences.pg.sql +26 -0
- package/migrations/010_preferences.sqlite.sql +32 -0
- package/package.json +17 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAsB7C,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `registerLifecycleHandlers` — wires the lifecycle step handler into a
|
|
3
|
+
* hazo_jobs scheduler instance.
|
|
4
|
+
*
|
|
5
|
+
* Call this once at worker startup, before the scheduler begins consuming jobs.
|
|
6
|
+
* Re-registering the same type is idempotent (hazo_jobs replaces the handler).
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createLifecycleScheduler, registerLifecycleHandlers } from 'hazo_notify/lifecycle';
|
|
11
|
+
*
|
|
12
|
+
* const lifecycle = createLifecycleScheduler(opts, getUserEmail);
|
|
13
|
+
* registerLifecycleHandlers(lifecycle, jobsScheduler);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
import type { LifecycleScheduler, HazoJobHandler, LifecycleStepPayload } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Register the lifecycle step handler with a hazo_jobs-compatible scheduler.
|
|
21
|
+
*
|
|
22
|
+
* @param lifecycle - The `LifecycleScheduler` returned by `createLifecycleScheduler`.
|
|
23
|
+
* @param jobsScheduler - Any object with a `register(type, handler)` method
|
|
24
|
+
* (structural match for hazo_jobs `Scheduler`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerLifecycleHandlers(lifecycle: LifecycleScheduler, jobsScheduler: {
|
|
27
|
+
register(type: string, handler: HazoJobHandler<LifecycleStepPayload>): void;
|
|
28
|
+
}): void;
|
|
29
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE3F;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,kBAAkB,EAC7B,aAAa,EAAE;IACb,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,CAAC,oBAAoB,CAAC,GAC5C,IAAI,CAAC;CACT,GACA,IAAI,CAKN"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `registerLifecycleHandlers` — wires the lifecycle step handler into a
|
|
3
|
+
* hazo_jobs scheduler instance.
|
|
4
|
+
*
|
|
5
|
+
* Call this once at worker startup, before the scheduler begins consuming jobs.
|
|
6
|
+
* Re-registering the same type is idempotent (hazo_jobs replaces the handler).
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createLifecycleScheduler, registerLifecycleHandlers } from 'hazo_notify/lifecycle';
|
|
11
|
+
*
|
|
12
|
+
* const lifecycle = createLifecycleScheduler(opts, getUserEmail);
|
|
13
|
+
* registerLifecycleHandlers(lifecycle, jobsScheduler);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
import { LIFECYCLE_STEP_JOB_TYPE } from './scheduler.js';
|
|
19
|
+
/**
|
|
20
|
+
* Register the lifecycle step handler with a hazo_jobs-compatible scheduler.
|
|
21
|
+
*
|
|
22
|
+
* @param lifecycle - The `LifecycleScheduler` returned by `createLifecycleScheduler`.
|
|
23
|
+
* @param jobsScheduler - Any object with a `register(type, handler)` method
|
|
24
|
+
* (structural match for hazo_jobs `Scheduler`).
|
|
25
|
+
*/
|
|
26
|
+
export function registerLifecycleHandlers(lifecycle, jobsScheduler) {
|
|
27
|
+
jobsScheduler.register(LIFECYCLE_STEP_JOB_TYPE, lifecycle.__jobHandler);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAGzD;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CACvC,SAA6B,EAC7B,aAKC;IAED,aAAa,CAAC,QAAQ,CACpB,uBAAuB,EACvB,SAAS,CAAC,YAAY,CACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condition evaluation engine for lifecycle steps.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates a `ConditionNode` graph against the named resolvers supplied at
|
|
5
|
+
* scheduler construction time. The top-level `evaluateConditions` function
|
|
6
|
+
* treats the array as an implicit AND — all nodes must pass.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import type { ConditionNode, LifecycleSchedulerOptions } from './types.js';
|
|
11
|
+
type Resolvers = LifecycleSchedulerOptions['resolvers'];
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate a list of top-level condition nodes as an implicit AND.
|
|
14
|
+
*
|
|
15
|
+
* @returns `{ pass: true }` when all conditions pass, or `{ pass: false, reason }`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function evaluateConditions(conditions: ConditionNode[], resolvers: Resolvers, user_id: string): Promise<{
|
|
18
|
+
pass: boolean;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAa,aAAa,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAEtF,KAAK,SAAS,GAAG,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAMxD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,aAAa,EAAE,EAC3B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ7C"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condition evaluation engine for lifecycle steps.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates a `ConditionNode` graph against the named resolvers supplied at
|
|
5
|
+
* scheduler construction time. The top-level `evaluateConditions` function
|
|
6
|
+
* treats the array as an implicit AND — all nodes must pass.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================
|
|
11
|
+
// Public API
|
|
12
|
+
// ============================================================
|
|
13
|
+
/**
|
|
14
|
+
* Evaluate a list of top-level condition nodes as an implicit AND.
|
|
15
|
+
*
|
|
16
|
+
* @returns `{ pass: true }` when all conditions pass, or `{ pass: false, reason }`.
|
|
17
|
+
*/
|
|
18
|
+
export async function evaluateConditions(conditions, resolvers, user_id) {
|
|
19
|
+
for (const node of conditions) {
|
|
20
|
+
const result = await evaluateNode(node, resolvers, user_id);
|
|
21
|
+
if (!result.pass) {
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { pass: true };
|
|
26
|
+
}
|
|
27
|
+
// ============================================================
|
|
28
|
+
// Internal helpers
|
|
29
|
+
// ============================================================
|
|
30
|
+
async function evaluateNode(node, resolvers, user_id) {
|
|
31
|
+
if ('kind' in node) {
|
|
32
|
+
switch (node.kind) {
|
|
33
|
+
case 'and': {
|
|
34
|
+
for (const child of node.conditions) {
|
|
35
|
+
const r = await evaluateNode(child, resolvers, user_id);
|
|
36
|
+
if (!r.pass)
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
return { pass: true };
|
|
40
|
+
}
|
|
41
|
+
case 'or': {
|
|
42
|
+
let last_reason;
|
|
43
|
+
for (const child of node.conditions) {
|
|
44
|
+
const r = await evaluateNode(child, resolvers, user_id);
|
|
45
|
+
if (r.pass)
|
|
46
|
+
return { pass: true };
|
|
47
|
+
last_reason = r.reason;
|
|
48
|
+
}
|
|
49
|
+
return { pass: false, reason: last_reason ?? 'or_all_failed' };
|
|
50
|
+
}
|
|
51
|
+
case 'not': {
|
|
52
|
+
const r = await evaluateNode(node.condition, resolvers, user_id);
|
|
53
|
+
return r.pass
|
|
54
|
+
? { pass: false, reason: 'not_condition_passed' }
|
|
55
|
+
: { pass: true };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Leaf condition
|
|
60
|
+
return evaluateLeaf(node, resolvers, user_id);
|
|
61
|
+
}
|
|
62
|
+
async function evaluateLeaf(condition, resolvers, user_id) {
|
|
63
|
+
const resolver = resolvers[condition.resolver];
|
|
64
|
+
if (!resolver) {
|
|
65
|
+
return {
|
|
66
|
+
pass: false,
|
|
67
|
+
reason: `unknown_resolver:${condition.resolver}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let actual;
|
|
71
|
+
try {
|
|
72
|
+
actual = await resolver(user_id, condition.args);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return { pass: false, reason: `resolver_error:${condition.resolver}:${msg}` };
|
|
77
|
+
}
|
|
78
|
+
const pass = compare(actual, condition.op, condition.value);
|
|
79
|
+
if (!pass) {
|
|
80
|
+
return {
|
|
81
|
+
pass: false,
|
|
82
|
+
reason: `condition_failed:${condition.resolver}:${condition.op}:${String(actual)}!=${String(condition.value)}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { pass: true };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Compare `actual` to `expected` using the given operator.
|
|
89
|
+
* Works on numbers and booleans; falls back to strict equality for other types.
|
|
90
|
+
*/
|
|
91
|
+
function compare(actual, op, expected) {
|
|
92
|
+
switch (op) {
|
|
93
|
+
case 'eq':
|
|
94
|
+
return actual === expected;
|
|
95
|
+
case 'lt':
|
|
96
|
+
return actual < expected;
|
|
97
|
+
case 'lte':
|
|
98
|
+
return actual <= expected;
|
|
99
|
+
case 'gt':
|
|
100
|
+
return actual > expected;
|
|
101
|
+
case 'gte':
|
|
102
|
+
return actual >= expected;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createLifecycleScheduler` factory.
|
|
3
|
+
*
|
|
4
|
+
* Returns a `LifecycleScheduler` that manages the full lifecycle of a
|
|
5
|
+
* multi-step email sequence per user: starting sequences, recording events,
|
|
6
|
+
* and reading status.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import type { LifecycleScheduler, LifecycleSchedulerOptions } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Job type string registered with hazo_jobs for deferred lifecycle steps.
|
|
13
|
+
* Re-exported from `hazo_notify/lifecycle` for consumer use.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LIFECYCLE_STEP_JOB_TYPE = "hazo_notify:lifecycle_step";
|
|
16
|
+
/**
|
|
17
|
+
* Create a lifecycle scheduler.
|
|
18
|
+
*
|
|
19
|
+
* The returned `LifecycleScheduler` should be created once at application
|
|
20
|
+
* startup and reused across requests. Callers must supply a `get_user_email`
|
|
21
|
+
* function so the scheduler can resolve recipient addresses at dispatch time.
|
|
22
|
+
*
|
|
23
|
+
* @param opts - Scheduler configuration.
|
|
24
|
+
* @param get_user_email - Async resolver from user_id to email address.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createLifecycleScheduler(opts: LifecycleSchedulerOptions, get_user_email: (user_id: string) => Promise<string>): LifecycleScheduler;
|
|
27
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EACV,kBAAkB,EAClB,yBAAyB,EAE1B,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,eAAO,MAAM,uBAAuB,+BAA+B,CAAC;AAcpE;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,yBAAyB,EAC/B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACnD,kBAAkB,CAmGpB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createLifecycleScheduler` factory.
|
|
3
|
+
*
|
|
4
|
+
* Returns a `LifecycleScheduler` that manages the full lifecycle of a
|
|
5
|
+
* multi-step email sequence per user: starting sequences, recording events,
|
|
6
|
+
* and reading status.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { insertStatusRow, queryStatusRows } from './status.js';
|
|
11
|
+
import { recordEvent as _recordEvent } from './events.js';
|
|
12
|
+
import { createLifecycleStepHandler } from './handler.js';
|
|
13
|
+
/**
|
|
14
|
+
* Job type string registered with hazo_jobs for deferred lifecycle steps.
|
|
15
|
+
* Re-exported from `hazo_notify/lifecycle` for consumer use.
|
|
16
|
+
*/
|
|
17
|
+
export const LIFECYCLE_STEP_JOB_TYPE = 'hazo_notify:lifecycle_step';
|
|
18
|
+
/**
|
|
19
|
+
* Detect UNIQUE / duplicate-key violation errors from Postgres (PostgREST),
|
|
20
|
+
* SQLite (better-sqlite3), and generic RDBMS drivers.
|
|
21
|
+
*
|
|
22
|
+
* PostgREST returns HTTP 409 with "duplicate key" in the message body.
|
|
23
|
+
* better-sqlite3 throws "UNIQUE constraint failed".
|
|
24
|
+
*/
|
|
25
|
+
function is_unique_violation(err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
27
|
+
return msg.includes('unique') || msg.includes('duplicate');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a lifecycle scheduler.
|
|
31
|
+
*
|
|
32
|
+
* The returned `LifecycleScheduler` should be created once at application
|
|
33
|
+
* startup and reused across requests. Callers must supply a `get_user_email`
|
|
34
|
+
* function so the scheduler can resolve recipient addresses at dispatch time.
|
|
35
|
+
*
|
|
36
|
+
* @param opts - Scheduler configuration.
|
|
37
|
+
* @param get_user_email - Async resolver from user_id to email address.
|
|
38
|
+
*/
|
|
39
|
+
export function createLifecycleScheduler(opts, get_user_email) {
|
|
40
|
+
const job_handler = createLifecycleStepHandler(opts, get_user_email);
|
|
41
|
+
async function start({ user_id, sequence_id, }) {
|
|
42
|
+
const steps = opts.sequences[sequence_id];
|
|
43
|
+
if (!steps) {
|
|
44
|
+
throw new Error(`[lifecycle] sequence not found: '${sequence_id}'`);
|
|
45
|
+
}
|
|
46
|
+
const now = new Date();
|
|
47
|
+
for (const step of steps) {
|
|
48
|
+
// 1. Insert status row (idempotent — skip on unique violation).
|
|
49
|
+
let inserted_row;
|
|
50
|
+
try {
|
|
51
|
+
inserted_row = await insertStatusRow(opts.hazo_connect, {
|
|
52
|
+
app_id: opts.app_id,
|
|
53
|
+
user_id,
|
|
54
|
+
sequence_id,
|
|
55
|
+
step_id: step.id,
|
|
56
|
+
status: 'scheduled',
|
|
57
|
+
sent_at: null,
|
|
58
|
+
skip_reason: null,
|
|
59
|
+
job_id: null,
|
|
60
|
+
failure_message: null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (is_unique_violation(err)) {
|
|
65
|
+
// Already started — idempotent no-op for this step.
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
// 2. For time-triggered steps, submit a deferred job.
|
|
71
|
+
if (step.trigger.kind === 'time') {
|
|
72
|
+
const run_at = new Date(now.getTime() + step.trigger.offset_seconds * 1000).toISOString();
|
|
73
|
+
const { jobId } = await opts.jobs.submit({
|
|
74
|
+
type: LIFECYCLE_STEP_JOB_TYPE,
|
|
75
|
+
payload: {
|
|
76
|
+
status_row_id: inserted_row.id,
|
|
77
|
+
app_id: opts.app_id,
|
|
78
|
+
user_id,
|
|
79
|
+
sequence_id,
|
|
80
|
+
step_id: step.id,
|
|
81
|
+
},
|
|
82
|
+
runAt: run_at,
|
|
83
|
+
});
|
|
84
|
+
// Persist job_id on the status row so it can be correlated later.
|
|
85
|
+
try {
|
|
86
|
+
await opts.hazo_connect.updateById('hazo_notify_lifecycle_status', inserted_row.id, { job_id: jobId });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Non-fatal: the job was still submitted. job_id is cosmetic.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Event-triggered steps: no job submitted — they fire via recordEvent().
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function recordEventPublic(params) {
|
|
96
|
+
return _recordEvent(opts, get_user_email, params);
|
|
97
|
+
}
|
|
98
|
+
async function getStatus({ user_id, sequence_id, }) {
|
|
99
|
+
return queryStatusRows(opts.hazo_connect, opts.app_id, user_id, sequence_id);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
start,
|
|
103
|
+
recordEvent: recordEventPublic,
|
|
104
|
+
getStatus,
|
|
105
|
+
__jobHandler: job_handler,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD helpers for the `hazo_notify_lifecycle_status` table.
|
|
3
|
+
*
|
|
4
|
+
* All functions accept a `HazoConnectInstance` so they work against any
|
|
5
|
+
* conforming adapter (PostgREST, direct Postgres, SQLite test doubles, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import type { HazoConnectInstance } from '../template_manager/types.js';
|
|
10
|
+
import type { LifecycleStatusRow } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Insert a new lifecycle status row.
|
|
13
|
+
*
|
|
14
|
+
* The caller must NOT supply `id`, `scheduled_at`, `attempt_count`, or
|
|
15
|
+
* `dispatch_meta` — those are set by the DB default or left as NULL.
|
|
16
|
+
*/
|
|
17
|
+
export declare function insertStatusRow(hazo_connect: HazoConnectInstance, row: Omit<LifecycleStatusRow, 'id' | 'scheduled_at' | 'attempt_count' | 'dispatch_meta'>): Promise<LifecycleStatusRow>;
|
|
18
|
+
/**
|
|
19
|
+
* Load a single status row by primary key.
|
|
20
|
+
* Returns `null` if not found.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadStatusRow(hazo_connect: HazoConnectInstance, id: string): Promise<LifecycleStatusRow | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Partial update a status row by primary key.
|
|
25
|
+
* The `patch` may include any subset of `LifecycleStatusRow` fields.
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateStatusRow(hazo_connect: HazoConnectInstance, id: string, patch: Partial<LifecycleStatusRow>): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Query status rows for a (app_id, user_id) pair, optionally filtered to one
|
|
30
|
+
* sequence. Returns rows ordered by `scheduled_at` ascending.
|
|
31
|
+
*/
|
|
32
|
+
export declare function queryStatusRows(hazo_connect: HazoConnectInstance, app_id: string, user_id: string, sequence_id?: string): Promise<LifecycleStatusRow[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Find a single status row for a specific (app_id, user_id, sequence_id, step_id).
|
|
35
|
+
* Used by `recordEvent` to locate event-triggered steps.
|
|
36
|
+
* Returns `null` if not found.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findEventStepRow(hazo_connect: HazoConnectInstance, app_id: string, user_id: string, sequence_id: string, step_id: string): Promise<LifecycleStatusRow | null>;
|
|
39
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/status.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,EACjC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,GAAG,cAAc,GAAG,eAAe,GAAG,eAAe,CAAC,GACvF,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,YAAY,EAAE,mBAAmB,EACjC,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAEpC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,EACjC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,EACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAe/B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,mBAAmB,EACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAapC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD helpers for the `hazo_notify_lifecycle_status` table.
|
|
3
|
+
*
|
|
4
|
+
* All functions accept a `HazoConnectInstance` so they work against any
|
|
5
|
+
* conforming adapter (PostgREST, direct Postgres, SQLite test doubles, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
const TABLE = 'hazo_notify_lifecycle_status';
|
|
10
|
+
/**
|
|
11
|
+
* Insert a new lifecycle status row.
|
|
12
|
+
*
|
|
13
|
+
* The caller must NOT supply `id`, `scheduled_at`, `attempt_count`, or
|
|
14
|
+
* `dispatch_meta` — those are set by the DB default or left as NULL.
|
|
15
|
+
*/
|
|
16
|
+
export async function insertStatusRow(hazo_connect, row) {
|
|
17
|
+
return hazo_connect.insert(TABLE, row);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a single status row by primary key.
|
|
21
|
+
* Returns `null` if not found.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadStatusRow(hazo_connect, id) {
|
|
24
|
+
return hazo_connect.findById(TABLE, id);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Partial update a status row by primary key.
|
|
28
|
+
* The `patch` may include any subset of `LifecycleStatusRow` fields.
|
|
29
|
+
*/
|
|
30
|
+
export async function updateStatusRow(hazo_connect, id, patch) {
|
|
31
|
+
await hazo_connect.updateById(TABLE, id, patch);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Query status rows for a (app_id, user_id) pair, optionally filtered to one
|
|
35
|
+
* sequence. Returns rows ordered by `scheduled_at` ascending.
|
|
36
|
+
*/
|
|
37
|
+
export async function queryStatusRows(hazo_connect, app_id, user_id, sequence_id) {
|
|
38
|
+
let q = hazo_connect
|
|
39
|
+
.query()
|
|
40
|
+
.from(TABLE)
|
|
41
|
+
.select('*')
|
|
42
|
+
.where('app_id', 'eq', app_id)
|
|
43
|
+
.where('user_id', 'eq', user_id);
|
|
44
|
+
if (sequence_id !== undefined) {
|
|
45
|
+
q = q.where('sequence_id', 'eq', sequence_id);
|
|
46
|
+
}
|
|
47
|
+
q = q.order('scheduled_at', 'asc');
|
|
48
|
+
return hazo_connect.list(q);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Find a single status row for a specific (app_id, user_id, sequence_id, step_id).
|
|
52
|
+
* Used by `recordEvent` to locate event-triggered steps.
|
|
53
|
+
* Returns `null` if not found.
|
|
54
|
+
*/
|
|
55
|
+
export async function findEventStepRow(hazo_connect, app_id, user_id, sequence_id, step_id) {
|
|
56
|
+
const rows = await hazo_connect.list(hazo_connect
|
|
57
|
+
.query()
|
|
58
|
+
.from(TABLE)
|
|
59
|
+
.select('*')
|
|
60
|
+
.where('app_id', 'eq', app_id)
|
|
61
|
+
.where('user_id', 'eq', user_id)
|
|
62
|
+
.where('sequence_id', 'eq', sequence_id)
|
|
63
|
+
.where('step_id', 'eq', step_id)
|
|
64
|
+
.limit(1));
|
|
65
|
+
return rows[0] ?? null;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/status.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,KAAK,GAAG,8BAA8B,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAiC,EACjC,GAAwF;IAExF,OAAO,YAAY,CAAC,MAAM,CAAqB,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,YAAiC,EACjC,EAAU;IAEV,OAAO,YAAY,CAAC,QAAQ,CAAqB,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAiC,EACjC,EAAU,EACV,KAAkC;IAElC,MAAM,YAAY,CAAC,UAAU,CAAqB,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAiC,EACjC,MAAc,EACd,OAAe,EACf,WAAoB;IAEpB,IAAI,CAAC,GAAG,YAAY;SACjB,KAAK,EAAE;SACP,IAAI,CAAC,KAAK,CAAC;SACX,MAAM,CAAC,GAAG,CAAC;SACX,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC;SAC7B,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAEnC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAChD,CAAC;IAED,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAEnC,OAAO,YAAY,CAAC,IAAI,CAAqB,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,YAAiC,EACjC,MAAc,EACd,OAAe,EACf,WAAmB,EACnB,OAAe;IAEf,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAClC,YAAY;SACT,KAAK,EAAE;SACP,IAAI,CAAC,KAAK,CAAC;SACX,MAAM,CAAC,GAAG,CAAC;SACX,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC;SAC7B,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC;SAC/B,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC;SACvC,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC;SAC/B,KAAK,CAAC,CAAC,CAAC,CACZ,CAAC;IACF,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the hazo_notify lifecycle scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle sequences define time-triggered and event-triggered email steps
|
|
5
|
+
* with conditional dispatch logic. The scheduler manages status tracking and
|
|
6
|
+
* deferred job submission via hazo_jobs (optional peer dep).
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import type { HazoConnectInstance } from '../template_manager/types.js';
|
|
11
|
+
import type { HazoJob, HazoJobLogger, HazoJobHandler } from '../jobs/types.js';
|
|
12
|
+
export type { HazoConnectInstance, HazoJob, HazoJobLogger, HazoJobHandler };
|
|
13
|
+
/**
|
|
14
|
+
* Structural subset of hazo_jobs `JobsClient.submit` extended with `runAt`
|
|
15
|
+
* for deferred scheduling. Declared locally so the lifecycle module does not
|
|
16
|
+
* import from `hazo_jobs` at runtime (optional peer dep).
|
|
17
|
+
*/
|
|
18
|
+
export interface HazoJobsSubmitter {
|
|
19
|
+
submit(opts: {
|
|
20
|
+
type: string;
|
|
21
|
+
payload?: unknown;
|
|
22
|
+
priority?: number;
|
|
23
|
+
maxAttempts?: number;
|
|
24
|
+
expiresInSec?: number;
|
|
25
|
+
/** ISO 8601 string for deferred execution. */
|
|
26
|
+
runAt?: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
jobId: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/** A time-based trigger fires after `offset_seconds` from sequence start. */
|
|
32
|
+
export type LifecycleTrigger = {
|
|
33
|
+
kind: 'time';
|
|
34
|
+
offset_seconds: number;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'event';
|
|
37
|
+
event_kind: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A leaf condition node. The `resolver` key names a function in
|
|
41
|
+
* `LifecycleSchedulerOptions.resolvers` that computes the current value
|
|
42
|
+
* for `user_id`. The result is compared against `value` using `op`.
|
|
43
|
+
*/
|
|
44
|
+
export interface Condition {
|
|
45
|
+
resolver: string;
|
|
46
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte';
|
|
47
|
+
value: unknown;
|
|
48
|
+
args?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
/** A composite condition node (boolean operators) or a leaf `Condition`. */
|
|
51
|
+
export type ConditionNode = Condition | {
|
|
52
|
+
kind: 'and';
|
|
53
|
+
conditions: ConditionNode[];
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'or';
|
|
56
|
+
conditions: ConditionNode[];
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'not';
|
|
59
|
+
condition: ConditionNode;
|
|
60
|
+
};
|
|
61
|
+
/** A single step within a lifecycle sequence. */
|
|
62
|
+
export interface LifecycleStep {
|
|
63
|
+
id: string;
|
|
64
|
+
trigger: LifecycleTrigger;
|
|
65
|
+
/** Template name to render and send when this step dispatches. */
|
|
66
|
+
template: string;
|
|
67
|
+
/**
|
|
68
|
+
* Top-level conditions evaluated as an implicit AND. All must pass for the
|
|
69
|
+
* step to dispatch. An empty array means "always dispatch".
|
|
70
|
+
*/
|
|
71
|
+
conditions: ConditionNode[];
|
|
72
|
+
}
|
|
73
|
+
export interface LifecycleStatusRow {
|
|
74
|
+
id: string;
|
|
75
|
+
app_id: string;
|
|
76
|
+
user_id: string;
|
|
77
|
+
sequence_id: string;
|
|
78
|
+
step_id: string;
|
|
79
|
+
status: 'scheduled' | 'sent' | 'skipped' | 'failed';
|
|
80
|
+
scheduled_at: string;
|
|
81
|
+
sent_at: string | null;
|
|
82
|
+
skip_reason: string | null;
|
|
83
|
+
job_id: string | null;
|
|
84
|
+
failure_message: string | null;
|
|
85
|
+
attempt_count: number;
|
|
86
|
+
dispatch_meta: Record<string, unknown> | null;
|
|
87
|
+
}
|
|
88
|
+
export interface LifecycleSchedulerOptions {
|
|
89
|
+
/** DB adapter (from hazo_notify/template_manager). */
|
|
90
|
+
hazo_connect: HazoConnectInstance;
|
|
91
|
+
/**
|
|
92
|
+
* Structural hazo_jobs submitter. Must support `runAt` for deferred steps.
|
|
93
|
+
* Declared locally — no runtime `hazo_jobs` import.
|
|
94
|
+
*/
|
|
95
|
+
jobs: HazoJobsSubmitter;
|
|
96
|
+
/** Application identifier stored on every status row. */
|
|
97
|
+
app_id: string;
|
|
98
|
+
/** Default locale for template resolution (v1: English only). */
|
|
99
|
+
default_locale?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Named resolver functions. Each resolver receives a `user_id` and optional
|
|
102
|
+
* `args`, and returns the current value for that dimension (number, boolean,
|
|
103
|
+
* string, etc.). Used by the condition evaluator.
|
|
104
|
+
*/
|
|
105
|
+
resolvers: Record<string, (user_id: string, args?: Record<string, unknown>) => Promise<unknown>>;
|
|
106
|
+
/**
|
|
107
|
+
* Optional opt-out check. When provided, the handler calls this before
|
|
108
|
+
* dispatching; if it returns `true`, the step is skipped with
|
|
109
|
+
* `skip_reason='opted_out'`.
|
|
110
|
+
*/
|
|
111
|
+
is_opted_out?: (user_id: string) => Promise<boolean>;
|
|
112
|
+
/** Sequence definitions keyed by sequence_id. */
|
|
113
|
+
sequences: Record<string, LifecycleStep[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Max job attempts before a step row is marked `failed`.
|
|
116
|
+
* Defaults to 3.
|
|
117
|
+
*/
|
|
118
|
+
max_attempts?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Optional per-user variable resolver. Called at dispatch time to supply
|
|
121
|
+
* extra template variables (e.g. personalised opt-out links). The returned
|
|
122
|
+
* object is merged with the base `variables` map; caller-supplied keys win.
|
|
123
|
+
*/
|
|
124
|
+
get_variables?: (user_id: string, step_id: string) => Promise<Record<string, string>>;
|
|
125
|
+
}
|
|
126
|
+
export interface LifecycleScheduler {
|
|
127
|
+
/**
|
|
128
|
+
* Begin a sequence for a user. Inserts one status row per step and submits
|
|
129
|
+
* deferred jobs for time-triggered steps. Idempotent — duplicate calls for
|
|
130
|
+
* the same (user_id, sequence_id) are no-ops (unique constraint handling).
|
|
131
|
+
*/
|
|
132
|
+
start(opts: {
|
|
133
|
+
user_id: string;
|
|
134
|
+
sequence_id: string;
|
|
135
|
+
}): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Fire event-triggered steps. Scans all sequences for steps whose trigger
|
|
138
|
+
* matches `event_kind`, then dispatches any that are in `scheduled` state.
|
|
139
|
+
*/
|
|
140
|
+
recordEvent(opts: {
|
|
141
|
+
user_id: string;
|
|
142
|
+
event_kind: string;
|
|
143
|
+
payload?: Record<string, unknown>;
|
|
144
|
+
}): Promise<void>;
|
|
145
|
+
/** Read status rows for a user, optionally filtered to one sequence. */
|
|
146
|
+
getStatus(opts: {
|
|
147
|
+
user_id: string;
|
|
148
|
+
sequence_id?: string;
|
|
149
|
+
}): Promise<LifecycleStatusRow[]>;
|
|
150
|
+
/**
|
|
151
|
+
* Internal hazo_jobs handler exposed so `registerLifecycleHandlers` can wire
|
|
152
|
+
* it in without reaching into scheduler internals. Not intended for direct
|
|
153
|
+
* consumer use.
|
|
154
|
+
*/
|
|
155
|
+
__jobHandler: HazoJobHandler<LifecycleStepPayload>;
|
|
156
|
+
}
|
|
157
|
+
/** Payload submitted when a time-triggered lifecycle step is deferred. */
|
|
158
|
+
export interface LifecycleStepPayload {
|
|
159
|
+
status_row_id: string;
|
|
160
|
+
app_id: string;
|
|
161
|
+
user_id: string;
|
|
162
|
+
sequence_id: string;
|
|
163
|
+
step_id: string;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the hazo_notify lifecycle scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle sequences define time-triggered and event-triggered email steps
|
|
5
|
+
* with conditional dispatch logic. The scheduler manages status tracking and
|
|
6
|
+
* deferred job submission via hazo_jobs (optional peer dep).
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|