@hogsend/cli 0.2.0 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogsend/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "tsup": "^8.5.1",
33
33
  "tsx": "^4.22.4",
34
34
  "vitest": "^4.1.7",
35
- "@hogsend/studio": "^0.3.0",
35
+ "@hogsend/studio": "^0.4.0",
36
36
  "@repo/typescript-config": "0.0.0"
37
37
  },
38
38
  "engines": {
@@ -59,7 +59,7 @@ export const welcome = defineJourney({
59
59
  ## Key concepts
60
60
 
61
61
  - **`ctx` is orchestration primitives ONLY** — `sleep`, `sleepUntil`, `when`,
62
- `checkpoint`, `trigger`, `identify`, `guard.isSubscribed`,
62
+ `waitForEvent`, `checkpoint`, `trigger`, `identify`, `guard.isSubscribed`,
63
63
  `history.hasEvent/journey/email`, `posthog.capture`. Features are standalone
64
64
  imports: `sendEmail()` and `getPostHog()` come from `@hogsend/engine`, NOT off
65
65
  `ctx`.
@@ -63,6 +63,30 @@ export const activation = defineJourney({
63
63
  after a long `ctx.sleep` — a user can unsubscribe during the wait. Enrollment
64
64
  only checks preferences at entry, not at each send.
65
65
 
66
+ ## Reactive alternative: `ctx.waitForEvent`
67
+
68
+ The pattern above sleeps a **fixed window** then polls history — good when you
69
+ want to wait a set time regardless. When you're waiting **for a specific event to
70
+ happen** (and want to react the moment it does, or give up after a deadline),
71
+ `ctx.waitForEvent` is the sharper tool: it resumes the instant the user fires the
72
+ event, or returns `timedOut: true` when the timeout wins.
73
+
74
+ ```ts
75
+ const { timedOut } = await ctx.waitForEvent({
76
+ event: Events.FEATURE_USED,
77
+ timeout: days(7), // required; capped at the 720h task limit
78
+ label: "await-activation",
79
+ });
80
+ if (!timedOut) return; // they activated on their own — done
81
+ if (!(await ctx.guard.isSubscribed())) return;
82
+ await sendEmail({ /* nudge */ });
83
+ ```
84
+
85
+ Rule of thumb: **fixed delay → `ctx.sleep` then `ctx.history.hasEvent`**;
86
+ **wait until they do X (or time out) → `ctx.waitForEvent`**. The wait is
87
+ forward-looking (only events after it begins count), and an `exitOn` match
88
+ cancels it mid-wait, so no post-wait send fires after the journey exits.
89
+
66
90
  ## Idempotency — journeys can replay
67
91
 
68
92
  A journey task is durable, and a step before a `ctx.sleep` can be re-executed if
@@ -29,6 +29,29 @@ await ctx.sleepUntil(at, { label: "morning-nudge" });
29
29
  // .in(days(3)).at("HH:mm"), and chainers .tz(zone) / .window(start,end) / .ifPast("next"|"now")
30
30
  ```
31
31
 
32
+ ## Durable wait-for-event
33
+
34
+ ```ts
35
+ // Park the journey until THIS user emits `event`, OR `timeout` elapses —
36
+ // whichever first. The reactive alternative to "sleep a fixed window, then poll
37
+ // ctx.history": it resumes the INSTANT the event lands. Forward-looking — only
38
+ // events fired AFTER the wait begins count (use ctx.history.hasEvent for the past).
39
+ const { timedOut } = await ctx.waitForEvent({
40
+ event: Events.FEATURE_USED,
41
+ timeout: days(7), // REQUIRED, capped at the 720h task execution limit
42
+ label: "await-activation", // optional — written as currentNodeId
43
+ });
44
+ if (timedOut) {
45
+ // they never did it — nudge (re-check ctx.guard.isSubscribed() first after a long wait)
46
+ } else {
47
+ // event arrived — they activated on their own
48
+ }
49
+ ```
50
+
51
+ If the journey `exitOn`-matches (or is cancelled) WHILE waiting, the run aborts
52
+ cleanly — state goes `"exited"`, the durable run is cancelled, and no post-wait
53
+ step (or email) fires. You don't catch anything; the engine handles it.
54
+
32
55
  ## Observability
33
56
 
34
57
  ```ts
@@ -129,13 +129,16 @@ overlapping runs of the same journey.
129
129
  A `journeyStates` row tracks each run. Once the gates pass:
130
130
 
131
131
  - **enter** → row created with `status: "active"`, `currentNodeId: "start"`.
132
- - **`ctx.sleep` / `ctx.sleepUntil`** → `status: "waiting"` for the duration, back
133
- to `"active"` on resume.
132
+ - **`ctx.sleep` / `ctx.sleepUntil` / `ctx.waitForEvent`** → `status: "waiting"`
133
+ while suspended, back to `"active"` on resume.
134
134
  - **`run()` returns** → `status: "completed"`, `completedAt` set, and a
135
135
  `journey:completed` event is pushed.
136
136
  - **`run()` throws** → `status: "failed"`, `errorMessage` recorded, and a
137
137
  `journey:failed` event is pushed; the error re-throws so Hatchet sees the
138
138
  failure.
139
+ - **`exitOn` matches (or cancelled)** → `status: "exited"`. If it happens while
140
+ the journey is suspended in a `ctx.sleep`/`ctx.waitForEvent`, the durable run
141
+ is cancelled so no further step runs — even mid-wait.
139
142
 
140
143
  Because the gates run before any state is created, a skipped event is invisible
141
144
  in `journeyStates` — to debug "why didn't this user enroll?", check the gate