@cowliss/cli 0.7.0 → 0.9.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/dist/guest/driver-emails.d.ts +12 -0
- package/dist/guest/{driver-C1bsCjZT.js → driver-emails.js} +11 -312
- package/dist/guest/driver-poSdZIj8.js +320 -0
- package/dist/guest/driver.d.ts +19 -6
- package/dist/guest/driver.js +2 -2
- package/dist/guest/emails-5GindgMQ.d.ts +47 -0
- package/dist/guest/emails.d.ts +1 -46
- package/dist/guest/{index-3PMqJKmc.d.ts → index-EqBCZnpq.d.ts} +11 -12
- package/dist/guest/{journeys-Dpsp225V.js → journeys-F8Yk8s1P.js} +93 -45
- package/dist/guest/journeys.d.ts +13 -8
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.d.ts +4 -2
- package/dist/guest/wasi.js +1 -3
- package/dist/index.js +546 -354
- package/package.json +17 -1
- package/examples/cross-app-pitch/emails/cross-app-pitch.tsx +0 -77
- package/examples/cross-app-pitch/journeys/cross-app-pitch.ts +0 -39
- package/examples/cross-app-pitch/scenarios/cross-app-pitch.json +0 -17
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GuestModule, GuestOutput } from "./driver.js";
|
|
2
|
+
//#region src/guest/driver-emails.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The template half of the driver (spec: Guest protocol). It answers the
|
|
5
|
+
* two inputs a bundle built from `emails/` can get, `manifest` and
|
|
6
|
+
* `template`, and it is the only guest entry that imports `render.ts`:
|
|
7
|
+
* react and react-dom reach a bundle through this file or not at all.
|
|
8
|
+
* `driver.ts` is the journey half and holds everything both kinds share.
|
|
9
|
+
*/
|
|
10
|
+
declare function runGuest(module: GuestModule, input: unknown): Promise<GuestOutput>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { runGuest };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
|
|
2
|
+
import { n as readManifest, t as asTemplate } from "./driver-poSdZIj8.js";
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
import { renderToStaticMarkup } from "react-dom/server.browser";
|
|
5
5
|
|
|
@@ -30,154 +30,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
enumerable: true
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/guest/api.ts
|
|
35
|
-
/**
|
|
36
|
-
* The journaled capability implementation (spec: Guest protocol). Every
|
|
37
|
-
* `api` method funnels into `call`, which either replays the matching
|
|
38
|
-
* journal entry or records the pending call and hangs, letting the driver
|
|
39
|
-
* report the suspension.
|
|
40
|
-
*/
|
|
41
|
-
/** A replayed call did not match the journal: the execution cannot continue. */
|
|
42
|
-
var NondeterminismError = class extends Error {
|
|
43
|
-
expected;
|
|
44
|
-
actual;
|
|
45
|
-
constructor(expected, actual) {
|
|
46
|
-
super(`Journey replay diverged: the journal recorded "${expected.name}" but the code called "${actual.name}" with different arguments`);
|
|
47
|
-
this.name = "NondeterminismError";
|
|
48
|
-
this.expected = expected;
|
|
49
|
-
this.actual = actual;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Structural equality over JSON values. Key order differs between a
|
|
54
|
-
* literal in journey code and the same object round-tripped through the
|
|
55
|
-
* journal, so comparing serialized forms would report a divergence that
|
|
56
|
-
* is not one.
|
|
57
|
-
*/
|
|
58
|
-
function sameValue(a, b) {
|
|
59
|
-
if (a === b) return true;
|
|
60
|
-
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
|
|
61
|
-
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
62
|
-
const left = a;
|
|
63
|
-
const right = b;
|
|
64
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
65
|
-
for (const key of keys) if (!sameValue(left[key], right[key])) return false;
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
function formatLogArg(value) {
|
|
69
|
-
if (typeof value === "string") return value;
|
|
70
|
-
try {
|
|
71
|
-
return JSON.stringify(value) ?? String(value);
|
|
72
|
-
} catch {
|
|
73
|
-
return String(value);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Builds the `api` a journey's `run` receives, over the step's journal.
|
|
78
|
-
* The returned state is the driver's window into the run: the pending
|
|
79
|
-
* call, the captured logs, and the clock the shims read.
|
|
80
|
-
*/
|
|
81
|
-
function createApi(input, journey) {
|
|
82
|
-
let cursor = 0;
|
|
83
|
-
let markSuspended = () => {};
|
|
84
|
-
const state = {
|
|
85
|
-
clock: { now: input.now },
|
|
86
|
-
logs: [],
|
|
87
|
-
pending: null,
|
|
88
|
-
suspended: new Promise((resolve) => {
|
|
89
|
-
markSuspended = resolve;
|
|
90
|
-
})
|
|
91
|
-
};
|
|
92
|
-
function call(command) {
|
|
93
|
-
if (command.name !== "restart" && cursor >= input.limits.calls) throw new CapabilityError("call_limit_exceeded", `Journey exceeded the per-execution limit of ${input.limits.calls} capability calls; use api.restart() for loops`);
|
|
94
|
-
const entry = input.journal[cursor];
|
|
95
|
-
if (!entry) {
|
|
96
|
-
if (!state.pending) {
|
|
97
|
-
state.pending = command;
|
|
98
|
-
markSuspended();
|
|
99
|
-
}
|
|
100
|
-
return new Promise(() => {});
|
|
101
|
-
}
|
|
102
|
-
if (entry.call.name !== command.name || !sameValue(entry.call.args, command.args)) throw new NondeterminismError(entry.call, command);
|
|
103
|
-
cursor += 1;
|
|
104
|
-
state.clock.now = cursor < input.journal.length ? entry.at : input.now;
|
|
105
|
-
if (entry.result.ok) return Promise.resolve(entry.result.value);
|
|
106
|
-
return Promise.reject(new CapabilityError(entry.result.error.code, entry.result.error.message));
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
api: {
|
|
110
|
-
sleep: (duration) => call({
|
|
111
|
-
name: "sleep",
|
|
112
|
-
args: { duration }
|
|
113
|
-
}),
|
|
114
|
-
waitForEvent: (name, options) => call({
|
|
115
|
-
name: "waitForEvent",
|
|
116
|
-
args: {
|
|
117
|
-
event: name,
|
|
118
|
-
timeout: options.timeout
|
|
119
|
-
}
|
|
120
|
-
}),
|
|
121
|
-
send: {
|
|
122
|
-
email: (args) => call({
|
|
123
|
-
name: "send.email",
|
|
124
|
-
args: {
|
|
125
|
-
template: args.template,
|
|
126
|
-
props: args.props,
|
|
127
|
-
from: args.from ?? journey.from,
|
|
128
|
-
replyTo: args.replyTo
|
|
129
|
-
}
|
|
130
|
-
}),
|
|
131
|
-
webhook: (args) => call({
|
|
132
|
-
name: "send.webhook",
|
|
133
|
-
args
|
|
134
|
-
})
|
|
135
|
-
},
|
|
136
|
-
traits: {
|
|
137
|
-
set: (key, value) => call({
|
|
138
|
-
name: "traits.set",
|
|
139
|
-
args: {
|
|
140
|
-
key,
|
|
141
|
-
value
|
|
142
|
-
}
|
|
143
|
-
}),
|
|
144
|
-
unset: (key) => call({
|
|
145
|
-
name: "traits.unset",
|
|
146
|
-
args: { key }
|
|
147
|
-
})
|
|
148
|
-
},
|
|
149
|
-
profile: { get: () => call({
|
|
150
|
-
name: "profile.get",
|
|
151
|
-
args: {}
|
|
152
|
-
}) },
|
|
153
|
-
profiles: { get: (id) => call({
|
|
154
|
-
name: "profiles.get",
|
|
155
|
-
args: { id }
|
|
156
|
-
}) },
|
|
157
|
-
events: { track: (name, properties) => call({
|
|
158
|
-
name: "events.track",
|
|
159
|
-
args: {
|
|
160
|
-
event: name,
|
|
161
|
-
properties
|
|
162
|
-
}
|
|
163
|
-
}) },
|
|
164
|
-
log: (...args) => {
|
|
165
|
-
if (cursor < input.journal.length) return;
|
|
166
|
-
if (state.logs.length >= input.limits.logLines) return;
|
|
167
|
-
state.logs.push({
|
|
168
|
-
level: "info",
|
|
169
|
-
message: args.map(formatLogArg).join(" ").slice(0, input.limits.logLineBytes)
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
restart: (args) => call({
|
|
173
|
-
name: "restart",
|
|
174
|
-
args: { event: args?.event }
|
|
175
|
-
})
|
|
176
|
-
},
|
|
177
|
-
state
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
33
|
//#endregion
|
|
182
34
|
//#region ../../node_modules/.pnpm/domelementtype@2.3.0/node_modules/domelementtype/lib/esm/index.js
|
|
183
35
|
/** Types of elements found in htmlparser2's DOM */
|
|
@@ -5482,173 +5334,20 @@ function renderTemplateModule(template, props) {
|
|
|
5482
5334
|
}
|
|
5483
5335
|
|
|
5484
5336
|
//#endregion
|
|
5485
|
-
//#region src/guest/
|
|
5337
|
+
//#region src/guest/driver-emails.ts
|
|
5486
5338
|
/**
|
|
5487
|
-
*
|
|
5488
|
-
*
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
5493
|
-
hash ^= seed.charCodeAt(index);
|
|
5494
|
-
hash = Math.imul(hash, 16777619);
|
|
5495
|
-
}
|
|
5496
|
-
return hash >>> 0;
|
|
5497
|
-
}
|
|
5498
|
-
/** mulberry32: small, fast, and good enough for journey branching. */
|
|
5499
|
-
function mulberry32(seed) {
|
|
5500
|
-
let state = hashSeed(seed);
|
|
5501
|
-
return () => {
|
|
5502
|
-
state = state + 1831565813 | 0;
|
|
5503
|
-
let value = Math.imul(state ^ state >>> 15, 1 | state);
|
|
5504
|
-
value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
|
|
5505
|
-
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
5506
|
-
};
|
|
5507
|
-
}
|
|
5508
|
-
const BLOCKED = [
|
|
5509
|
-
"setTimeout",
|
|
5510
|
-
"setInterval",
|
|
5511
|
-
"setImmediate",
|
|
5512
|
-
"fetch",
|
|
5513
|
-
"XMLHttpRequest",
|
|
5514
|
-
"queueMicrotask"
|
|
5515
|
-
];
|
|
5516
|
-
function blocked(name) {
|
|
5517
|
-
return () => {
|
|
5518
|
-
throw new CapabilityError("capability_unavailable", `${name} is not available in journeys: use the api object, which is journaled and replays exactly`);
|
|
5519
|
-
};
|
|
5520
|
-
}
|
|
5521
|
-
/**
|
|
5522
|
-
* Installs the shims and returns the undo. Call it before user code runs
|
|
5523
|
-
* and restore in a `finally`, so a driver embedded in a Node process does
|
|
5524
|
-
* not leak a frozen clock into its host.
|
|
5525
|
-
*/
|
|
5526
|
-
function installShims(options) {
|
|
5527
|
-
const globals = globalThis;
|
|
5528
|
-
const saved = /* @__PURE__ */ new Map();
|
|
5529
|
-
const save = (name) => {
|
|
5530
|
-
saved.set(name, name in globals ? globals[name] : void 0);
|
|
5531
|
-
};
|
|
5532
|
-
const RealDate = Date;
|
|
5533
|
-
class GuestDate extends RealDate {
|
|
5534
|
-
constructor(...args) {
|
|
5535
|
-
if (args.length === 0) super(options.clock.now);
|
|
5536
|
-
else super(...args);
|
|
5537
|
-
}
|
|
5538
|
-
static now() {
|
|
5539
|
-
return options.clock.now;
|
|
5540
|
-
}
|
|
5541
|
-
}
|
|
5542
|
-
save("Date");
|
|
5543
|
-
globals.Date = GuestDate;
|
|
5544
|
-
const realRandom = Math.random;
|
|
5545
|
-
Math.random = mulberry32(options.seed);
|
|
5546
|
-
for (const name of BLOCKED) {
|
|
5547
|
-
save(name);
|
|
5548
|
-
globals[name] = blocked(name);
|
|
5549
|
-
}
|
|
5550
|
-
return () => {
|
|
5551
|
-
Math.random = realRandom;
|
|
5552
|
-
for (const [name, value] of saved) if (value === void 0) delete globals[name];
|
|
5553
|
-
else globals[name] = value;
|
|
5554
|
-
};
|
|
5555
|
-
}
|
|
5556
|
-
|
|
5557
|
-
//#endregion
|
|
5558
|
-
//#region src/guest/driver.ts
|
|
5559
|
-
function asJourney(module) {
|
|
5560
|
-
const journey = module.default;
|
|
5561
|
-
if (!journey || typeof journey.run !== "function") throw new Error("Journey module must default-export defineJourney({ ... }) with a run function");
|
|
5562
|
-
return journey;
|
|
5563
|
-
}
|
|
5564
|
-
function asTemplate(module) {
|
|
5565
|
-
const template = module;
|
|
5566
|
-
if (typeof template.default !== "function" || !template.props) throw new Error("Template module must export a default component and a zod `props` schema");
|
|
5567
|
-
return template;
|
|
5568
|
-
}
|
|
5569
|
-
function readManifest(module) {
|
|
5570
|
-
if (module.default && typeof module.default.run === "function") {
|
|
5571
|
-
const journey = asJourney(module);
|
|
5572
|
-
return manifestOutputSchema.parse({
|
|
5573
|
-
kind: "journey",
|
|
5574
|
-
trigger: journey.trigger,
|
|
5575
|
-
purpose: journey.purpose,
|
|
5576
|
-
enrollment: journey.enrollment,
|
|
5577
|
-
description: journey.description,
|
|
5578
|
-
from: journey.from,
|
|
5579
|
-
tags: journey.tags
|
|
5580
|
-
});
|
|
5581
|
-
}
|
|
5582
|
-
const template = asTemplate(module);
|
|
5583
|
-
return manifestOutputSchema.parse({
|
|
5584
|
-
kind: "template",
|
|
5585
|
-
tags: template.tags,
|
|
5586
|
-
sendClass: template.sendClass ?? "marketing",
|
|
5587
|
-
verifyLink: template.verifyLink ?? false,
|
|
5588
|
-
unsubscribeLink: template.unsubscribeLink ?? false,
|
|
5589
|
-
propsSchema: z.toJSONSchema(template.props, { target: "draft-2020-12" })
|
|
5590
|
-
});
|
|
5591
|
-
}
|
|
5592
|
-
async function runJourneyStep(module, input) {
|
|
5593
|
-
const journey = asJourney(module);
|
|
5594
|
-
const { api, state } = createApi(input, journey);
|
|
5595
|
-
const restore = installShims({
|
|
5596
|
-
clock: state.clock,
|
|
5597
|
-
seed: input.seed
|
|
5598
|
-
});
|
|
5599
|
-
try {
|
|
5600
|
-
if (await Promise.race([journey.run(input.event, api).then(() => true), state.suspended.then(() => false)])) return journeyStepOutput({
|
|
5601
|
-
status: "done",
|
|
5602
|
-
logs: state.logs
|
|
5603
|
-
});
|
|
5604
|
-
return journeyStepOutput({
|
|
5605
|
-
status: "suspend",
|
|
5606
|
-
call: state.pending,
|
|
5607
|
-
logs: state.logs
|
|
5608
|
-
});
|
|
5609
|
-
} catch (error) {
|
|
5610
|
-
if (error instanceof NondeterminismError) return journeyStepOutput({
|
|
5611
|
-
status: "error",
|
|
5612
|
-
error: {
|
|
5613
|
-
code: "journey_nondeterministic",
|
|
5614
|
-
message: error.message
|
|
5615
|
-
},
|
|
5616
|
-
nondeterministic: {
|
|
5617
|
-
expected: error.expected,
|
|
5618
|
-
actual: error.actual
|
|
5619
|
-
},
|
|
5620
|
-
logs: state.logs
|
|
5621
|
-
});
|
|
5622
|
-
const thrown = error instanceof Error ? error : new Error(String(error));
|
|
5623
|
-
return journeyStepOutput({
|
|
5624
|
-
status: "error",
|
|
5625
|
-
error: {
|
|
5626
|
-
code: "journey_error",
|
|
5627
|
-
message: thrown.message,
|
|
5628
|
-
stack: thrown.stack
|
|
5629
|
-
},
|
|
5630
|
-
logs: state.logs
|
|
5631
|
-
});
|
|
5632
|
-
} finally {
|
|
5633
|
-
restore();
|
|
5634
|
-
}
|
|
5635
|
-
}
|
|
5636
|
-
function journeyStepOutput(output) {
|
|
5637
|
-
return journeyStepOutputSchema.parse(output);
|
|
5638
|
-
}
|
|
5639
|
-
/**
|
|
5640
|
-
* Runs one guest invocation. `module` is the tenant module namespace the
|
|
5641
|
-
* bundle's entry imported; `input` is the raw JSON the host handed over.
|
|
5339
|
+
* The template half of the driver (spec: Guest protocol). It answers the
|
|
5340
|
+
* two inputs a bundle built from `emails/` can get, `manifest` and
|
|
5341
|
+
* `template`, and it is the only guest entry that imports `render.ts`:
|
|
5342
|
+
* react and react-dom reach a bundle through this file or not at all.
|
|
5343
|
+
* `driver.ts` is the journey half and holds everything both kinds share.
|
|
5642
5344
|
*/
|
|
5643
5345
|
async function runGuest(module, input) {
|
|
5644
5346
|
const parsed = guestInputSchema.parse(input);
|
|
5645
5347
|
if (parsed.kind === "manifest") return readManifest(module);
|
|
5646
|
-
if (parsed.kind === "
|
|
5647
|
-
return
|
|
5648
|
-
}
|
|
5649
|
-
function renderTemplate(module, input) {
|
|
5650
|
-
return templateRenderOutputSchema.parse(renderTemplateModule(asTemplate(module), input.props));
|
|
5348
|
+
if (parsed.kind === "journey") throw new Error("A template bundle cannot run a journey step.");
|
|
5349
|
+
return templateRenderOutputSchema.parse(renderTemplateModule(asTemplate(module), parsed.props));
|
|
5651
5350
|
}
|
|
5652
5351
|
|
|
5653
5352
|
//#endregion
|
|
5654
|
-
export { runGuest
|
|
5353
|
+
export { runGuest };
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { a as manifestOutputSchema, i as journeyStepOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/guest/api.ts
|
|
5
|
+
/**
|
|
6
|
+
* The journaled capability implementation (spec: Guest protocol). Every
|
|
7
|
+
* `api` method funnels into `call`, which either replays the matching
|
|
8
|
+
* journal entry or records the pending call and hangs, letting the driver
|
|
9
|
+
* report the suspension.
|
|
10
|
+
*/
|
|
11
|
+
/** A replayed call did not match the journal: the execution cannot continue. */
|
|
12
|
+
var NondeterminismError = class extends Error {
|
|
13
|
+
expected;
|
|
14
|
+
actual;
|
|
15
|
+
constructor(expected, actual) {
|
|
16
|
+
super(`Journey replay diverged: the journal recorded "${expected.name}" but the code called "${actual.name}" with different arguments`);
|
|
17
|
+
this.name = "NondeterminismError";
|
|
18
|
+
this.expected = expected;
|
|
19
|
+
this.actual = actual;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Structural equality over JSON values. Key order differs between a
|
|
24
|
+
* literal in journey code and the same object round-tripped through the
|
|
25
|
+
* journal, so comparing serialized forms would report a divergence that
|
|
26
|
+
* is not one.
|
|
27
|
+
*/
|
|
28
|
+
function sameValue(a, b) {
|
|
29
|
+
if (a === b) return true;
|
|
30
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
|
|
31
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
32
|
+
const left = a;
|
|
33
|
+
const right = b;
|
|
34
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
35
|
+
for (const key of keys) if (!sameValue(left[key], right[key])) return false;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function formatLogArg(value) {
|
|
39
|
+
if (typeof value === "string") return value;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(value) ?? String(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Builds the `api` a journey's `run` receives, over the step's journal.
|
|
48
|
+
* The returned state is the driver's window into the run: the pending
|
|
49
|
+
* call, the captured logs, and the clock the shims read.
|
|
50
|
+
*/
|
|
51
|
+
function createApi(input, journey) {
|
|
52
|
+
let cursor = 0;
|
|
53
|
+
let markSuspended = () => {};
|
|
54
|
+
const state = {
|
|
55
|
+
clock: { now: input.now },
|
|
56
|
+
logs: [],
|
|
57
|
+
pending: null,
|
|
58
|
+
suspended: new Promise((resolve) => {
|
|
59
|
+
markSuspended = resolve;
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
function call(command) {
|
|
63
|
+
if (command.name !== "restart" && cursor >= input.limits.calls) throw new CapabilityError("call_limit_exceeded", `Journey exceeded the per-execution limit of ${input.limits.calls} capability calls; use api.restart() for loops`);
|
|
64
|
+
const entry = input.journal[cursor];
|
|
65
|
+
if (!entry) {
|
|
66
|
+
if (!state.pending) {
|
|
67
|
+
state.pending = command;
|
|
68
|
+
markSuspended();
|
|
69
|
+
}
|
|
70
|
+
return new Promise(() => {});
|
|
71
|
+
}
|
|
72
|
+
if (entry.call.name !== command.name || !sameValue(entry.call.args, command.args)) throw new NondeterminismError(entry.call, command);
|
|
73
|
+
cursor += 1;
|
|
74
|
+
state.clock.now = cursor < input.journal.length ? entry.at : input.now;
|
|
75
|
+
if (entry.result.ok) return Promise.resolve(entry.result.value);
|
|
76
|
+
return Promise.reject(new CapabilityError(entry.result.error.code, entry.result.error.message));
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
api: {
|
|
80
|
+
sleep: (duration) => call({
|
|
81
|
+
name: "sleep",
|
|
82
|
+
args: { duration }
|
|
83
|
+
}),
|
|
84
|
+
waitForEvent: (name, options) => call({
|
|
85
|
+
name: "waitForEvent",
|
|
86
|
+
args: {
|
|
87
|
+
event: name,
|
|
88
|
+
timeout: options.timeout
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
send: {
|
|
92
|
+
email: (args) => call({
|
|
93
|
+
name: "send.email",
|
|
94
|
+
args: {
|
|
95
|
+
template: args.template,
|
|
96
|
+
props: args.props,
|
|
97
|
+
from: args.from ?? journey.from,
|
|
98
|
+
replyTo: args.replyTo
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
webhook: (args) => call({
|
|
102
|
+
name: "send.webhook",
|
|
103
|
+
args
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
traits: {
|
|
107
|
+
set: (key, value) => call({
|
|
108
|
+
name: "traits.set",
|
|
109
|
+
args: {
|
|
110
|
+
key,
|
|
111
|
+
value
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
unset: (key) => call({
|
|
115
|
+
name: "traits.unset",
|
|
116
|
+
args: { key }
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
profile: { get: () => call({
|
|
120
|
+
name: "profile.get",
|
|
121
|
+
args: {}
|
|
122
|
+
}) },
|
|
123
|
+
profiles: { get: (id) => call({
|
|
124
|
+
name: "profiles.get",
|
|
125
|
+
args: { id }
|
|
126
|
+
}) },
|
|
127
|
+
events: { track: (name, properties) => call({
|
|
128
|
+
name: "events.track",
|
|
129
|
+
args: {
|
|
130
|
+
event: name,
|
|
131
|
+
properties
|
|
132
|
+
}
|
|
133
|
+
}) },
|
|
134
|
+
log: (...args) => {
|
|
135
|
+
if (cursor < input.journal.length) return;
|
|
136
|
+
if (state.logs.length >= input.limits.logLines) return;
|
|
137
|
+
state.logs.push({
|
|
138
|
+
level: "info",
|
|
139
|
+
message: args.map(formatLogArg).join(" ").slice(0, input.limits.logLineBytes)
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
restart: (args) => call({
|
|
143
|
+
name: "restart",
|
|
144
|
+
args: { event: args?.event }
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
state
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/guest/shims.ts
|
|
153
|
+
/**
|
|
154
|
+
* A hash of the seed string into a 32-bit state (FNV-1a). The seed derives
|
|
155
|
+
* from the execution id, so the same execution replays the same numbers.
|
|
156
|
+
*/
|
|
157
|
+
function hashSeed(seed) {
|
|
158
|
+
let hash = 2166136261;
|
|
159
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
160
|
+
hash ^= seed.charCodeAt(index);
|
|
161
|
+
hash = Math.imul(hash, 16777619);
|
|
162
|
+
}
|
|
163
|
+
return hash >>> 0;
|
|
164
|
+
}
|
|
165
|
+
/** mulberry32: small, fast, and good enough for journey branching. */
|
|
166
|
+
function mulberry32(seed) {
|
|
167
|
+
let state = hashSeed(seed);
|
|
168
|
+
return () => {
|
|
169
|
+
state = state + 1831565813 | 0;
|
|
170
|
+
let value = Math.imul(state ^ state >>> 15, 1 | state);
|
|
171
|
+
value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
|
|
172
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const BLOCKED = [
|
|
176
|
+
"setTimeout",
|
|
177
|
+
"setInterval",
|
|
178
|
+
"setImmediate",
|
|
179
|
+
"fetch",
|
|
180
|
+
"XMLHttpRequest",
|
|
181
|
+
"queueMicrotask"
|
|
182
|
+
];
|
|
183
|
+
function blocked(name) {
|
|
184
|
+
return () => {
|
|
185
|
+
throw new CapabilityError("capability_unavailable", `${name} is not available in journeys: use the api object, which is journaled and replays exactly`);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Installs the shims and returns the undo. Call it before user code runs
|
|
190
|
+
* and restore in a `finally`, so a driver embedded in a Node process does
|
|
191
|
+
* not leak a frozen clock into its host.
|
|
192
|
+
*/
|
|
193
|
+
function installShims(options) {
|
|
194
|
+
const globals = globalThis;
|
|
195
|
+
const saved = /* @__PURE__ */ new Map();
|
|
196
|
+
const save = (name) => {
|
|
197
|
+
saved.set(name, name in globals ? globals[name] : void 0);
|
|
198
|
+
};
|
|
199
|
+
const RealDate = Date;
|
|
200
|
+
class GuestDate extends RealDate {
|
|
201
|
+
constructor(...args) {
|
|
202
|
+
if (args.length === 0) super(options.clock.now);
|
|
203
|
+
else super(...args);
|
|
204
|
+
}
|
|
205
|
+
static now() {
|
|
206
|
+
return options.clock.now;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
save("Date");
|
|
210
|
+
globals.Date = GuestDate;
|
|
211
|
+
const realRandom = Math.random;
|
|
212
|
+
Math.random = mulberry32(options.seed);
|
|
213
|
+
for (const name of BLOCKED) {
|
|
214
|
+
save(name);
|
|
215
|
+
globals[name] = blocked(name);
|
|
216
|
+
}
|
|
217
|
+
return () => {
|
|
218
|
+
Math.random = realRandom;
|
|
219
|
+
for (const [name, value] of saved) if (value === void 0) delete globals[name];
|
|
220
|
+
else globals[name] = value;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/guest/driver.ts
|
|
226
|
+
function asJourney(module) {
|
|
227
|
+
const journey = module.default;
|
|
228
|
+
if (!journey || typeof journey.run !== "function") throw new Error("Journey module must default-export defineJourney({ ... }) with a run function");
|
|
229
|
+
return journey;
|
|
230
|
+
}
|
|
231
|
+
function asTemplate(module) {
|
|
232
|
+
const template = module;
|
|
233
|
+
if (typeof template.default !== "function" || !template.props) throw new Error("Template module must export a default component and a zod `props` schema");
|
|
234
|
+
return template;
|
|
235
|
+
}
|
|
236
|
+
function readManifest(module) {
|
|
237
|
+
if (module.default && typeof module.default.run === "function") {
|
|
238
|
+
const journey = asJourney(module);
|
|
239
|
+
return manifestOutputSchema.parse({
|
|
240
|
+
kind: "journey",
|
|
241
|
+
trigger: journey.trigger,
|
|
242
|
+
purpose: journey.purpose,
|
|
243
|
+
enrollment: journey.enrollment,
|
|
244
|
+
description: journey.description,
|
|
245
|
+
from: journey.from,
|
|
246
|
+
tags: journey.tags
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const template = asTemplate(module);
|
|
250
|
+
return manifestOutputSchema.parse({
|
|
251
|
+
kind: "template",
|
|
252
|
+
tags: template.tags,
|
|
253
|
+
sendClass: template.sendClass ?? "marketing",
|
|
254
|
+
verifyLink: template.verifyLink ?? false,
|
|
255
|
+
unsubscribeLink: template.unsubscribeLink ?? false,
|
|
256
|
+
propsSchema: z.toJSONSchema(template.props, { target: "draft-2020-12" })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function runJourneyStep(module, input) {
|
|
260
|
+
const journey = asJourney(module);
|
|
261
|
+
const { api, state } = createApi(input, journey);
|
|
262
|
+
const restore = installShims({
|
|
263
|
+
clock: state.clock,
|
|
264
|
+
seed: input.seed
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
if (await Promise.race([journey.run(input.event, api).then(() => true), state.suspended.then(() => false)])) return journeyStepOutput({
|
|
268
|
+
status: "done",
|
|
269
|
+
logs: state.logs
|
|
270
|
+
});
|
|
271
|
+
return journeyStepOutput({
|
|
272
|
+
status: "suspend",
|
|
273
|
+
call: state.pending,
|
|
274
|
+
logs: state.logs
|
|
275
|
+
});
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error instanceof NondeterminismError) return journeyStepOutput({
|
|
278
|
+
status: "error",
|
|
279
|
+
error: {
|
|
280
|
+
code: "journey_nondeterministic",
|
|
281
|
+
message: error.message
|
|
282
|
+
},
|
|
283
|
+
nondeterministic: {
|
|
284
|
+
expected: error.expected,
|
|
285
|
+
actual: error.actual
|
|
286
|
+
},
|
|
287
|
+
logs: state.logs
|
|
288
|
+
});
|
|
289
|
+
const thrown = error instanceof Error ? error : new Error(String(error));
|
|
290
|
+
return journeyStepOutput({
|
|
291
|
+
status: "error",
|
|
292
|
+
error: {
|
|
293
|
+
code: "journey_error",
|
|
294
|
+
message: thrown.message,
|
|
295
|
+
stack: thrown.stack
|
|
296
|
+
},
|
|
297
|
+
logs: state.logs
|
|
298
|
+
});
|
|
299
|
+
} finally {
|
|
300
|
+
restore();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function journeyStepOutput(output) {
|
|
304
|
+
return journeyStepOutputSchema.parse(output);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Runs one guest invocation against a journey bundle. `module` is the tenant
|
|
308
|
+
* module namespace the bundle's entry imported; `input` is the raw JSON the
|
|
309
|
+
* host handed over. A `template` input is a host bug, not a tenant one: this
|
|
310
|
+
* bundle was built from `journeys/` and carries no renderer.
|
|
311
|
+
*/
|
|
312
|
+
async function runGuest(module, input) {
|
|
313
|
+
const parsed = guestInputSchema.parse(input);
|
|
314
|
+
if (parsed.kind === "manifest") return readManifest(module);
|
|
315
|
+
if (parsed.kind === "template") throw new Error("A journey bundle cannot render a template.");
|
|
316
|
+
return runJourneyStep(module, parsed);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
//#endregion
|
|
320
|
+
export { readManifest as n, runGuest as r, asTemplate as t };
|