@shware/workflow 6.1.1 → 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 +132 -35
- 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/aws/index.cjs +3 -2
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.mjs +3 -2
- package/dist/aws/index.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 +13 -2
- 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 +13 -2
- 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/email-sender.cjs +8 -1
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +2 -2
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +2 -2
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +9 -2
- package/dist/email-sender.mjs.map +1 -1
- package/dist/email.cjs +5 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +2 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +2 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +5 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/condition.cjs +7 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +3 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +3 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +7 -5
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/interpreter.cjs +11 -6
- 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 +11 -6
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +21 -3
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +21 -3
- 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 +31 -90
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +4 -2
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +4 -2
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +25 -84
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/ingest.cjs +164 -25
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +38 -8
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +38 -8
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +161 -26
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/host/journey-host.cjs +8 -1
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +10 -4
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +10 -4
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +8 -1
- 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/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- 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/reporting/observer.cjs +1 -0
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.mjs +1 -0
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/store/facts.cjs +29 -9
- package/dist/store/facts.cjs.map +1 -1
- package/dist/store/facts.d.cts +7 -2
- package/dist/store/facts.d.cts.map +1 -1
- package/dist/store/facts.d.mts +7 -2
- package/dist/store/facts.d.mts.map +1 -1
- package/dist/store/facts.mjs +29 -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 +119 -25
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +119 -25
- 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-reporting.cjs +4 -0
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +1 -0
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +1 -0
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +4 -0
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +271 -55
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +29 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +29 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +272 -56
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +28 -9
- 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 +28 -9
- 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
package/dist/engine/ports.d.cts
CHANGED
|
@@ -67,11 +67,15 @@ export interface FactSource {
|
|
|
67
67
|
* How many times an event occurred. `sinceMs` bounds the window (within /
|
|
68
68
|
* anchoring); `where` is a payload-only condition tree — count only events
|
|
69
69
|
* whose payload matches (evaluate with matchesWhere from engine/condition).
|
|
70
|
+
* `atLeast` is the threshold the caller compares against: an implementation
|
|
71
|
+
* may stop counting there and return any number ≥ it, or ignore it and count
|
|
72
|
+
* them all.
|
|
70
73
|
*/
|
|
71
74
|
countEvents(event: string, opts?: {
|
|
72
75
|
sinceMs?: number;
|
|
73
76
|
untilMs?: number;
|
|
74
77
|
where?: ConditionIR;
|
|
78
|
+
atLeast?: number;
|
|
75
79
|
}): Promise<number>;
|
|
76
80
|
getProperty(path: string): Promise<ScalarIR | undefined>;
|
|
77
81
|
/** Resolve a segment definition by id (stored with the bundle at deploy time). */
|
|
@@ -106,9 +110,12 @@ export interface OutboundMessage {
|
|
|
106
110
|
/** Email and push: the template's subscription topic, null for a transactional message (absent for other channels). */
|
|
107
111
|
topic?: string | null | undefined;
|
|
108
112
|
/**
|
|
109
|
-
* `${instanceId}:${nodeId}` (plus `:${token}` for a push copy) — one per copy
|
|
110
|
-
*
|
|
111
|
-
*
|
|
113
|
+
* `${instanceId}:${nodeId}` (plus `:${token}` for a push copy) — one per copy, stable across
|
|
114
|
+
* replays and retries. The engine skips a copy the observer reports settled
|
|
115
|
+
* (`messageSettled`); past that, de-duplication is the provider's: APNs (collapse id) and Web
|
|
116
|
+
* Push (`Topic`) collapse on it, SES and the Cloudflare REST API cannot, so a crash between the
|
|
117
|
+
* provider taking a copy and its report still sends it twice there. It is also the copy's
|
|
118
|
+
* identity for tracking: a push carries it in its payload, so the app can report the tap.
|
|
112
119
|
*/
|
|
113
120
|
idempotencyKey: string;
|
|
114
121
|
/** What the host's observer asked the sender to add for measuring this copy (see `MessageTracking`). */
|
|
@@ -185,6 +192,15 @@ export interface JourneyObserver {
|
|
|
185
192
|
nodeId: string;
|
|
186
193
|
at: number;
|
|
187
194
|
}): Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* Whether this copy (its idempotency key) was already settled — sent, or settled undelivered —
|
|
197
|
+
* by an earlier run of the same step. The send and its report sit inside one durable step, so
|
|
198
|
+
* an instance that crashes after both but before the step's checkpoint replays the step; a
|
|
199
|
+
* settled copy is then skipped instead of reaching the transport a second time. (A crash
|
|
200
|
+
* between the transport taking it and the report is still a second send: only a provider that
|
|
201
|
+
* de-duplicates on the key closes that.)
|
|
202
|
+
*/
|
|
203
|
+
messageSettled?(message: OutboundMessage): Promise<boolean>;
|
|
188
204
|
/** A copy is about to go out; the tracking returned travels with it to the sender. */
|
|
189
205
|
messagePrepared?(message: OutboundMessage): Promise<MessageTracking | undefined>;
|
|
190
206
|
/** The transport took the copy (`delivered: true`), or settled it undelivered; a thrown send is not reported. */
|
|
@@ -256,6 +272,8 @@ export interface JourneyContext {
|
|
|
256
272
|
* Date.now() at run time.
|
|
257
273
|
*/
|
|
258
274
|
enteredAtMs: number;
|
|
275
|
+
/** Who the instance belongs to now, for one that can change hands; `userId` otherwise. */
|
|
276
|
+
currentUserId?: (() => Promise<string>) | undefined;
|
|
259
277
|
step: EngineStep;
|
|
260
278
|
facts: FactSource;
|
|
261
279
|
messages: MessageSender;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;EAMnD,MAAM,cAAc,aAAa;;;;;;;;;;;;;;;EAgBjC,UACE,cACA,2BACA;IAAY;MACX;;EAGH,YAAY,eAAe;;;;;;;;;;;;EAa3B,YACE,cACA;IAAQ;IAAmB;MAC1B;;;iBAIY;EACf;EACA;;;iBAIe
|
|
1
|
+
{"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;EAMnD,MAAM,cAAc,aAAa;;;;;;;;;;;;;;;EAgBjC,UACE,cACA,2BACA;IAAY;MACX;;EAGH,YAAY,eAAe;;;;;;;;;;;;EAa3B,YACE,cACA;IAAQ;IAAmB;MAC1B;;;iBAIY;EACf;EACA;;;iBAIe;;;;;;;;;EASf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;IAAa;MACjE;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;;;;;EAMvB,cAAc,SAAS,WAAW,iBAAiB;;iBAGpC;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;EAET;;;;;;;;;EASA;;EAEA,WAAW;;;YAID;;;;;;iBAOK;;;;;;EAMf;;;;;EAKA,QAAQ,aAAa,SAAS;;EAE9B;;;;;;EAMA;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;;;;;;;;EASJ,gBAAgB,SAAS,kBAAkB;;EAE3C,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;;EAEA,uBAAuB;EACvB,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
|
package/dist/engine/ports.d.mts
CHANGED
|
@@ -67,11 +67,15 @@ export interface FactSource {
|
|
|
67
67
|
* How many times an event occurred. `sinceMs` bounds the window (within /
|
|
68
68
|
* anchoring); `where` is a payload-only condition tree — count only events
|
|
69
69
|
* whose payload matches (evaluate with matchesWhere from engine/condition).
|
|
70
|
+
* `atLeast` is the threshold the caller compares against: an implementation
|
|
71
|
+
* may stop counting there and return any number ≥ it, or ignore it and count
|
|
72
|
+
* them all.
|
|
70
73
|
*/
|
|
71
74
|
countEvents(event: string, opts?: {
|
|
72
75
|
sinceMs?: number;
|
|
73
76
|
untilMs?: number;
|
|
74
77
|
where?: ConditionIR;
|
|
78
|
+
atLeast?: number;
|
|
75
79
|
}): Promise<number>;
|
|
76
80
|
getProperty(path: string): Promise<ScalarIR | undefined>;
|
|
77
81
|
/** Resolve a segment definition by id (stored with the bundle at deploy time). */
|
|
@@ -106,9 +110,12 @@ export interface OutboundMessage {
|
|
|
106
110
|
/** Email and push: the template's subscription topic, null for a transactional message (absent for other channels). */
|
|
107
111
|
topic?: string | null | undefined;
|
|
108
112
|
/**
|
|
109
|
-
* `${instanceId}:${nodeId}` (plus `:${token}` for a push copy) — one per copy
|
|
110
|
-
*
|
|
111
|
-
*
|
|
113
|
+
* `${instanceId}:${nodeId}` (plus `:${token}` for a push copy) — one per copy, stable across
|
|
114
|
+
* replays and retries. The engine skips a copy the observer reports settled
|
|
115
|
+
* (`messageSettled`); past that, de-duplication is the provider's: APNs (collapse id) and Web
|
|
116
|
+
* Push (`Topic`) collapse on it, SES and the Cloudflare REST API cannot, so a crash between the
|
|
117
|
+
* provider taking a copy and its report still sends it twice there. It is also the copy's
|
|
118
|
+
* identity for tracking: a push carries it in its payload, so the app can report the tap.
|
|
112
119
|
*/
|
|
113
120
|
idempotencyKey: string;
|
|
114
121
|
/** What the host's observer asked the sender to add for measuring this copy (see `MessageTracking`). */
|
|
@@ -185,6 +192,15 @@ export interface JourneyObserver {
|
|
|
185
192
|
nodeId: string;
|
|
186
193
|
at: number;
|
|
187
194
|
}): Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* Whether this copy (its idempotency key) was already settled — sent, or settled undelivered —
|
|
197
|
+
* by an earlier run of the same step. The send and its report sit inside one durable step, so
|
|
198
|
+
* an instance that crashes after both but before the step's checkpoint replays the step; a
|
|
199
|
+
* settled copy is then skipped instead of reaching the transport a second time. (A crash
|
|
200
|
+
* between the transport taking it and the report is still a second send: only a provider that
|
|
201
|
+
* de-duplicates on the key closes that.)
|
|
202
|
+
*/
|
|
203
|
+
messageSettled?(message: OutboundMessage): Promise<boolean>;
|
|
188
204
|
/** A copy is about to go out; the tracking returned travels with it to the sender. */
|
|
189
205
|
messagePrepared?(message: OutboundMessage): Promise<MessageTracking | undefined>;
|
|
190
206
|
/** The transport took the copy (`delivered: true`), or settled it undelivered; a thrown send is not reported. */
|
|
@@ -256,6 +272,8 @@ export interface JourneyContext {
|
|
|
256
272
|
* Date.now() at run time.
|
|
257
273
|
*/
|
|
258
274
|
enteredAtMs: number;
|
|
275
|
+
/** Who the instance belongs to now, for one that can change hands; `userId` otherwise. */
|
|
276
|
+
currentUserId?: (() => Promise<string>) | undefined;
|
|
259
277
|
step: EngineStep;
|
|
260
278
|
facts: FactSource;
|
|
261
279
|
messages: MessageSender;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;EAMnD,MAAM,cAAc,aAAa;;;;;;;;;;;;;;;EAgBjC,UACE,cACA,2BACA;IAAY;MACX;;EAGH,YAAY,eAAe;;;;;;;;;;;;EAa3B,YACE,cACA;IAAQ;IAAmB;MAC1B;;;iBAIY;EACf;EACA;;;iBAIe
|
|
1
|
+
{"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;EAMnD,MAAM,cAAc,aAAa;;;;;;;;;;;;;;;EAgBjC,UACE,cACA,2BACA;IAAY;MACX;;EAGH,YAAY,eAAe;;;;;;;;;;;;EAa3B,YACE,cACA;IAAQ;IAAmB;MAC1B;;;iBAIY;EACf;EACA;;;iBAIe;;;;;;;;;EASf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;IAAa;MACjE;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;;;;;EAMvB,cAAc,SAAS,WAAW,iBAAiB;;iBAGpC;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;EAET;;;;;;;;;EASA;;EAEA,WAAW;;;YAID;;;;;;iBAOK;;;;;;EAMf;;;;;EAKA,QAAQ,aAAa,SAAS;;EAE9B;;;;;;EAMA;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;;;;;;;;EASJ,gBAAgB,SAAS,kBAAkB;;EAE3C,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;;EAEA,uBAAuB;EACvB,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
|
|
@@ -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"}
|