@shware/workflow 7.0.0 → 8.0.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/README.md +121 -34
- package/dist/aws/engine-step.cjs +4 -1
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +4 -1
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/client.cjs +66 -41
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +72 -22
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +72 -22
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +66 -42
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/index.d.cts +4 -3
- package/dist/cloudflare/index.d.mts +4 -3
- package/dist/cloudflare/runner.cjs +3 -1
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +3 -1
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +12 -1
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +9 -1
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +9 -1
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +12 -1
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +4 -4
- package/dist/contract.d.mts +4 -4
- package/dist/contract.mjs.map +1 -1
- package/dist/dsl/trigger.cjs +2 -1
- package/dist/dsl/trigger.cjs.map +1 -1
- package/dist/dsl/trigger.d.cts +6 -0
- package/dist/dsl/trigger.d.cts.map +1 -1
- package/dist/dsl/trigger.d.mts +6 -0
- package/dist/dsl/trigger.d.mts.map +1 -1
- package/dist/dsl/trigger.mjs +2 -1
- package/dist/dsl/trigger.mjs.map +1 -1
- package/dist/engine/condition.cjs +3 -3
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +3 -3
- package/dist/engine/condition.d.mts +3 -3
- package/dist/engine/condition.mjs +3 -3
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/interpreter.cjs +9 -5
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +9 -5
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +2 -0
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +2 -0
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/host/batch.cjs +172 -0
- package/dist/host/batch.cjs.map +1 -0
- package/dist/host/batch.d.cts +88 -0
- package/dist/host/batch.d.cts.map +1 -0
- package/dist/host/batch.d.mts +88 -0
- package/dist/host/batch.d.mts.map +1 -0
- package/dist/host/batch.mjs +163 -0
- package/dist/host/batch.mjs.map +1 -0
- package/dist/host/execute.cjs +10 -6
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.mjs +10 -6
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/http.cjs +30 -89
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +1 -1
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +1 -1
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +24 -83
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/ingest.cjs +56 -13
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +17 -6
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +17 -6
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +55 -14
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/host/journey-host.cjs +6 -0
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +5 -2
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +5 -2
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +6 -0
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs.map +1 -1
- package/dist/host/platform.d.cts +2 -0
- package/dist/host/platform.d.cts.map +1 -1
- package/dist/host/platform.d.mts +2 -0
- package/dist/host/platform.d.mts.map +1 -1
- package/dist/host/platform.mjs.map +1 -1
- package/dist/ir.cjs +2 -1
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -0
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -0
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -1
- package/dist/ir.mjs.map +1 -1
- package/dist/store/facts.cjs +13 -9
- package/dist/store/facts.cjs.map +1 -1
- package/dist/store/facts.d.cts +6 -2
- package/dist/store/facts.d.cts.map +1 -1
- package/dist/store/facts.d.mts +6 -2
- package/dist/store/facts.d.mts.map +1 -1
- package/dist/store/facts.mjs +13 -9
- package/dist/store/facts.mjs.map +1 -1
- package/dist/store/identity.cjs +34 -0
- package/dist/store/identity.cjs.map +1 -0
- package/dist/store/identity.d.cts +13 -0
- package/dist/store/identity.d.cts.map +1 -0
- package/dist/store/identity.d.mts +13 -0
- package/dist/store/identity.d.mts.map +1 -0
- package/dist/store/identity.mjs +30 -0
- package/dist/store/identity.mjs.map +1 -0
- package/dist/store/index.cjs +5 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +49 -15
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +49 -15
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +206 -44
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +17 -4
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +17 -4
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +207 -45
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +24 -6
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +24 -6
- package/dist/store/tables.mjs.map +1 -1
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +1 -1
- package/dist/webpush.d.mts +1 -1
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_store_erasure = require("../store/erasure.cjs");
|
|
3
|
+
const require_store_identity = require("../store/identity.cjs");
|
|
4
|
+
require("../store/index.cjs");
|
|
5
|
+
const require_host_ingest = require("./ingest.cjs");
|
|
6
|
+
const require_contract = require("../contract.cjs");
|
|
7
|
+
// src/host/batch.ts
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function isNonEmptyString(value) {
|
|
12
|
+
return typeof value === "string" && value.length > 0;
|
|
13
|
+
}
|
|
14
|
+
/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */
|
|
15
|
+
function isScalarOrNull(value) {
|
|
16
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
17
|
+
}
|
|
18
|
+
/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */
|
|
19
|
+
const TS_FUTURE_TOLERANCE_MS = 3e5;
|
|
20
|
+
const TS_ERROR = "ts must be a non-negative integer no later than 5 minutes in the future";
|
|
21
|
+
/**
|
|
22
|
+
* A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.
|
|
23
|
+
* A far-future ts would put the event ahead of everything a window condition reads, and a
|
|
24
|
+
* fractional or unsafe one does not survive the store.
|
|
25
|
+
*/
|
|
26
|
+
function isValidTs(ts) {
|
|
27
|
+
return typeof ts === "number" && Number.isSafeInteger(ts) && ts >= 0 && ts <= Date.now() + TS_FUTURE_TOLERANCE_MS;
|
|
28
|
+
}
|
|
29
|
+
/** What is wrong with an untrusted identity: at least one id, and a user id outside the visitors' prefix. */
|
|
30
|
+
function identityError(userId, visitorId) {
|
|
31
|
+
if (userId === void 0 && visitorId === void 0) return "userId or visitorId required";
|
|
32
|
+
if (userId !== void 0 && !isNonEmptyString(userId)) return "userId must be a non-empty string";
|
|
33
|
+
if (typeof userId === "string" && require_store_identity.isVisitorSubject(userId)) return "userId must not start with 'visitor:'";
|
|
34
|
+
if (visitorId !== void 0 && !isNonEmptyString(visitorId)) return "visitorId must be a non-empty string";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An untrusted batch, checked whole: one bad part refuses all of it, so a producer never has half
|
|
38
|
+
* a report applied. Contract `mode: 'report'` logs what it would refuse and keeps the original.
|
|
39
|
+
*/
|
|
40
|
+
function parseBatch(raw, validation) {
|
|
41
|
+
const refuse = (error) => ({
|
|
42
|
+
ok: false,
|
|
43
|
+
error
|
|
44
|
+
});
|
|
45
|
+
if (!isPlainObject(raw)) return refuse("batch must be an object");
|
|
46
|
+
const { userId, visitorId, profile, at, events } = raw;
|
|
47
|
+
const identity = identityError(userId, visitorId);
|
|
48
|
+
if (identity !== void 0) return refuse(identity);
|
|
49
|
+
if (at !== void 0 && !isValidTs(at)) return refuse(`at: ${TS_ERROR}`);
|
|
50
|
+
if (profile === void 0 && events === void 0) return refuse("profile or events required");
|
|
51
|
+
const batch = {
|
|
52
|
+
...userId === void 0 ? {} : { userId },
|
|
53
|
+
...visitorId === void 0 ? {} : { visitorId },
|
|
54
|
+
...at === void 0 ? {} : { at }
|
|
55
|
+
};
|
|
56
|
+
const contract = validation.contract;
|
|
57
|
+
const report = (what, issues) => console.warn(`[workflow] contract would refuse ${what}: ${issues.map((i) => `${i.path || "(root)"}: ${i.message}`).join("; ")}`);
|
|
58
|
+
if (profile !== void 0) {
|
|
59
|
+
if (!isPlainObject(profile)) return refuse("profile must be an object");
|
|
60
|
+
if (!Object.values(profile).every(isScalarOrNull)) return refuse("profile values must be scalars (string, number, boolean) or null");
|
|
61
|
+
batch.profile = profile;
|
|
62
|
+
if (contract !== void 0) {
|
|
63
|
+
const result = contract.parseProfilePatch(profile);
|
|
64
|
+
if (result.ok) batch.profile = result.data;
|
|
65
|
+
else if (validation.mode === "report") report("the profile", result.issues);
|
|
66
|
+
else return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: "profile refused by the contract",
|
|
69
|
+
issues: result.issues
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (events !== void 0) {
|
|
74
|
+
if (!Array.isArray(events)) return refuse("events must be an array");
|
|
75
|
+
const parsed = [];
|
|
76
|
+
for (const item of events) {
|
|
77
|
+
if (!isPlainObject(item)) return refuse("every event must be an object");
|
|
78
|
+
const { event, payload, id, ts } = item;
|
|
79
|
+
if (!isNonEmptyString(event)) return refuse("every event needs a name");
|
|
80
|
+
if (event.startsWith("$")) return refuse("event names starting with $ are reserved");
|
|
81
|
+
if (id !== void 0 && !isNonEmptyString(id)) return refuse("an event id must be a non-empty string");
|
|
82
|
+
if (ts !== void 0 && !isValidTs(ts)) return refuse(TS_ERROR);
|
|
83
|
+
if (payload !== void 0 && !isPlainObject(payload)) return refuse("payload must be an object");
|
|
84
|
+
let accepted = payload;
|
|
85
|
+
if (contract !== void 0) {
|
|
86
|
+
const result = require_contract.checkEvent(contract, event, payload ?? {});
|
|
87
|
+
const what = `event '${event}'${id === void 0 ? "" : ` (${id})`}`;
|
|
88
|
+
if (result.ok) accepted = result.data;
|
|
89
|
+
else if (validation.mode === "report") report(what, result.issues);
|
|
90
|
+
else return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: `${what} refused by the contract`,
|
|
93
|
+
issues: result.issues
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
parsed.push({
|
|
97
|
+
event,
|
|
98
|
+
payload: accepted,
|
|
99
|
+
id,
|
|
100
|
+
ts
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
batch.events = parsed;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
batch
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Apply a checked batch: the visitor's merge, the profile, then each event in order. Every part is idempotent (a
|
|
112
|
+
* property older than the stored one is skipped, an event id is stored once), so a caller retries
|
|
113
|
+
* a failed batch whole.
|
|
114
|
+
*/
|
|
115
|
+
async function ingestBatch(env, batch, store) {
|
|
116
|
+
const { userId, visitorId, profile, at, events = [] } = batch;
|
|
117
|
+
if (userId !== void 0 && visitorId !== void 0) await require_host_ingest.linkVisitor(env, visitorId, userId, store, at);
|
|
118
|
+
const subject = userId ?? await store.touchVisitor(visitorId ?? "", at ?? Date.now());
|
|
119
|
+
const result = { events: [] };
|
|
120
|
+
if (profile !== void 0) result.profile = await require_host_ingest.identifyUser(env, subject, profile, store, at);
|
|
121
|
+
for (const { event, payload, id, ts } of events) result.events.push(await require_host_ingest.ingestEvent(env, {
|
|
122
|
+
userId: subject,
|
|
123
|
+
event,
|
|
124
|
+
payload,
|
|
125
|
+
id,
|
|
126
|
+
ts
|
|
127
|
+
}, store));
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/** First retry after 30 s, doubling up to 15 minutes. */
|
|
131
|
+
function retryDelay(attempts) {
|
|
132
|
+
return Math.min(30 * 2 ** Math.max(attempts - 1, 0), 900);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Consume delivered batches one at a time. A batch that can never succeed is acknowledged, not
|
|
136
|
+
* retried: one the contract refuses (logged with the field paths, never the payload) and one for
|
|
137
|
+
* an erased user. Anything else is retried with a backoff until the queue's `max_retries` hands
|
|
138
|
+
* it to its dead letter queue.
|
|
139
|
+
*/
|
|
140
|
+
async function consumeBatches(env, messages, store, validation) {
|
|
141
|
+
for (const message of messages) {
|
|
142
|
+
const parsed = parseBatch(message.body, validation);
|
|
143
|
+
if (!parsed.ok) {
|
|
144
|
+
const issues = parsed.issues?.map((i) => `${i.path || "(root)"}: ${i.message}`).join("; ");
|
|
145
|
+
console.error(`[workflow queue] batch ${message.id} refused: ${parsed.error}${issues ? ` (${issues})` : ""}`);
|
|
146
|
+
message.ack();
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await ingestBatch(env, parsed.batch, store);
|
|
151
|
+
message.ack();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof require_store_erasure.UserErasedError) {
|
|
154
|
+
message.ack();
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
console.error(`[workflow queue] batch ${message.id} failed`, error);
|
|
158
|
+
message.retry({ delaySeconds: retryDelay(message.attempts) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.TS_ERROR = TS_ERROR;
|
|
163
|
+
exports.consumeBatches = consumeBatches;
|
|
164
|
+
exports.identityError = identityError;
|
|
165
|
+
exports.ingestBatch = ingestBatch;
|
|
166
|
+
exports.isNonEmptyString = isNonEmptyString;
|
|
167
|
+
exports.isPlainObject = isPlainObject;
|
|
168
|
+
exports.isScalarOrNull = isScalarOrNull;
|
|
169
|
+
exports.isValidTs = isValidTs;
|
|
170
|
+
exports.parseBatch = parseBatch;
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=batch.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.cjs","names":["isVisitorSubject","checkEvent","linkVisitor","identifyUser","ingestEvent","UserErasedError"],"sources":["../../src/host/batch.ts"],"sourcesContent":["import { checkEvent } from '../contract';\nimport {\n type JourneyStore,\n type ProfileProps,\n UserErasedError,\n isVisitorSubject,\n} from '../store/index';\nimport type { IngestValidation } from './http';\nimport {\n type IdentifyResult,\n type IngestResult,\n identifyUser,\n ingestEvent,\n linkVisitor,\n} from './ingest';\nimport type { JourneyEnv } from './platform';\n\n/**\n * One producer report as a single unit: what the user's profile now says and what they just did.\n * The profile is merged before the first event, so a journey an event starts always finds the\n * recipient that report knew about, whichever transport carried it and whatever arrived around\n * it. The same shape travels over HTTP (`POST /ingest`) and through a queue.\n */\nexport interface JourneyBatch {\n /** The signed-in user. With `visitorId` too, that visitor is this user: they merge, once. */\n userId?: string | undefined;\n /** The visitor before sign-in: a stable id for one client, renewed when a user signs out. */\n visitorId?: string | undefined;\n profile?: ProfileProps | undefined;\n /**\n * When the profile was read (epoch ms), the Worker's clock when omitted. A property written\n * later stays: a redelivered batch cannot put an old address back.\n */\n at?: number | undefined;\n events?: readonly BatchEvent[] | undefined;\n}\n\nexport interface BatchEvent {\n event: string;\n payload?: Record<string, unknown> | undefined;\n /** The occurrence's identity: a redelivered batch records it once. */\n id?: string | undefined;\n ts?: number | undefined;\n}\n\nexport interface BatchResult {\n profile?: IdentifyResult;\n events: IngestResult[];\n}\n\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */\nexport function isScalarOrNull(value: unknown): value is string | number | boolean | null {\n return (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */\nconst TS_FUTURE_TOLERANCE_MS = 5 * 60_000;\nexport const TS_ERROR = 'ts must be a non-negative integer no later than 5 minutes in the future';\n\n/**\n * A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.\n * A far-future ts would put the event ahead of everything a window condition reads, and a\n * fractional or unsafe one does not survive the store.\n */\nexport function isValidTs(ts: unknown): ts is number {\n return (\n typeof ts === 'number' &&\n Number.isSafeInteger(ts) &&\n ts >= 0 &&\n ts <= Date.now() + TS_FUTURE_TOLERANCE_MS\n );\n}\n\n/** What is wrong with an untrusted identity: at least one id, and a user id outside the visitors' prefix. */\nexport function identityError(userId: unknown, visitorId: unknown): string | undefined {\n if (userId === undefined && visitorId === undefined) return 'userId or visitorId required';\n if (userId !== undefined && !isNonEmptyString(userId)) {\n return 'userId must be a non-empty string';\n }\n if (typeof userId === 'string' && isVisitorSubject(userId)) {\n return \"userId must not start with 'visitor:'\";\n }\n if (visitorId !== undefined && !isNonEmptyString(visitorId)) {\n return 'visitorId must be a non-empty string';\n }\n return undefined;\n}\n\nexport type ParsedBatch =\n | { ok: true; batch: JourneyBatch }\n | { ok: false; error: string; issues?: readonly { path: string; message: string }[] };\n\n/**\n * An untrusted batch, checked whole: one bad part refuses all of it, so a producer never has half\n * a report applied. Contract `mode: 'report'` logs what it would refuse and keeps the original.\n */\nexport function parseBatch(raw: unknown, validation: IngestValidation): ParsedBatch {\n const refuse = (error: string): ParsedBatch => ({ ok: false, error });\n if (!isPlainObject(raw)) return refuse('batch must be an object');\n const { userId, visitorId, profile, at, events } = raw;\n const identity = identityError(userId, visitorId);\n if (identity !== undefined) return refuse(identity);\n if (at !== undefined && !isValidTs(at)) return refuse(`at: ${TS_ERROR}`);\n if (profile === undefined && events === undefined) return refuse('profile or events required');\n\n const batch: JourneyBatch = {\n ...(userId === undefined ? {} : { userId: userId as string }),\n ...(visitorId === undefined ? {} : { visitorId: visitorId as string }),\n ...(at === undefined ? {} : { at }),\n };\n const contract = validation.contract;\n const report = (what: string, issues: readonly { path: string; message: string }[]) =>\n console.warn(\n `[workflow] contract would refuse ${what}: ${issues\n .map((i) => `${i.path || '(root)'}: ${i.message}`)\n .join('; ')}`\n );\n\n if (profile !== undefined) {\n if (!isPlainObject(profile)) return refuse('profile must be an object');\n if (!Object.values(profile).every(isScalarOrNull)) {\n return refuse('profile values must be scalars (string, number, boolean) or null');\n }\n batch.profile = profile as ProfileProps;\n if (contract !== undefined) {\n const result = contract.parseProfilePatch(profile);\n if (result.ok) batch.profile = result.data as ProfileProps;\n else if (validation.mode === 'report') report('the profile', result.issues);\n else return { ok: false, error: 'profile refused by the contract', issues: result.issues };\n }\n }\n\n if (events !== undefined) {\n if (!Array.isArray(events)) return refuse('events must be an array');\n const parsed: BatchEvent[] = [];\n for (const item of events as unknown[]) {\n if (!isPlainObject(item)) return refuse('every event must be an object');\n const { event, payload, id, ts } = item;\n if (!isNonEmptyString(event)) return refuse('every event needs a name');\n if (event.startsWith('$')) return refuse('event names starting with $ are reserved');\n if (id !== undefined && !isNonEmptyString(id)) {\n return refuse('an event id must be a non-empty string');\n }\n if (ts !== undefined && !isValidTs(ts)) return refuse(TS_ERROR);\n if (payload !== undefined && !isPlainObject(payload)) {\n return refuse('payload must be an object');\n }\n let accepted = payload;\n if (contract !== undefined) {\n const result = checkEvent(contract, event, payload ?? {});\n const what = `event '${event}'${id === undefined ? '' : ` (${id})`}`;\n if (result.ok) accepted = result.data;\n else if (validation.mode === 'report') report(what, result.issues);\n else return { ok: false, error: `${what} refused by the contract`, issues: result.issues };\n }\n parsed.push({ event, payload: accepted, id, ts });\n }\n batch.events = parsed;\n }\n return { ok: true, batch };\n}\n\n/**\n * Apply a checked batch: the visitor's merge, the profile, then each event in order. Every part is idempotent (a\n * property older than the stored one is skipped, an event id is stored once), so a caller retries\n * a failed batch whole.\n */\nexport async function ingestBatch(\n env: JourneyEnv,\n batch: JourneyBatch,\n store: JourneyStore\n): Promise<BatchResult> {\n const { userId, visitorId, profile, at, events = [] } = batch;\n if (userId !== undefined && visitorId !== undefined) {\n await linkVisitor(env, visitorId, userId, store, at);\n }\n // A late report of a visitor who has signed in since reaches the user\n const subject = userId ?? (await store.touchVisitor(visitorId ?? '', at ?? Date.now()));\n const result: BatchResult = { events: [] };\n if (profile !== undefined) result.profile = await identifyUser(env, subject, profile, store, at);\n for (const { event, payload, id, ts } of events) {\n result.events.push(await ingestEvent(env, { userId: subject, event, payload, id, ts }, store));\n }\n return result;\n}\n\n/** A delivered queue message (the subset of the Cloudflare Queues `Message` used). */\nexport interface QueueMessageLike {\n id: string;\n body: unknown;\n /** Delivery attempts so far, this one included. */\n attempts: number;\n ack(): void;\n retry(options?: { delaySeconds?: number }): void;\n}\n\n/** First retry after 30 s, doubling up to 15 minutes. */\nfunction retryDelay(attempts: number): number {\n return Math.min(30 * 2 ** Math.max(attempts - 1, 0), 900);\n}\n\n/**\n * Consume delivered batches one at a time. A batch that can never succeed is acknowledged, not\n * retried: one the contract refuses (logged with the field paths, never the payload) and one for\n * an erased user. Anything else is retried with a backoff until the queue's `max_retries` hands\n * it to its dead letter queue.\n */\nexport async function consumeBatches(\n env: JourneyEnv,\n messages: readonly QueueMessageLike[],\n store: JourneyStore,\n validation: IngestValidation\n): Promise<void> {\n for (const message of messages) {\n const parsed = parseBatch(message.body, validation);\n if (!parsed.ok) {\n const issues = parsed.issues?.map((i) => `${i.path || '(root)'}: ${i.message}`).join('; ');\n console.error(\n `[workflow queue] batch ${message.id} refused: ${parsed.error}${issues ? ` (${issues})` : ''}`\n );\n message.ack();\n continue;\n }\n try {\n await ingestBatch(env, parsed.batch, store);\n message.ack();\n } catch (error) {\n if (error instanceof UserErasedError) {\n message.ack();\n continue;\n }\n console.error(`[workflow queue] batch ${message.id} failed`, error);\n message.retry({ delaySeconds: retryDelay(message.attempts) });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { JourneyStore, ProfileProps } from "../store/index.cjs";
|
|
2
|
+
import { JourneyEnv } from "./platform.cjs";
|
|
3
|
+
import { IdentifyResult, IngestResult } from "./ingest.cjs";
|
|
4
|
+
import { IngestValidation } from "./http.cjs";
|
|
5
|
+
/**
|
|
6
|
+
* One producer report as a single unit: what the user's profile now says and what they just did.
|
|
7
|
+
* The profile is merged before the first event, so a journey an event starts always finds the
|
|
8
|
+
* recipient that report knew about, whichever transport carried it and whatever arrived around
|
|
9
|
+
* it. The same shape travels over HTTP (`POST /ingest`) and through a queue.
|
|
10
|
+
*/
|
|
11
|
+
export interface JourneyBatch {
|
|
12
|
+
/** The signed-in user. With `visitorId` too, that visitor is this user: they merge, once. */
|
|
13
|
+
userId?: string | undefined;
|
|
14
|
+
/** The visitor before sign-in: a stable id for one client, renewed when a user signs out. */
|
|
15
|
+
visitorId?: string | undefined;
|
|
16
|
+
profile?: ProfileProps | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* When the profile was read (epoch ms), the Worker's clock when omitted. A property written
|
|
19
|
+
* later stays: a redelivered batch cannot put an old address back.
|
|
20
|
+
*/
|
|
21
|
+
at?: number | undefined;
|
|
22
|
+
events?: readonly BatchEvent[] | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface BatchEvent {
|
|
25
|
+
event: string;
|
|
26
|
+
payload?: Record<string, unknown> | undefined;
|
|
27
|
+
/** The occurrence's identity: a redelivered batch records it once. */
|
|
28
|
+
id?: string | undefined;
|
|
29
|
+
ts?: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface BatchResult {
|
|
32
|
+
profile?: IdentifyResult;
|
|
33
|
+
events: IngestResult[];
|
|
34
|
+
}
|
|
35
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
36
|
+
export declare function isNonEmptyString(value: unknown): value is string;
|
|
37
|
+
/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */
|
|
38
|
+
export declare function isScalarOrNull(value: unknown): value is string | number | boolean | null;
|
|
39
|
+
export declare const TS_ERROR = "ts must be a non-negative integer no later than 5 minutes in the future";
|
|
40
|
+
/**
|
|
41
|
+
* A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.
|
|
42
|
+
* A far-future ts would put the event ahead of everything a window condition reads, and a
|
|
43
|
+
* fractional or unsafe one does not survive the store.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isValidTs(ts: unknown): ts is number;
|
|
46
|
+
/** What is wrong with an untrusted identity: at least one id, and a user id outside the visitors' prefix. */
|
|
47
|
+
export declare function identityError(userId: unknown, visitorId: unknown): string | undefined;
|
|
48
|
+
export type ParsedBatch = {
|
|
49
|
+
ok: true;
|
|
50
|
+
batch: JourneyBatch;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
error: string;
|
|
54
|
+
issues?: readonly {
|
|
55
|
+
path: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* An untrusted batch, checked whole: one bad part refuses all of it, so a producer never has half
|
|
61
|
+
* a report applied. Contract `mode: 'report'` logs what it would refuse and keeps the original.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseBatch(raw: unknown, validation: IngestValidation): ParsedBatch;
|
|
64
|
+
/**
|
|
65
|
+
* Apply a checked batch: the visitor's merge, the profile, then each event in order. Every part is idempotent (a
|
|
66
|
+
* property older than the stored one is skipped, an event id is stored once), so a caller retries
|
|
67
|
+
* a failed batch whole.
|
|
68
|
+
*/
|
|
69
|
+
export declare function ingestBatch(env: JourneyEnv, batch: JourneyBatch, store: JourneyStore): Promise<BatchResult>;
|
|
70
|
+
/** A delivered queue message (the subset of the Cloudflare Queues `Message` used). */
|
|
71
|
+
export interface QueueMessageLike {
|
|
72
|
+
id: string;
|
|
73
|
+
body: unknown;
|
|
74
|
+
/** Delivery attempts so far, this one included. */
|
|
75
|
+
attempts: number;
|
|
76
|
+
ack(): void;
|
|
77
|
+
retry(options?: {
|
|
78
|
+
delaySeconds?: number;
|
|
79
|
+
}): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Consume delivered batches one at a time. A batch that can never succeed is acknowledged, not
|
|
83
|
+
* retried: one the contract refuses (logged with the field paths, never the payload) and one for
|
|
84
|
+
* an erased user. Anything else is retried with a backoff until the queue's `max_retries` hands
|
|
85
|
+
* it to its dead letter queue.
|
|
86
|
+
*/
|
|
87
|
+
export declare function consumeBatches(env: JourneyEnv, messages: readonly QueueMessageLike[], store: JourneyStore, validation: IngestValidation): Promise<void>;
|
|
88
|
+
//# sourceMappingURL=batch.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.cts","names":[],"sources":["../../src/host/batch.ts"],"mappings":";;;;;;;;;;iBAuBiB;;EAEf;;EAEA;EACA,UAAU;;;;;EAKV;EACA,kBAAkB;;iBAGH;EACf;EACA,UAAU;;EAEV;EACA;;iBAGe;EACf,UAAU;EACV,QAAQ;;wBAGM,cAAc,iBAAiB,SAAS;wBAIxC,iBAAiB,iBAAiB;;wBAKlC,eAAe,iBAAiB;qBAWnC;;;;;;wBAOG,UAAU,cAAc;;wBAUxB,cAAc,iBAAiB;YAcnC;EACN;EAAU,OAAO;;EACjB;EAAW;EAAe;IAAoB;IAAc;;;;;;;wBAMlD,WAAW,cAAc,YAAY,mBAAmB;;;;;;wBAuElD,YACpB,KAAK,YACL,OAAO,cACP,OAAO,eACN,QAAQ;;iBAgBM;EACf;EACA;;EAEA;EACA;EACA,MAAM;IAAY;;;;;;;;;wBAcE,eACpB,KAAK,YACL,mBAAmB,oBACnB,OAAO,cACP,YAAY,mBACX"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { JourneyStore, ProfileProps } from "../store/index.mjs";
|
|
2
|
+
import { JourneyEnv } from "./platform.mjs";
|
|
3
|
+
import { IdentifyResult, IngestResult } from "./ingest.mjs";
|
|
4
|
+
import { IngestValidation } from "./http.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* One producer report as a single unit: what the user's profile now says and what they just did.
|
|
7
|
+
* The profile is merged before the first event, so a journey an event starts always finds the
|
|
8
|
+
* recipient that report knew about, whichever transport carried it and whatever arrived around
|
|
9
|
+
* it. The same shape travels over HTTP (`POST /ingest`) and through a queue.
|
|
10
|
+
*/
|
|
11
|
+
export interface JourneyBatch {
|
|
12
|
+
/** The signed-in user. With `visitorId` too, that visitor is this user: they merge, once. */
|
|
13
|
+
userId?: string | undefined;
|
|
14
|
+
/** The visitor before sign-in: a stable id for one client, renewed when a user signs out. */
|
|
15
|
+
visitorId?: string | undefined;
|
|
16
|
+
profile?: ProfileProps | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* When the profile was read (epoch ms), the Worker's clock when omitted. A property written
|
|
19
|
+
* later stays: a redelivered batch cannot put an old address back.
|
|
20
|
+
*/
|
|
21
|
+
at?: number | undefined;
|
|
22
|
+
events?: readonly BatchEvent[] | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface BatchEvent {
|
|
25
|
+
event: string;
|
|
26
|
+
payload?: Record<string, unknown> | undefined;
|
|
27
|
+
/** The occurrence's identity: a redelivered batch records it once. */
|
|
28
|
+
id?: string | undefined;
|
|
29
|
+
ts?: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface BatchResult {
|
|
32
|
+
profile?: IdentifyResult;
|
|
33
|
+
events: IngestResult[];
|
|
34
|
+
}
|
|
35
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
36
|
+
export declare function isNonEmptyString(value: unknown): value is string;
|
|
37
|
+
/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */
|
|
38
|
+
export declare function isScalarOrNull(value: unknown): value is string | number | boolean | null;
|
|
39
|
+
export declare const TS_ERROR = "ts must be a non-negative integer no later than 5 minutes in the future";
|
|
40
|
+
/**
|
|
41
|
+
* A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.
|
|
42
|
+
* A far-future ts would put the event ahead of everything a window condition reads, and a
|
|
43
|
+
* fractional or unsafe one does not survive the store.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isValidTs(ts: unknown): ts is number;
|
|
46
|
+
/** What is wrong with an untrusted identity: at least one id, and a user id outside the visitors' prefix. */
|
|
47
|
+
export declare function identityError(userId: unknown, visitorId: unknown): string | undefined;
|
|
48
|
+
export type ParsedBatch = {
|
|
49
|
+
ok: true;
|
|
50
|
+
batch: JourneyBatch;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
error: string;
|
|
54
|
+
issues?: readonly {
|
|
55
|
+
path: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* An untrusted batch, checked whole: one bad part refuses all of it, so a producer never has half
|
|
61
|
+
* a report applied. Contract `mode: 'report'` logs what it would refuse and keeps the original.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseBatch(raw: unknown, validation: IngestValidation): ParsedBatch;
|
|
64
|
+
/**
|
|
65
|
+
* Apply a checked batch: the visitor's merge, the profile, then each event in order. Every part is idempotent (a
|
|
66
|
+
* property older than the stored one is skipped, an event id is stored once), so a caller retries
|
|
67
|
+
* a failed batch whole.
|
|
68
|
+
*/
|
|
69
|
+
export declare function ingestBatch(env: JourneyEnv, batch: JourneyBatch, store: JourneyStore): Promise<BatchResult>;
|
|
70
|
+
/** A delivered queue message (the subset of the Cloudflare Queues `Message` used). */
|
|
71
|
+
export interface QueueMessageLike {
|
|
72
|
+
id: string;
|
|
73
|
+
body: unknown;
|
|
74
|
+
/** Delivery attempts so far, this one included. */
|
|
75
|
+
attempts: number;
|
|
76
|
+
ack(): void;
|
|
77
|
+
retry(options?: {
|
|
78
|
+
delaySeconds?: number;
|
|
79
|
+
}): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Consume delivered batches one at a time. A batch that can never succeed is acknowledged, not
|
|
83
|
+
* retried: one the contract refuses (logged with the field paths, never the payload) and one for
|
|
84
|
+
* an erased user. Anything else is retried with a backoff until the queue's `max_retries` hands
|
|
85
|
+
* it to its dead letter queue.
|
|
86
|
+
*/
|
|
87
|
+
export declare function consumeBatches(env: JourneyEnv, messages: readonly QueueMessageLike[], store: JourneyStore, validation: IngestValidation): Promise<void>;
|
|
88
|
+
//# sourceMappingURL=batch.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.mts","names":[],"sources":["../../src/host/batch.ts"],"mappings":";;;;;;;;;;iBAuBiB;;EAEf;;EAEA;EACA,UAAU;;;;;EAKV;EACA,kBAAkB;;iBAGH;EACf;EACA,UAAU;;EAEV;EACA;;iBAGe;EACf,UAAU;EACV,QAAQ;;wBAGM,cAAc,iBAAiB,SAAS;wBAIxC,iBAAiB,iBAAiB;;wBAKlC,eAAe,iBAAiB;qBAWnC;;;;;;wBAOG,UAAU,cAAc;;wBAUxB,cAAc,iBAAiB;YAcnC;EACN;EAAU,OAAO;;EACjB;EAAW;EAAe;IAAoB;IAAc;;;;;;;wBAMlD,WAAW,cAAc,YAAY,mBAAmB;;;;;;wBAuElD,YACpB,KAAK,YACL,OAAO,cACP,OAAO,eACN,QAAQ;;iBAgBM;EACf;EACA;;EAEA;EACA;EACA,MAAM;IAAY;;;;;;;;;wBAcE,eACpB,KAAK,YACL,mBAAmB,oBACnB,OAAO,cACP,YAAY,mBACX"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { UserErasedError } from "../store/erasure.mjs";
|
|
2
|
+
import { isVisitorSubject } from "../store/identity.mjs";
|
|
3
|
+
import "../store/index.mjs";
|
|
4
|
+
import { identifyUser, ingestEvent, linkVisitor } from "./ingest.mjs";
|
|
5
|
+
import { checkEvent } from "../contract.mjs";
|
|
6
|
+
// src/host/batch.ts
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function isNonEmptyString(value) {
|
|
11
|
+
return typeof value === "string" && value.length > 0;
|
|
12
|
+
}
|
|
13
|
+
/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */
|
|
14
|
+
function isScalarOrNull(value) {
|
|
15
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
16
|
+
}
|
|
17
|
+
/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */
|
|
18
|
+
const TS_FUTURE_TOLERANCE_MS = 3e5;
|
|
19
|
+
const TS_ERROR = "ts must be a non-negative integer no later than 5 minutes in the future";
|
|
20
|
+
/**
|
|
21
|
+
* A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.
|
|
22
|
+
* A far-future ts would put the event ahead of everything a window condition reads, and a
|
|
23
|
+
* fractional or unsafe one does not survive the store.
|
|
24
|
+
*/
|
|
25
|
+
function isValidTs(ts) {
|
|
26
|
+
return typeof ts === "number" && Number.isSafeInteger(ts) && ts >= 0 && ts <= Date.now() + TS_FUTURE_TOLERANCE_MS;
|
|
27
|
+
}
|
|
28
|
+
/** What is wrong with an untrusted identity: at least one id, and a user id outside the visitors' prefix. */
|
|
29
|
+
function identityError(userId, visitorId) {
|
|
30
|
+
if (userId === void 0 && visitorId === void 0) return "userId or visitorId required";
|
|
31
|
+
if (userId !== void 0 && !isNonEmptyString(userId)) return "userId must be a non-empty string";
|
|
32
|
+
if (typeof userId === "string" && isVisitorSubject(userId)) return "userId must not start with 'visitor:'";
|
|
33
|
+
if (visitorId !== void 0 && !isNonEmptyString(visitorId)) return "visitorId must be a non-empty string";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An untrusted batch, checked whole: one bad part refuses all of it, so a producer never has half
|
|
37
|
+
* a report applied. Contract `mode: 'report'` logs what it would refuse and keeps the original.
|
|
38
|
+
*/
|
|
39
|
+
function parseBatch(raw, validation) {
|
|
40
|
+
const refuse = (error) => ({
|
|
41
|
+
ok: false,
|
|
42
|
+
error
|
|
43
|
+
});
|
|
44
|
+
if (!isPlainObject(raw)) return refuse("batch must be an object");
|
|
45
|
+
const { userId, visitorId, profile, at, events } = raw;
|
|
46
|
+
const identity = identityError(userId, visitorId);
|
|
47
|
+
if (identity !== void 0) return refuse(identity);
|
|
48
|
+
if (at !== void 0 && !isValidTs(at)) return refuse(`at: ${TS_ERROR}`);
|
|
49
|
+
if (profile === void 0 && events === void 0) return refuse("profile or events required");
|
|
50
|
+
const batch = {
|
|
51
|
+
...userId === void 0 ? {} : { userId },
|
|
52
|
+
...visitorId === void 0 ? {} : { visitorId },
|
|
53
|
+
...at === void 0 ? {} : { at }
|
|
54
|
+
};
|
|
55
|
+
const contract = validation.contract;
|
|
56
|
+
const report = (what, issues) => console.warn(`[workflow] contract would refuse ${what}: ${issues.map((i) => `${i.path || "(root)"}: ${i.message}`).join("; ")}`);
|
|
57
|
+
if (profile !== void 0) {
|
|
58
|
+
if (!isPlainObject(profile)) return refuse("profile must be an object");
|
|
59
|
+
if (!Object.values(profile).every(isScalarOrNull)) return refuse("profile values must be scalars (string, number, boolean) or null");
|
|
60
|
+
batch.profile = profile;
|
|
61
|
+
if (contract !== void 0) {
|
|
62
|
+
const result = contract.parseProfilePatch(profile);
|
|
63
|
+
if (result.ok) batch.profile = result.data;
|
|
64
|
+
else if (validation.mode === "report") report("the profile", result.issues);
|
|
65
|
+
else return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: "profile refused by the contract",
|
|
68
|
+
issues: result.issues
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (events !== void 0) {
|
|
73
|
+
if (!Array.isArray(events)) return refuse("events must be an array");
|
|
74
|
+
const parsed = [];
|
|
75
|
+
for (const item of events) {
|
|
76
|
+
if (!isPlainObject(item)) return refuse("every event must be an object");
|
|
77
|
+
const { event, payload, id, ts } = item;
|
|
78
|
+
if (!isNonEmptyString(event)) return refuse("every event needs a name");
|
|
79
|
+
if (event.startsWith("$")) return refuse("event names starting with $ are reserved");
|
|
80
|
+
if (id !== void 0 && !isNonEmptyString(id)) return refuse("an event id must be a non-empty string");
|
|
81
|
+
if (ts !== void 0 && !isValidTs(ts)) return refuse(TS_ERROR);
|
|
82
|
+
if (payload !== void 0 && !isPlainObject(payload)) return refuse("payload must be an object");
|
|
83
|
+
let accepted = payload;
|
|
84
|
+
if (contract !== void 0) {
|
|
85
|
+
const result = checkEvent(contract, event, payload ?? {});
|
|
86
|
+
const what = `event '${event}'${id === void 0 ? "" : ` (${id})`}`;
|
|
87
|
+
if (result.ok) accepted = result.data;
|
|
88
|
+
else if (validation.mode === "report") report(what, result.issues);
|
|
89
|
+
else return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: `${what} refused by the contract`,
|
|
92
|
+
issues: result.issues
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
parsed.push({
|
|
96
|
+
event,
|
|
97
|
+
payload: accepted,
|
|
98
|
+
id,
|
|
99
|
+
ts
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
batch.events = parsed;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
batch
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Apply a checked batch: the visitor's merge, the profile, then each event in order. Every part is idempotent (a
|
|
111
|
+
* property older than the stored one is skipped, an event id is stored once), so a caller retries
|
|
112
|
+
* a failed batch whole.
|
|
113
|
+
*/
|
|
114
|
+
async function ingestBatch(env, batch, store) {
|
|
115
|
+
const { userId, visitorId, profile, at, events = [] } = batch;
|
|
116
|
+
if (userId !== void 0 && visitorId !== void 0) await linkVisitor(env, visitorId, userId, store, at);
|
|
117
|
+
const subject = userId ?? await store.touchVisitor(visitorId ?? "", at ?? Date.now());
|
|
118
|
+
const result = { events: [] };
|
|
119
|
+
if (profile !== void 0) result.profile = await identifyUser(env, subject, profile, store, at);
|
|
120
|
+
for (const { event, payload, id, ts } of events) result.events.push(await ingestEvent(env, {
|
|
121
|
+
userId: subject,
|
|
122
|
+
event,
|
|
123
|
+
payload,
|
|
124
|
+
id,
|
|
125
|
+
ts
|
|
126
|
+
}, store));
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
/** First retry after 30 s, doubling up to 15 minutes. */
|
|
130
|
+
function retryDelay(attempts) {
|
|
131
|
+
return Math.min(30 * 2 ** Math.max(attempts - 1, 0), 900);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Consume delivered batches one at a time. A batch that can never succeed is acknowledged, not
|
|
135
|
+
* retried: one the contract refuses (logged with the field paths, never the payload) and one for
|
|
136
|
+
* an erased user. Anything else is retried with a backoff until the queue's `max_retries` hands
|
|
137
|
+
* it to its dead letter queue.
|
|
138
|
+
*/
|
|
139
|
+
async function consumeBatches(env, messages, store, validation) {
|
|
140
|
+
for (const message of messages) {
|
|
141
|
+
const parsed = parseBatch(message.body, validation);
|
|
142
|
+
if (!parsed.ok) {
|
|
143
|
+
const issues = parsed.issues?.map((i) => `${i.path || "(root)"}: ${i.message}`).join("; ");
|
|
144
|
+
console.error(`[workflow queue] batch ${message.id} refused: ${parsed.error}${issues ? ` (${issues})` : ""}`);
|
|
145
|
+
message.ack();
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await ingestBatch(env, parsed.batch, store);
|
|
150
|
+
message.ack();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof UserErasedError) {
|
|
153
|
+
message.ack();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
console.error(`[workflow queue] batch ${message.id} failed`, error);
|
|
157
|
+
message.retry({ delaySeconds: retryDelay(message.attempts) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export { TS_ERROR, consumeBatches, identityError, ingestBatch, isNonEmptyString, isPlainObject, isScalarOrNull, isValidTs, parseBatch };
|
|
162
|
+
|
|
163
|
+
//# sourceMappingURL=batch.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.mjs","names":[],"sources":["../../src/host/batch.ts"],"sourcesContent":["import { checkEvent } from '../contract';\nimport {\n type JourneyStore,\n type ProfileProps,\n UserErasedError,\n isVisitorSubject,\n} from '../store/index';\nimport type { IngestValidation } from './http';\nimport {\n type IdentifyResult,\n type IngestResult,\n identifyUser,\n ingestEvent,\n linkVisitor,\n} from './ingest';\nimport type { JourneyEnv } from './platform';\n\n/**\n * One producer report as a single unit: what the user's profile now says and what they just did.\n * The profile is merged before the first event, so a journey an event starts always finds the\n * recipient that report knew about, whichever transport carried it and whatever arrived around\n * it. The same shape travels over HTTP (`POST /ingest`) and through a queue.\n */\nexport interface JourneyBatch {\n /** The signed-in user. With `visitorId` too, that visitor is this user: they merge, once. */\n userId?: string | undefined;\n /** The visitor before sign-in: a stable id for one client, renewed when a user signs out. */\n visitorId?: string | undefined;\n profile?: ProfileProps | undefined;\n /**\n * When the profile was read (epoch ms), the Worker's clock when omitted. A property written\n * later stays: a redelivered batch cannot put an old address back.\n */\n at?: number | undefined;\n events?: readonly BatchEvent[] | undefined;\n}\n\nexport interface BatchEvent {\n event: string;\n payload?: Record<string, unknown> | undefined;\n /** The occurrence's identity: a redelivered batch records it once. */\n id?: string | undefined;\n ts?: number | undefined;\n}\n\nexport interface BatchResult {\n profile?: IdentifyResult;\n events: IngestResult[];\n}\n\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */\nexport function isScalarOrNull(value: unknown): value is string | number | boolean | null {\n return (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */\nconst TS_FUTURE_TOLERANCE_MS = 5 * 60_000;\nexport const TS_ERROR = 'ts must be a non-negative integer no later than 5 minutes in the future';\n\n/**\n * A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.\n * A far-future ts would put the event ahead of everything a window condition reads, and a\n * fractional or unsafe one does not survive the store.\n */\nexport function isValidTs(ts: unknown): ts is number {\n return (\n typeof ts === 'number' &&\n Number.isSafeInteger(ts) &&\n ts >= 0 &&\n ts <= Date.now() + TS_FUTURE_TOLERANCE_MS\n );\n}\n\n/** What is wrong with an untrusted identity: at least one id, and a user id outside the visitors' prefix. */\nexport function identityError(userId: unknown, visitorId: unknown): string | undefined {\n if (userId === undefined && visitorId === undefined) return 'userId or visitorId required';\n if (userId !== undefined && !isNonEmptyString(userId)) {\n return 'userId must be a non-empty string';\n }\n if (typeof userId === 'string' && isVisitorSubject(userId)) {\n return \"userId must not start with 'visitor:'\";\n }\n if (visitorId !== undefined && !isNonEmptyString(visitorId)) {\n return 'visitorId must be a non-empty string';\n }\n return undefined;\n}\n\nexport type ParsedBatch =\n | { ok: true; batch: JourneyBatch }\n | { ok: false; error: string; issues?: readonly { path: string; message: string }[] };\n\n/**\n * An untrusted batch, checked whole: one bad part refuses all of it, so a producer never has half\n * a report applied. Contract `mode: 'report'` logs what it would refuse and keeps the original.\n */\nexport function parseBatch(raw: unknown, validation: IngestValidation): ParsedBatch {\n const refuse = (error: string): ParsedBatch => ({ ok: false, error });\n if (!isPlainObject(raw)) return refuse('batch must be an object');\n const { userId, visitorId, profile, at, events } = raw;\n const identity = identityError(userId, visitorId);\n if (identity !== undefined) return refuse(identity);\n if (at !== undefined && !isValidTs(at)) return refuse(`at: ${TS_ERROR}`);\n if (profile === undefined && events === undefined) return refuse('profile or events required');\n\n const batch: JourneyBatch = {\n ...(userId === undefined ? {} : { userId: userId as string }),\n ...(visitorId === undefined ? {} : { visitorId: visitorId as string }),\n ...(at === undefined ? {} : { at }),\n };\n const contract = validation.contract;\n const report = (what: string, issues: readonly { path: string; message: string }[]) =>\n console.warn(\n `[workflow] contract would refuse ${what}: ${issues\n .map((i) => `${i.path || '(root)'}: ${i.message}`)\n .join('; ')}`\n );\n\n if (profile !== undefined) {\n if (!isPlainObject(profile)) return refuse('profile must be an object');\n if (!Object.values(profile).every(isScalarOrNull)) {\n return refuse('profile values must be scalars (string, number, boolean) or null');\n }\n batch.profile = profile as ProfileProps;\n if (contract !== undefined) {\n const result = contract.parseProfilePatch(profile);\n if (result.ok) batch.profile = result.data as ProfileProps;\n else if (validation.mode === 'report') report('the profile', result.issues);\n else return { ok: false, error: 'profile refused by the contract', issues: result.issues };\n }\n }\n\n if (events !== undefined) {\n if (!Array.isArray(events)) return refuse('events must be an array');\n const parsed: BatchEvent[] = [];\n for (const item of events as unknown[]) {\n if (!isPlainObject(item)) return refuse('every event must be an object');\n const { event, payload, id, ts } = item;\n if (!isNonEmptyString(event)) return refuse('every event needs a name');\n if (event.startsWith('$')) return refuse('event names starting with $ are reserved');\n if (id !== undefined && !isNonEmptyString(id)) {\n return refuse('an event id must be a non-empty string');\n }\n if (ts !== undefined && !isValidTs(ts)) return refuse(TS_ERROR);\n if (payload !== undefined && !isPlainObject(payload)) {\n return refuse('payload must be an object');\n }\n let accepted = payload;\n if (contract !== undefined) {\n const result = checkEvent(contract, event, payload ?? {});\n const what = `event '${event}'${id === undefined ? '' : ` (${id})`}`;\n if (result.ok) accepted = result.data;\n else if (validation.mode === 'report') report(what, result.issues);\n else return { ok: false, error: `${what} refused by the contract`, issues: result.issues };\n }\n parsed.push({ event, payload: accepted, id, ts });\n }\n batch.events = parsed;\n }\n return { ok: true, batch };\n}\n\n/**\n * Apply a checked batch: the visitor's merge, the profile, then each event in order. Every part is idempotent (a\n * property older than the stored one is skipped, an event id is stored once), so a caller retries\n * a failed batch whole.\n */\nexport async function ingestBatch(\n env: JourneyEnv,\n batch: JourneyBatch,\n store: JourneyStore\n): Promise<BatchResult> {\n const { userId, visitorId, profile, at, events = [] } = batch;\n if (userId !== undefined && visitorId !== undefined) {\n await linkVisitor(env, visitorId, userId, store, at);\n }\n // A late report of a visitor who has signed in since reaches the user\n const subject = userId ?? (await store.touchVisitor(visitorId ?? '', at ?? Date.now()));\n const result: BatchResult = { events: [] };\n if (profile !== undefined) result.profile = await identifyUser(env, subject, profile, store, at);\n for (const { event, payload, id, ts } of events) {\n result.events.push(await ingestEvent(env, { userId: subject, event, payload, id, ts }, store));\n }\n return result;\n}\n\n/** A delivered queue message (the subset of the Cloudflare Queues `Message` used). */\nexport interface QueueMessageLike {\n id: string;\n body: unknown;\n /** Delivery attempts so far, this one included. */\n attempts: number;\n ack(): void;\n retry(options?: { delaySeconds?: number }): void;\n}\n\n/** First retry after 30 s, doubling up to 15 minutes. */\nfunction retryDelay(attempts: number): number {\n return Math.min(30 * 2 ** Math.max(attempts - 1, 0), 900);\n}\n\n/**\n * Consume delivered batches one at a time. A batch that can never succeed is acknowledged, not\n * retried: one the contract refuses (logged with the field paths, never the payload) and one for\n * an erased user. Anything else is retried with a backoff until the queue's `max_retries` hands\n * it to its dead letter queue.\n */\nexport async function consumeBatches(\n env: JourneyEnv,\n messages: readonly QueueMessageLike[],\n store: JourneyStore,\n validation: IngestValidation\n): Promise<void> {\n for (const message of messages) {\n const parsed = parseBatch(message.body, validation);\n if (!parsed.ok) {\n const issues = parsed.issues?.map((i) => `${i.path || '(root)'}: ${i.message}`).join('; ');\n console.error(\n `[workflow queue] batch ${message.id} refused: ${parsed.error}${issues ? ` (${issues})` : ''}`\n );\n message.ack();\n continue;\n }\n try {\n await ingestBatch(env, parsed.batch, store);\n message.ack();\n } catch (error) {\n if (error instanceof UserErasedError) {\n message.ack();\n continue;\n }\n console.error(`[workflow queue] batch ${message.id} failed`, error);\n message.retry({ delaySeconds: retryDelay(message.attempts) });\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/host/execute.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_store_facts = require("../store/facts.cjs");
|
|
3
|
+
const require_store_identity = require("../store/identity.cjs");
|
|
4
|
+
require("../store/index.cjs");
|
|
3
5
|
const require_host_ingest = require("./ingest.cjs");
|
|
4
6
|
const require_contract = require("../contract.cjs");
|
|
5
7
|
const require_runtime = require("../runtime.cjs");
|
|
@@ -19,10 +21,11 @@ async function executeJourney(input, parts) {
|
|
|
19
21
|
const { workflowId, contentHash, userId } = params;
|
|
20
22
|
const { store, env, observer, contract } = parts;
|
|
21
23
|
const runtime = require_runtime.resolveRuntime({ current: parts.runtime }, "current");
|
|
22
|
-
const
|
|
24
|
+
const owner = () => require_store_identity.instanceOwner(store, instanceId, userId);
|
|
25
|
+
const finished = async (status, convertedAt) => observer?.finished?.({
|
|
23
26
|
workflow: workflowId,
|
|
24
27
|
instanceId,
|
|
25
|
-
userId,
|
|
28
|
+
userId: await owner(),
|
|
26
29
|
status,
|
|
27
30
|
at: Date.now(),
|
|
28
31
|
...convertedAt === void 0 ? {} : { convertedAt }
|
|
@@ -37,12 +40,12 @@ async function executeJourney(input, parts) {
|
|
|
37
40
|
}
|
|
38
41
|
await require_host_ingest.ingestEvent(env, {
|
|
39
42
|
id,
|
|
40
|
-
userId,
|
|
43
|
+
userId: await owner(),
|
|
41
44
|
event: name,
|
|
42
45
|
payload: accepted
|
|
43
46
|
}, store);
|
|
44
47
|
} };
|
|
45
|
-
if (!await require_host_fresh_read.readWithRetry(() => store.admitInstance(instanceId,
|
|
48
|
+
if (!await require_host_fresh_read.readWithRetry(async () => store.admitInstance(instanceId, await owner()), {
|
|
46
49
|
what: "admission",
|
|
47
50
|
instanceId
|
|
48
51
|
})) {
|
|
@@ -63,9 +66,10 @@ async function executeJourney(input, parts) {
|
|
|
63
66
|
outcome = await require_engine_interpreter.runJourney(ir, {
|
|
64
67
|
userId,
|
|
65
68
|
instanceId,
|
|
66
|
-
|
|
69
|
+
currentUserId: owner,
|
|
70
|
+
enteredAtMs: Math.min(input.enteredAtMs, input.params.trigger.ts ?? input.enteredAtMs),
|
|
67
71
|
step,
|
|
68
|
-
facts: new require_store_facts.JourneyFactSource(store, userId),
|
|
72
|
+
facts: new require_store_facts.JourneyFactSource(store, userId, owner),
|
|
69
73
|
messages,
|
|
70
74
|
events,
|
|
71
75
|
actions: parts.actions,
|