hazo_notify 5.0.1 → 5.3.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 +42 -10
- 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 +30 -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.map +1 -1
- package/dist/lib/inbox/storage.js +8 -2
- package/dist/lib/inbox/storage.js.map +1 -1
- package/dist/lib/inbox/types.d.ts +10 -1
- package/dist/lib/inbox/types.d.ts.map +1 -1
- package/dist/lib/jobs/digest.d.ts +77 -0
- package/dist/lib/jobs/digest.d.ts.map +1 -0
- package/dist/lib/jobs/digest.js +127 -0
- package/dist/lib/jobs/digest.js.map +1 -0
- package/dist/lib/jobs/index.d.ts +2 -0
- package/dist/lib/jobs/index.d.ts.map +1 -1
- package/dist/lib/jobs/index.js +1 -0
- package/dist/lib/jobs/index.js.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 +196 -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 +129 -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/migrations/011_digest_status.pg.sql +21 -0
- package/migrations/011_digest_status.sqlite.sql +43 -0
- package/migrations/012_preferences_digest.pg.sql +12 -0
- package/migrations/012_preferences_digest.sqlite.sql +51 -0
- package/package.json +15 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `recordEvent` implementation for lifecycle sequences.
|
|
3
|
+
*
|
|
4
|
+
* When a product event fires (e.g. "user_added_first_person"), this module
|
|
5
|
+
* scans all sequences for matching event-triggered steps, loads their status
|
|
6
|
+
* rows, and dispatches any that are still in `scheduled` state.
|
|
7
|
+
*
|
|
8
|
+
* Deduplication contract:
|
|
9
|
+
* - If the status row is 'sent' → no-op (already dispatched).
|
|
10
|
+
* - If the status row is not found → no-op (user never started the sequence).
|
|
11
|
+
* - If the status row is 'skipped' or 'failed' → no-op (terminal state).
|
|
12
|
+
* - Only 'scheduled' rows trigger dispatch.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { evaluateConditions } from './resolver.js';
|
|
18
|
+
import { dispatchStep } from './dispatch.js';
|
|
19
|
+
import { findEventStepRow, updateStatusRow } from './status.js';
|
|
20
|
+
/**
|
|
21
|
+
* Fire all event-triggered steps whose `event_kind` matches the emitted event.
|
|
22
|
+
*
|
|
23
|
+
* Scans every sequence in `opts.sequences` (or only `sequence_id` if supplied)
|
|
24
|
+
* for steps with `trigger.kind === 'event' && trigger.event_kind === event_kind`.
|
|
25
|
+
* For each match, loads the status row and dispatches if status is `scheduled`.
|
|
26
|
+
*
|
|
27
|
+
* @param opts - Scheduler options (hazo_connect, resolvers, sequences, etc.)
|
|
28
|
+
* @param get_user_email - Async function that resolves the email address for user_id.
|
|
29
|
+
* @param params - Event parameters from the caller.
|
|
30
|
+
*/
|
|
31
|
+
export async function recordEvent(opts, get_user_email, params) {
|
|
32
|
+
const { user_id, event_kind } = params;
|
|
33
|
+
// Determine which sequence IDs to scan.
|
|
34
|
+
const sequence_ids = params.sequence_id
|
|
35
|
+
? [params.sequence_id]
|
|
36
|
+
: Object.keys(opts.sequences);
|
|
37
|
+
for (const seq_id of sequence_ids) {
|
|
38
|
+
const steps = opts.sequences[seq_id];
|
|
39
|
+
if (!steps)
|
|
40
|
+
continue;
|
|
41
|
+
for (const step of steps) {
|
|
42
|
+
// Only process event-triggered steps matching the event_kind.
|
|
43
|
+
if (step.trigger.kind !== 'event' ||
|
|
44
|
+
step.trigger.event_kind !== event_kind) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Load the status row for this (app_id, user_id, sequence_id, step_id).
|
|
48
|
+
const row = await findEventStepRow(opts.hazo_connect, opts.app_id, user_id, seq_id, step.id);
|
|
49
|
+
if (!row) {
|
|
50
|
+
// User never started this sequence — no-op.
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (row.status === 'sent') {
|
|
54
|
+
// Already dispatched — dedupe.
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (row.status !== 'scheduled') {
|
|
58
|
+
// Skipped or failed — don't re-trigger.
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Opt-out check.
|
|
62
|
+
if (opts.is_opted_out) {
|
|
63
|
+
const opted_out = await opts.is_opted_out(user_id);
|
|
64
|
+
if (opted_out) {
|
|
65
|
+
await updateStatusRow(opts.hazo_connect, row.id, {
|
|
66
|
+
status: 'skipped',
|
|
67
|
+
skip_reason: 'opted_out',
|
|
68
|
+
attempt_count: row.attempt_count + 1,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Evaluate conditions.
|
|
74
|
+
const condition_result = await evaluateConditions(step.conditions, opts.resolvers, user_id);
|
|
75
|
+
if (!condition_result.pass) {
|
|
76
|
+
await updateStatusRow(opts.hazo_connect, row.id, {
|
|
77
|
+
status: 'skipped',
|
|
78
|
+
skip_reason: `condition_failed:${condition_result.reason ?? 'unknown'}`,
|
|
79
|
+
attempt_count: row.attempt_count + 1,
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Resolve recipient, extra variables, and dispatch.
|
|
84
|
+
const to = await get_user_email(user_id);
|
|
85
|
+
const raw_vars = opts.get_variables
|
|
86
|
+
? await opts.get_variables(user_id, step.id)
|
|
87
|
+
: {};
|
|
88
|
+
let subject_override;
|
|
89
|
+
let ab_variant;
|
|
90
|
+
const { subject_a, subject_b, ...extra_vars } = raw_vars;
|
|
91
|
+
if (subject_a && subject_b) {
|
|
92
|
+
const hash_byte = createHash('sha256')
|
|
93
|
+
.update(user_id + step.template)
|
|
94
|
+
.digest()[0];
|
|
95
|
+
ab_variant = (hash_byte & 1) === 0 ? 'a' : 'b';
|
|
96
|
+
subject_override = ab_variant === 'a' ? subject_a : subject_b;
|
|
97
|
+
}
|
|
98
|
+
await dispatchStep({
|
|
99
|
+
hazo_connect: opts.hazo_connect,
|
|
100
|
+
user_id,
|
|
101
|
+
template_name: step.template,
|
|
102
|
+
variables: extra_vars,
|
|
103
|
+
to,
|
|
104
|
+
locale: opts.default_locale ?? null,
|
|
105
|
+
subject_override,
|
|
106
|
+
});
|
|
107
|
+
await updateStatusRow(opts.hazo_connect, row.id, {
|
|
108
|
+
status: 'sent',
|
|
109
|
+
sent_at: new Date().toISOString(),
|
|
110
|
+
attempt_count: row.attempt_count + 1,
|
|
111
|
+
...(ab_variant
|
|
112
|
+
? { dispatch_meta: { ab_variant, subject: subject_override } }
|
|
113
|
+
: {}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/events.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hazo_jobs handler for `hazo_notify:lifecycle_step` jobs.
|
|
3
|
+
*
|
|
4
|
+
* Each time-triggered lifecycle step is deferred via hazo_jobs. When the job
|
|
5
|
+
* fires, this handler re-evaluates conditions and either dispatches the email
|
|
6
|
+
* or marks the row as skipped/failed.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import type { HazoJobHandler, LifecycleSchedulerOptions, LifecycleStepPayload } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Factory that produces the hazo_jobs handler for `lifecycle_step` jobs.
|
|
13
|
+
*
|
|
14
|
+
* Handler flow:
|
|
15
|
+
* 1. Load status row — guard on status !== 'scheduled' (idempotency).
|
|
16
|
+
* 2. Increment attempt_count.
|
|
17
|
+
* 3. Check opt-out via opts.is_opted_out (if provided).
|
|
18
|
+
* 4. Evaluate ConditionNode graph via evaluateConditions.
|
|
19
|
+
* 5. If pass: dispatch via dispatchStep, mark 'sent'.
|
|
20
|
+
* 6. If fail: mark 'skipped' with reason.
|
|
21
|
+
* 7. On exception: if attempts exhausted, mark 'failed'; else leave 'scheduled'
|
|
22
|
+
* so hazo_jobs retries.
|
|
23
|
+
*
|
|
24
|
+
* @param opts - Same options passed to `createLifecycleScheduler`.
|
|
25
|
+
* @param get_user_email - Async function that resolves an email address for a
|
|
26
|
+
* given user_id. Required to know where to send the step email.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createLifecycleStepHandler(opts: LifecycleSchedulerOptions, get_user_email: (user_id: string) => Promise<string>): HazoJobHandler<LifecycleStepPayload>;
|
|
29
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAGV,cAAc,EACd,yBAAyB,EACzB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,yBAAyB,EAC/B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACnD,cAAc,CAAC,oBAAoB,CAAC,CA2ItC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hazo_jobs handler for `hazo_notify:lifecycle_step` jobs.
|
|
3
|
+
*
|
|
4
|
+
* Each time-triggered lifecycle step is deferred via hazo_jobs. When the job
|
|
5
|
+
* fires, this handler re-evaluates conditions and either dispatches the email
|
|
6
|
+
* or marks the row as skipped/failed.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { evaluateConditions } from './resolver.js';
|
|
12
|
+
import { dispatchStep } from './dispatch.js';
|
|
13
|
+
import { loadStatusRow, updateStatusRow } from './status.js';
|
|
14
|
+
/**
|
|
15
|
+
* Factory that produces the hazo_jobs handler for `lifecycle_step` jobs.
|
|
16
|
+
*
|
|
17
|
+
* Handler flow:
|
|
18
|
+
* 1. Load status row — guard on status !== 'scheduled' (idempotency).
|
|
19
|
+
* 2. Increment attempt_count.
|
|
20
|
+
* 3. Check opt-out via opts.is_opted_out (if provided).
|
|
21
|
+
* 4. Evaluate ConditionNode graph via evaluateConditions.
|
|
22
|
+
* 5. If pass: dispatch via dispatchStep, mark 'sent'.
|
|
23
|
+
* 6. If fail: mark 'skipped' with reason.
|
|
24
|
+
* 7. On exception: if attempts exhausted, mark 'failed'; else leave 'scheduled'
|
|
25
|
+
* so hazo_jobs retries.
|
|
26
|
+
*
|
|
27
|
+
* @param opts - Same options passed to `createLifecycleScheduler`.
|
|
28
|
+
* @param get_user_email - Async function that resolves an email address for a
|
|
29
|
+
* given user_id. Required to know where to send the step email.
|
|
30
|
+
*/
|
|
31
|
+
export function createLifecycleStepHandler(opts, get_user_email) {
|
|
32
|
+
const max_attempts = opts.max_attempts ?? 3;
|
|
33
|
+
return async function lifecycle_step_handler(job, log) {
|
|
34
|
+
// hazo_jobs (SQLite backend) may deliver payload as a JSON string.
|
|
35
|
+
const payload = typeof job.payload === 'string'
|
|
36
|
+
? JSON.parse(job.payload)
|
|
37
|
+
: job.payload;
|
|
38
|
+
const { status_row_id, user_id, sequence_id, step_id } = payload;
|
|
39
|
+
log?.info?.(`[lifecycle] step start row=${status_row_id} user=${user_id} seq=${sequence_id} step=${step_id}`);
|
|
40
|
+
// 1. Load the status row.
|
|
41
|
+
const row = await loadStatusRow(opts.hazo_connect, status_row_id);
|
|
42
|
+
// 2. Guard: already processed or gone.
|
|
43
|
+
if (!row || row.status !== 'scheduled') {
|
|
44
|
+
log?.info?.(`[lifecycle] step skip (already handled) row=${status_row_id} status=${row?.status ?? 'not_found'}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// 3. Increment attempt counter.
|
|
48
|
+
const attempt_count = row.attempt_count + 1;
|
|
49
|
+
try {
|
|
50
|
+
// 4. Opt-out check.
|
|
51
|
+
if (opts.is_opted_out) {
|
|
52
|
+
const opted_out = await opts.is_opted_out(user_id);
|
|
53
|
+
if (opted_out) {
|
|
54
|
+
await updateStatusRow(opts.hazo_connect, status_row_id, {
|
|
55
|
+
status: 'skipped',
|
|
56
|
+
skip_reason: 'opted_out',
|
|
57
|
+
attempt_count,
|
|
58
|
+
});
|
|
59
|
+
log?.info?.(`[lifecycle] step skipped (opted_out) row=${status_row_id}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 5. Locate the step definition.
|
|
64
|
+
const sequence = opts.sequences[sequence_id];
|
|
65
|
+
if (!sequence) {
|
|
66
|
+
throw new Error(`sequence_not_found:${sequence_id}`);
|
|
67
|
+
}
|
|
68
|
+
const step = sequence.find((s) => s.id === step_id);
|
|
69
|
+
if (!step) {
|
|
70
|
+
throw new Error(`step_not_found:${sequence_id}/${step_id}`);
|
|
71
|
+
}
|
|
72
|
+
// 6. Evaluate conditions.
|
|
73
|
+
const condition_result = await evaluateConditions(step.conditions, opts.resolvers, user_id);
|
|
74
|
+
if (!condition_result.pass) {
|
|
75
|
+
const skip_reason = `condition_failed:${condition_result.reason ?? 'unknown'}`;
|
|
76
|
+
await updateStatusRow(opts.hazo_connect, status_row_id, {
|
|
77
|
+
status: 'skipped',
|
|
78
|
+
skip_reason,
|
|
79
|
+
attempt_count,
|
|
80
|
+
});
|
|
81
|
+
log?.info?.(`[lifecycle] step skipped row=${status_row_id} reason=${skip_reason}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// 7. Resolve recipient email address.
|
|
85
|
+
const to = await get_user_email(user_id);
|
|
86
|
+
// 8. Resolve extra template variables (e.g. opt-out link, A/B subjects).
|
|
87
|
+
const raw_vars = opts.get_variables
|
|
88
|
+
? await opts.get_variables(user_id, step_id)
|
|
89
|
+
: {};
|
|
90
|
+
// 8a. A/B subject selection: if both subject_a and subject_b are provided,
|
|
91
|
+
// pick one deterministically by hashing user_id + template_name.
|
|
92
|
+
// Remove both from template variables so they don't appear in the body.
|
|
93
|
+
let subject_override;
|
|
94
|
+
let ab_variant;
|
|
95
|
+
const { subject_a, subject_b, ...extra_vars } = raw_vars;
|
|
96
|
+
if (subject_a && subject_b) {
|
|
97
|
+
const hash_byte = createHash('sha256')
|
|
98
|
+
.update(user_id + step.template)
|
|
99
|
+
.digest()[0];
|
|
100
|
+
ab_variant = (hash_byte & 1) === 0 ? 'a' : 'b';
|
|
101
|
+
subject_override = ab_variant === 'a' ? subject_a : subject_b;
|
|
102
|
+
}
|
|
103
|
+
// 9. Dispatch the email.
|
|
104
|
+
await dispatchStep({
|
|
105
|
+
hazo_connect: opts.hazo_connect,
|
|
106
|
+
user_id,
|
|
107
|
+
template_name: step.template,
|
|
108
|
+
variables: extra_vars,
|
|
109
|
+
to,
|
|
110
|
+
locale: opts.default_locale ?? null,
|
|
111
|
+
subject_override,
|
|
112
|
+
});
|
|
113
|
+
// 10. Mark as sent (include dispatch_meta for A/B tracking).
|
|
114
|
+
await updateStatusRow(opts.hazo_connect, status_row_id, {
|
|
115
|
+
status: 'sent',
|
|
116
|
+
sent_at: new Date().toISOString(),
|
|
117
|
+
attempt_count,
|
|
118
|
+
...(ab_variant
|
|
119
|
+
? { dispatch_meta: { ab_variant, subject: subject_override } }
|
|
120
|
+
: {}),
|
|
121
|
+
});
|
|
122
|
+
log?.info?.(`[lifecycle] step sent row=${status_row_id} template=${step.template}`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
126
|
+
log?.error?.(`[lifecycle] step error row=${status_row_id} err=${msg}`);
|
|
127
|
+
if (attempt_count >= max_attempts) {
|
|
128
|
+
await updateStatusRow(opts.hazo_connect, status_row_id, {
|
|
129
|
+
status: 'failed',
|
|
130
|
+
failure_message: msg,
|
|
131
|
+
attempt_count,
|
|
132
|
+
});
|
|
133
|
+
log?.error?.(`[lifecycle] step failed permanently row=${status_row_id}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Leave status as 'scheduled' so hazo_jobs retries the job.
|
|
137
|
+
await updateStatusRow(opts.hazo_connect, status_row_id, {
|
|
138
|
+
attempt_count,
|
|
139
|
+
failure_message: msg,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hazo_notify/lifecycle — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import {
|
|
8
|
+
* createLifecycleScheduler,
|
|
9
|
+
* registerLifecycleHandlers,
|
|
10
|
+
* } from 'hazo_notify/lifecycle';
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
export { createLifecycleScheduler, LIFECYCLE_STEP_JOB_TYPE } from './scheduler.js';
|
|
16
|
+
export { registerLifecycleHandlers } from './register.js';
|
|
17
|
+
export { evaluateConditions } from './resolver.js';
|
|
18
|
+
export { insertStatusRow, loadStatusRow, updateStatusRow, queryStatusRows, findEventStepRow, } from './status.js';
|
|
19
|
+
export { dispatchStep } from './dispatch.js';
|
|
20
|
+
export type { LifecycleTrigger, Condition, ConditionNode, LifecycleStep, LifecycleStatusRow, LifecycleSchedulerOptions, LifecycleScheduler, LifecycleStepPayload, HazoJobsSubmitter, } from './types.js';
|
|
21
|
+
export type { HazoConnectInstance, HazoJob, HazoJobLogger, HazoJobHandler, } from './types.js';
|
|
22
|
+
export { lifecycleDefaultTemplates } from './default_templates.js';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","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;AAE7C,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,mBAAmB,EACnB,OAAO,EACP,aAAa,EACb,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hazo_notify/lifecycle — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import {
|
|
8
|
+
* createLifecycleScheduler,
|
|
9
|
+
* registerLifecycleHandlers,
|
|
10
|
+
* } from 'hazo_notify/lifecycle';
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
export { createLifecycleScheduler, LIFECYCLE_STEP_JOB_TYPE } from './scheduler.js';
|
|
16
|
+
export { registerLifecycleHandlers } from './register.js';
|
|
17
|
+
export { evaluateConditions } from './resolver.js';
|
|
18
|
+
export { insertStatusRow, loadStatusRow, updateStatusRow, queryStatusRows, findEventStepRow, } from './status.js';
|
|
19
|
+
export { dispatchStep } from './dispatch.js';
|
|
20
|
+
export { lifecycleDefaultTemplates } from './default_templates.js';
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -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"}
|