@genn-inc/cluebase-frontend-sdk 0.0.6 → 0.0.8

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.
Files changed (62) hide show
  1. package/dist/_generated/correlation-attribute-keys.d.ts +3 -0
  2. package/dist/_generated/correlation-attribute-keys.js +34 -0
  3. package/dist/_generated/diagnostic-codes.d.ts +4 -0
  4. package/dist/_generated/diagnostic-codes.js +48 -0
  5. package/dist/_generated/drop-reasons.d.ts +2 -0
  6. package/dist/_generated/drop-reasons.js +25 -0
  7. package/dist/_generated/ingest-routes.d.ts +5 -0
  8. package/dist/_generated/ingest-routes.js +12 -0
  9. package/dist/context/trace.js +2 -4
  10. package/dist/exporter/batch.d.ts +23 -26
  11. package/dist/exporter/batch.js +247 -289
  12. package/dist/exporter/browser-outbox-records.d.ts +7 -24
  13. package/dist/exporter/browser-outbox-records.js +6 -94
  14. package/dist/exporter/browser-outbox.d.ts +23 -28
  15. package/dist/exporter/browser-outbox.js +99 -211
  16. package/dist/exporter/client.d.ts +24 -34
  17. package/dist/exporter/client.js +105 -318
  18. package/dist/exporter/delivery-pipeline.d.ts +71 -0
  19. package/dist/exporter/delivery-pipeline.js +196 -0
  20. package/dist/exporter/delivery-response-contract.d.ts +1 -81
  21. package/dist/exporter/delivery-response-contract.js +0 -168
  22. package/dist/exporter/delivery-response.d.ts +7 -4
  23. package/dist/exporter/delivery-response.js +59 -133
  24. package/dist/exporter/diagnostics.d.ts +2 -1
  25. package/dist/exporter/diagnostics.js +1 -25
  26. package/dist/exporter/event-id.d.ts +1 -0
  27. package/dist/exporter/event-id.js +3 -0
  28. package/dist/exporter/observation-source-event-exporter.d.ts +1 -1
  29. package/dist/exporter/observation-source-event-exporter.js +2 -2
  30. package/dist/exporter/sdk-core-delivery.d.ts +225 -0
  31. package/dist/exporter/sdk-core-delivery.js +97 -0
  32. package/dist/exporter/sdk-core-wasm.d.ts +7 -1
  33. package/dist/exporter/sdk-core-wasm.js +24 -3
  34. package/dist/exporter/source-event.d.ts +2 -0
  35. package/dist/exporter/source-event.js +17 -0
  36. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  37. package/dist/exporter/span-to-source-signal.js +20 -140
  38. package/dist/index.d.ts +3 -3
  39. package/dist/index.js +5 -3
  40. package/dist/instrumentation/network.d.ts +1 -1
  41. package/dist/instrumentation/network.js +17 -0
  42. package/dist/provider/_version.d.ts +1 -1
  43. package/dist/provider/_version.js +1 -1
  44. package/dist/provider/config.js +16 -3
  45. package/dist/provider/constants.js +0 -1
  46. package/dist/provider/lifecycle-event-dedup.d.ts +7 -0
  47. package/dist/provider/lifecycle-event-dedup.js +31 -0
  48. package/dist/provider/sdk-event-pipeline.d.ts +1 -3
  49. package/dist/provider/sdk-event-pipeline.js +0 -15
  50. package/dist/provider/sdk-otel-pipeline.d.ts +2 -1
  51. package/dist/provider/sdk-otel-pipeline.js +2 -11
  52. package/dist/provider/sdk-runtime-surfaces.d.ts +3 -2
  53. package/dist/provider/sdk-runtime-surfaces.js +7 -6
  54. package/dist/provider/sdk.js +88 -54
  55. package/dist/provider/types.d.ts +12 -19
  56. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +12 -9
  57. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +117 -29
  58. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  59. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +8 -9
  60. package/package.json +1 -1
  61. package/dist/provider/usage-governor.d.ts +0 -7
  62. package/dist/provider/usage-governor.js +0 -114
@@ -1,22 +1,30 @@
1
- import { CONSENT_DENIED_ABORT_REASON, DEFAULT_CONFIG, } from "../provider/constants.js";
2
- import { BrowserOutbox, } from "./browser-outbox.js";
3
- import { ownEvent } from "./browser-outbox-records.js";
1
+ import { DEFAULT_CONFIG } from "../provider/constants.js";
2
+ import { applyDeliveredPrivacyPolicy, resetPrivacyPolicy, } from "../provider/privacy-policy-store.js";
3
+ import { BrowserOutbox } from "./browser-outbox.js";
4
+ import { ownEvent, sentByteLength } from "./browser-outbox-records.js";
5
+ import { DeliveryPipeline, } from "./delivery-pipeline.js";
4
6
  import { DiagnosticStore } from "./diagnostics.js";
7
+ const CLOSE_DEADLINE_MS = 5000;
8
+ const CORE_CONSENT = {
9
+ unknown: "unknown",
10
+ granted: "granted",
11
+ denied: "denied",
12
+ };
5
13
  export class BatchTransport {
6
14
  constructor(options) {
7
- this.activeSendController = null;
8
- this.closeDeadlineController = null;
9
- this.closeDeadlineTimer = null;
15
+ this.idleHandle = null;
16
+ this.flushing = false;
17
+ this.flushAgainAfterCurrent = false;
10
18
  this.closeDeadlineAt = null;
11
- this.closePermitOrdinal = null;
12
- this.flushDeadlineController = null;
13
- this.flushDeadlineTimer = null;
14
- this.flushDeadlineAt = null;
19
+ this.clearedAt = null;
20
+ this.disposed = false;
21
+ this.closeDeadlineTimer = null;
15
22
  this.lifecycle = "open";
16
23
  this.closeHadTerminalDisposition = false;
24
+ this.restored = false;
25
+ this.tokenRefusedForGood = false;
17
26
  this.activeFlushPromise = null;
18
27
  this.enqueueTail = Promise.resolve();
19
- this.seenEventIds = new Set();
20
28
  this.enabled = options.enabled;
21
29
  this.flushIntervalMs = Math.max(100, options.flushIntervalMs);
22
30
  this.maxPayloadBytes = Math.max(1024, options.maxPayloadBytes);
@@ -25,29 +33,44 @@ export class BatchTransport {
25
33
  this.client = options.client;
26
34
  this.metrics = options.metrics ?? null;
27
35
  this.diagnostics = options.diagnostics ?? new DiagnosticStore();
28
- this.onGovernorFeedback = options.onGovernorFeedback ?? null;
29
36
  this.outbox = new BrowserOutbox({
30
37
  projectKey: options.projectKey ?? "unconfigured",
31
38
  initialConsent: options.initialConsent ?? "unknown",
32
39
  metrics: this.metrics ?? undefined,
33
40
  diagnostics: this.diagnostics,
34
- maxEvents: options.queueMaxItems ?? 5000,
35
- maxBytes: options.queueMaxBytes ?? 300 * 1024,
36
41
  onConsentRevoked: () => this.abortActiveSendForConsent(),
37
42
  ...(options.projectKey ? {} : { storage: null }),
38
43
  });
39
- this.idleHandle = null;
40
- this.flushing = false;
41
- this.flushAgainAfterCurrent = false;
44
+ this.pipeline = new DeliveryPipeline({
45
+ endpoint: options.endpoint,
46
+ projectKey: options.projectKey ?? "unconfigured",
47
+ sdkVersion: options.sdkVersion,
48
+ sourceSchemaVersion: options.sourceSchemaVersion,
49
+ sdkRequest: options.sdkRequest,
50
+ queueLimits: {
51
+ max_payload_bytes: this.maxPayloadBytes,
52
+ max_buffer_bytes: options.queueMaxBytes ?? 300 * 1024,
53
+ max_buffer_events: options.queueMaxItems ?? 5000,
54
+ },
55
+ ...(options.requestTimeoutMs === undefined
56
+ ? {}
57
+ : { requestTimeoutMs: options.requestTimeoutMs }),
58
+ initialConsent: CORE_CONSENT[options.initialConsent ?? "unknown"],
59
+ transport: (request) => this.client.perform(request),
60
+ clock: {
61
+ nowMs: monotonicNow,
62
+ sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
63
+ },
64
+ });
65
+ this.adoptStoredRecords();
42
66
  this.recordPendingBatchCount();
43
67
  }
44
68
  getDiagnostics() {
45
69
  return this.diagnostics.snapshot();
46
70
  }
47
71
  start() {
48
- if (!this.enabled) {
72
+ if (!this.enabled)
49
73
  return;
50
- }
51
74
  }
52
75
  stop() {
53
76
  if (this.idleHandle) {
@@ -56,55 +79,64 @@ export class BatchTransport {
56
79
  }
57
80
  }
58
81
  dispose() {
82
+ this.disposed = true;
59
83
  this.stop();
60
84
  this.outbox.close();
85
+ this.pipeline.dispose();
61
86
  }
62
87
  currentConsent() {
63
88
  return this.outbox.currentConsent();
64
89
  }
65
90
  capturePermit() {
66
- return this.outbox.capturePermit();
91
+ return this.pipeline.permit();
67
92
  }
68
93
  setConsent(consent) {
69
- if (consent === "denied") {
94
+ if (consent === "denied")
70
95
  this.abortActiveSendForConsent();
71
- }
72
96
  const transition = this.outbox.setConsent(consent);
97
+ const report = this.pipeline.declareConsent(CORE_CONSENT[consent]);
98
+ this.outbox.releaseEvents(report.purged_event_keys);
99
+ for (const entry of report.diagnostics)
100
+ this.diagnostics.record(entry);
73
101
  void this.queueOutboxMutation(async () => {
74
102
  await this.outbox.withDeliveryLock(async () => this.outbox.commitConsentTransition(transition), { restore: false });
75
103
  this.recordPendingBatchCount();
76
104
  });
77
105
  }
78
106
  enqueue(event, permit) {
79
- if (this.lifecycle !== "open" &&
80
- !(this.lifecycle === "closing" &&
81
- permit !== undefined &&
82
- this.closePermitOrdinal !== null &&
83
- permit.captureOrdinal < this.closePermitOrdinal)) {
84
- this.metrics?.increment("dropped_count");
85
- return;
86
- }
87
- const collectionPermit = permit ?? this.outbox.capturePermit();
88
- const owned = ownEvent(event, collectionPermit);
89
- const eventId = event.event_id;
90
- if (!owned || typeof eventId !== "string" || eventId.length === 0) {
107
+ const collectionPermit = permit ?? this.pipeline.permit();
108
+ const owned = ownEvent(event);
109
+ if (!owned) {
91
110
  this.metrics?.increment("dropped_count");
92
111
  return;
93
112
  }
113
+ const snapshot = owned.value();
94
114
  void this.queueOutboxMutation(async () => {
95
- if (this.seenEventIds.has(eventId)) {
96
- return;
97
- }
98
- const retained = await this.outbox.withDeliveryLock(async () => this.outbox.addDraft(owned.value(), owned.permit));
99
- if (!retained) {
115
+ const draft = await this.outbox.withDeliveryLock(async () => {
116
+ this.adoptStoredRecords();
117
+ return this.outbox.holdDraft(snapshot);
118
+ });
119
+ if (!draft) {
100
120
  this.metrics?.increment("dropped_count");
101
121
  return;
102
122
  }
103
- this.seenEventIds.add(eventId);
104
- if (this.seenEventIds.size > BatchTransport.DEDUP_CAPACITY) {
105
- const oldest = this.seenEventIds.values().next().value;
106
- if (oldest !== undefined)
107
- this.seenEventIds.delete(oldest);
123
+ const report = this.pipeline.admit({
124
+ eventKey: draft.eventKeyHash,
125
+ byteLength: sentByteLength(draft.event),
126
+ diagnosticCanonicalBytes: this.client.diagnosticCanonicalPayload(JSON.stringify(draft.event)),
127
+ }, collectionPermit);
128
+ for (const entry of report.diagnostics)
129
+ this.diagnostics.record(entry);
130
+ this.outbox.releaseEvents(report.overflowed_event_keys);
131
+ if (report.overflowed_event_keys.length > 0) {
132
+ this.metrics?.increment("dropped_count", report.overflowed_event_keys.length);
133
+ }
134
+ if (!report.retained) {
135
+ if (report.overflowed_event_keys.length === 0) {
136
+ this.outbox.releaseEvents([draft.eventKeyHash]);
137
+ this.metrics?.increment("dropped_count");
138
+ }
139
+ return;
108
140
  }
109
141
  this.recordPendingBatchCount();
110
142
  this.scheduleFlushDeadline();
@@ -112,71 +144,33 @@ export class BatchTransport {
112
144
  void this.flush();
113
145
  return;
114
146
  }
115
- if (this.outbox.draftCount() >= this.maxSize ||
116
- this.outbox.estimatedDraftBytes((events, metadata) => this.client.measurePayloadBytes([...events], metadata), this.client.createBatchEnvelopeMetadata()) >= this.flushThresholdBytes) {
147
+ const depth = this.pipeline.depth(this.envelopeOverheadBytes());
148
+ if (depth.draft_count >= this.maxSize ||
149
+ depth.draft_body_bytes >= this.flushThresholdBytes) {
117
150
  void this.flush();
118
151
  }
119
152
  });
120
153
  }
121
- scheduleFlushDeadline() {
122
- if (!this.enabled) {
123
- return;
124
- }
125
- if (this.idleHandle) {
126
- return;
127
- }
128
- this.idleHandle = setTimeout(() => {
129
- this.idleHandle = null;
130
- void this.flush();
131
- }, this.flushIntervalMs);
132
- }
133
- beginFlushDeadline() {
134
- const active = this.currentDeadline();
135
- if (active)
136
- return active;
137
- const controller = new AbortController();
138
- const deadlineAt = monotonicNow() + 5000;
139
- this.flushDeadlineController = controller;
140
- this.flushDeadlineAt = deadlineAt;
141
- this.flushDeadlineTimer = setTimeout(() => {
142
- controller.abort("shutdown_deadline");
143
- this.activeSendController?.abort("shutdown_deadline");
144
- }, 5000);
145
- return { signal: controller.signal, deadlineAt };
146
- }
147
- endFlushDeadline(deadline) {
148
- if (!deadline || this.flushDeadlineController?.signal !== deadline.signal)
149
- return;
150
- if (this.flushDeadlineTimer)
151
- clearTimeout(this.flushDeadlineTimer);
152
- this.flushDeadlineTimer = null;
153
- this.flushDeadlineController = null;
154
- this.flushDeadlineAt = null;
155
- }
156
- async flush(deadline = this.currentDeadline()) {
154
+ async flush() {
157
155
  if (this.lifecycle === "closed")
158
156
  return;
157
+ this.tokenRefusedForGood = false;
159
158
  if (this.flushing) {
160
159
  this.flushAgainAfterCurrent = true;
161
- if (this.activeFlushPromise) {
160
+ if (this.activeFlushPromise)
162
161
  await this.activeFlushPromise;
163
- }
164
162
  return;
165
163
  }
166
- const enqueuesAtStart = this.enqueueTail;
167
- await enqueuesAtStart;
164
+ await this.enqueueTail;
168
165
  if (this.flushing) {
169
166
  this.flushAgainAfterCurrent = true;
170
167
  if (this.activeFlushPromise)
171
168
  await this.activeFlushPromise;
172
169
  return;
173
170
  }
174
- if (this.idleHandle) {
175
- clearTimeout(this.idleHandle);
176
- this.idleHandle = null;
177
- }
171
+ this.stop();
178
172
  this.flushing = true;
179
- this.activeFlushPromise = this.runFlushUntilIdle(deadline);
173
+ this.activeFlushPromise = this.runFlushUntilIdle();
180
174
  try {
181
175
  await this.activeFlushPromise;
182
176
  }
@@ -184,56 +178,46 @@ export class BatchTransport {
184
178
  this.activeFlushPromise = null;
185
179
  this.flushing = false;
186
180
  }
187
- this.diagnostics.setFlushOutcome(this.outbox.pendingEventCount() === 0 ? "clean" : "failed");
188
- return;
181
+ this.diagnostics.setFlushOutcome(this.pipeline.pendingCount() === 0 ? "clean" : "failed");
189
182
  }
190
- async runFlushUntilIdle(deadline) {
183
+ async runFlushUntilIdle() {
191
184
  while (true) {
192
185
  const enqueuesAtCycleStart = this.enqueueTail;
193
186
  await enqueuesAtCycleStart;
194
- await this.outbox.withDeliveryLock(() => this.runFlushCycle(deadline));
195
- const enqueuesAfterCycle = this.enqueueTail;
196
- if (this.lifecycle !== "open" ||
187
+ const stop = await this.outbox.withDeliveryLock(() => this.runFlushCycle());
188
+ if (stop === "retries_exhausted" ||
189
+ this.lifecycle !== "open" ||
197
190
  (!this.flushAgainAfterCurrent &&
198
- enqueuesAfterCycle === enqueuesAtCycleStart)) {
191
+ this.enqueueTail === enqueuesAtCycleStart)) {
199
192
  return;
200
193
  }
201
194
  this.flushAgainAfterCurrent = false;
202
195
  }
203
196
  }
204
197
  beginClose() {
205
- if (this.lifecycle === "closed")
206
- return null;
207
- if (this.lifecycle === "closing" && this.closeDeadlineController) {
208
- return this.closeDeadlineController.signal;
209
- }
198
+ if (this.lifecycle === "closed" || this.lifecycle === "closing")
199
+ return;
210
200
  this.lifecycle = "closing";
211
201
  this.closeHadTerminalDisposition = false;
212
202
  this.diagnostics.setLifecycle("closing");
213
203
  this.stop();
214
- const controller = new AbortController();
215
- this.closeDeadlineController = controller;
216
- this.closeDeadlineAt = monotonicNow() + 5000;
217
- this.closePermitOrdinal = this.outbox.capturePermit().captureOrdinal;
204
+ this.pipeline.beginClose();
205
+ this.closeDeadlineAt = monotonicNow() + CLOSE_DEADLINE_MS;
218
206
  this.closeDeadlineTimer = setTimeout(() => {
219
- controller.abort("shutdown_deadline");
220
- this.activeSendController?.abort("shutdown_deadline");
221
- }, 5000);
222
- return controller.signal;
207
+ this.client.abortAttempt("deadline");
208
+ }, CLOSE_DEADLINE_MS);
223
209
  }
224
210
  async close() {
225
211
  if (this.lifecycle === "closed")
226
212
  return;
227
- const signal = this.beginClose();
228
- if (!signal)
229
- return;
213
+ this.beginClose();
230
214
  try {
231
215
  await this.flush();
232
216
  }
233
217
  finally {
234
- this.clearCloseDeadline(signal);
218
+ this.clearCloseDeadline();
235
219
  }
236
- if (this.outbox.pendingEventCount() > 0) {
220
+ if (this.pipeline.pendingCount() > 0) {
237
221
  this.lifecycle = "closing_failed";
238
222
  this.diagnostics.setLifecycle("closing_failed");
239
223
  this.diagnostics.setCloseOutcome("retryable_failure");
@@ -244,211 +228,185 @@ export class BatchTransport {
244
228
  this.diagnostics.setLifecycle("closed");
245
229
  this.diagnostics.setCloseOutcome(this.closeHadTerminalDisposition ? "terminal_failure" : "clean");
246
230
  }
247
- async runFlushCycle(deadline) {
248
- const fence = this.outbox.prepareFence({
249
- maxPayloadBytes: this.maxPayloadBytes,
250
- createMetadata: () => this.client.createBatchEnvelopeMetadata(),
251
- measure: (events, metadata) => this.client.measurePayloadBytes([...events], metadata),
252
- });
253
- if (fence.oversized > 0)
254
- this.recordPendingBatchCount();
255
- while (true) {
256
- const batch = this.outbox.headThrough(fence.targetOrdinal);
257
- if (!batch) {
258
- break;
259
- }
260
- if (batch.events.length === 0 ||
261
- batch.events.some((event) => !this.outbox.permits(event.permit))) {
262
- this.outbox.removeHead(batch);
263
- this.recordPendingBatchCount();
264
- continue;
265
- }
266
- this.metrics?.increment("batch_flush_count");
267
- const result = await this.sendBatchWhilePermitted(batch, deadline);
268
- if (!result || this.applyDeliveryResult(batch, result) === "break")
269
- break;
270
- }
231
+ flushOnUnload() {
232
+ if (!this.client.hasUsableBrowserToken())
233
+ return;
234
+ this.beginClose();
235
+ void this.flush();
271
236
  }
272
- async sendBatchWhilePermitted(batch, deadline) {
273
- const sendController = new AbortController();
274
- const abortForClose = () => sendController.abort("shutdown_deadline");
275
- if (deadline?.signal.aborted)
276
- abortForClose();
277
- deadline?.signal.addEventListener("abort", abortForClose, { once: true });
278
- this.activeSendController = sendController;
279
- let result;
280
- try {
281
- result = await this.client.send(batch.events.map((event) => event.value()), {
282
- maxPayloadBytes: this.maxPayloadBytes,
283
- metadata: batch.metadata,
284
- signal: sendController.signal,
285
- deadlineAt: deadline?.deadlineAt,
237
+ clear() {
238
+ this.clearedAt = monotonicNow();
239
+ this.client.abortAttempt("shutdown");
240
+ this.outbox.clearMemory();
241
+ if (this.flushing && this.activeFlushPromise) {
242
+ void this.activeFlushPromise.finally(() => {
243
+ if (this.disposed)
244
+ return;
245
+ this.pipeline.reset();
246
+ this.clearedAt = null;
286
247
  });
287
248
  }
288
- finally {
289
- deadline?.signal.removeEventListener("abort", abortForClose);
290
- if (this.activeSendController === sendController) {
291
- this.activeSendController = null;
292
- }
293
- }
294
- if (batch.events.some((event) => !this.outbox.permits(event.permit))) {
295
- this.recordPendingBatchCount();
296
- return null;
249
+ else {
250
+ this.pipeline.reset();
251
+ this.clearedAt = null;
297
252
  }
298
- return result;
299
- }
300
- applyDeliveryResult(batch, result) {
301
- if (result.governor)
302
- this.onGovernorFeedback?.(result.governor);
303
- return result.ok
304
- ? this.applySuccessfulDelivery(batch, result)
305
- : this.applyFailedDelivery(batch, result);
253
+ void this.queueOutboxMutation(async () => {
254
+ await this.outbox.withDeliveryLock(async () => this.outbox.clear());
255
+ });
256
+ this.recordPendingBatchCount();
306
257
  }
307
- applyFailedDelivery(batch, result) {
308
- if (result.retryStrategy === "split_batch") {
309
- if (this.outbox.splitHead(batch, () => this.client.createBatchEnvelopeMetadata())) {
310
- this.recordPendingBatchCount();
311
- this.scheduleFlushDeadline();
312
- return "continue";
313
- }
314
- this.metrics?.increment("dropped_count", batch.events.length);
315
- this.metrics?.increment("oversized_event_dropped_count");
316
- this.outbox.removeHead(batch);
258
+ async runFlushCycle() {
259
+ this.adoptStoredRecords();
260
+ if (!(await this.ensureDeliveryToken()))
261
+ return null;
262
+ const fence = this.pipeline.fence(this.envelopeOverheadBytes());
263
+ for (const entry of fence.diagnostics)
264
+ this.diagnostics.record(entry);
265
+ if (fence.oversized_event_keys.length > 0) {
266
+ this.outbox.releaseEvents(fence.oversized_event_keys);
267
+ this.metrics?.increment("oversized_event_count", fence.oversized_event_keys.length);
268
+ this.metrics?.increment("dropped_count", fence.oversized_event_keys.length);
317
269
  this.recordPendingBatchCount();
318
- return "continue";
319
270
  }
320
- if (this.shouldRetainFailedBatch(result.retryStrategy)) {
321
- this.diagnostics.record({
322
- code: "retry_exhausted",
323
- strategy: "retry_same_batch",
324
- attempt: result.attempts,
325
- batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
326
- });
327
- this.outbox.retainHead(batch);
271
+ const summary = await this.pipeline.run({
272
+ body: (batchId, eventKeys) => this.serializeBatch(batchId, eventKeys),
273
+ onObservation: (observation) => this.applyObservation(observation),
274
+ deadlineMs: () => this.closeDeadlineAt ?? this.clearedAt ?? undefined,
275
+ });
276
+ if (summary.stop === "retries_exhausted") {
328
277
  this.metrics?.increment("transport_requeue_count");
329
- this.recordPendingBatchCount();
330
278
  if (this.lifecycle === "open")
331
279
  this.scheduleFlushDeadline();
332
- return "break";
333
280
  }
334
- this.diagnostics.record({
335
- code: result.retryStrategy === "drop_event"
336
- ? "intentional_drop"
337
- : "terminal_rejection",
338
- strategy: result.retryStrategy ?? undefined,
339
- count: batch.events.length,
340
- batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
281
+ this.recordPendingBatchCount();
282
+ return summary.stop;
283
+ }
284
+ serializeBatch(batchId, eventKeys) {
285
+ const claim = this.outbox.claimBatch({
286
+ batchId,
287
+ eventKeys,
288
+ createMetadata: () => this.client.createBatchEnvelopeMetadata(),
341
289
  });
342
- if (this.lifecycle === "closing")
343
- this.closeHadTerminalDisposition = true;
344
- this.metrics?.increment("dropped_count", batch.events.length);
345
- this.metrics?.increment("transport_failed_count");
346
- if (!this.outbox.removeHead(batch)) {
347
- this.metrics?.increment("transport_requeue_count");
348
- this.recordPendingBatchCount();
349
- return "break";
290
+ if (!claim)
291
+ return null;
292
+ const metadata = {
293
+ batch_id: claim.record.batchId,
294
+ idempotency_key: claim.record.idempotencyKey,
295
+ sent_at: claim.record.sentAt,
296
+ };
297
+ const body = this.client.stringifyPayload(claim.record.events.map((event) => event.event), metadata);
298
+ if (claim.minted) {
299
+ this.metrics?.increment("payload_size_bytes", new TextEncoder().encode(body).byteLength);
350
300
  }
351
- this.recordPendingBatchCount();
352
- return "continue";
301
+ return {
302
+ body,
303
+ diagnosticCanonicalBytes: this.client.diagnosticCanonicalPayload(body),
304
+ };
353
305
  }
354
- applySuccessfulDelivery(batch, result) {
355
- if (!this.outbox.removeHead(batch)) {
356
- this.metrics?.increment("transport_requeue_count");
357
- this.recordPendingBatchCount();
358
- return "break";
306
+ applyObservation(observation) {
307
+ for (const entry of observation.diagnostics)
308
+ this.diagnostics.record(entry);
309
+ if (observation.privacyPolicy !== undefined) {
310
+ applyDeliveredPrivacyPolicy(observation.privacyPolicy);
359
311
  }
360
- if (result.partial && this.lifecycle === "closing") {
361
- this.closeHadTerminalDisposition = true;
312
+ else if (observation.directiveUnreadable) {
313
+ resetPrivacyPolicy();
314
+ }
315
+ const progress = observation.progress;
316
+ if (progress.kind !== "retry_scheduled") {
317
+ this.metrics?.increment("batch_flush_count");
318
+ }
319
+ else {
320
+ this.metrics?.increment("retry_count");
321
+ this.metrics?.increment("transport_retry_count");
322
+ }
323
+ if (progress.kind === "settled") {
324
+ this.outbox.releaseBatch(progress.batch_id);
325
+ this.metrics?.increment("sent_count", progress.settled_event_keys.length);
326
+ if (observation.droppedEventCount > 0) {
327
+ this.metrics?.increment("ingest_dropped_event_count", observation.droppedEventCount);
328
+ if (this.lifecycle === "closing") {
329
+ this.closeHadTerminalDisposition = true;
330
+ }
331
+ }
332
+ return;
333
+ }
334
+ if (progress.kind === "dropped_oversized") {
335
+ this.outbox.releaseBatch(progress.batch_id);
336
+ this.metrics?.increment("dropped_count");
337
+ this.metrics?.increment("oversized_event_dropped_count");
338
+ return;
339
+ }
340
+ if (progress.kind === "split_scheduled") {
341
+ this.outbox.reopenBatch(progress.batch_id);
342
+ this.scheduleFlushDeadline();
343
+ return;
344
+ }
345
+ if (progress.kind === "consent_revoked") {
346
+ this.metrics?.increment("transport_failed_count");
347
+ return;
348
+ }
349
+ if (progress.kind === "retries_exhausted") {
350
+ this.metrics?.increment("transport_failed_count");
362
351
  }
363
- this.recordPendingBatchCount();
364
- return "continue";
365
352
  }
366
- flushOnUnload() {
367
- if (!this.client.hasUsableBrowserToken()) {
353
+ adoptStoredRecords() {
354
+ if (this.restored)
368
355
  return;
356
+ this.restored = true;
357
+ const restorable = this.outbox.restorable();
358
+ for (const batch of restorable.batches) {
359
+ const report = this.pipeline.adoptRestoredBatch(batch.ordinal, batch.events);
360
+ if (report.adopted && report.batch_id !== undefined) {
361
+ this.outbox.bindRestoredBatch(batch.storedBatchId, report.batch_id);
362
+ }
363
+ }
364
+ for (const draft of restorable.drafts) {
365
+ this.pipeline.admit({
366
+ eventKey: draft.eventKeyHash,
367
+ byteLength: sentByteLength(draft.event),
368
+ diagnosticCanonicalBytes: this.client.diagnosticCanonicalPayload(JSON.stringify(draft.event)),
369
+ }, this.pipeline.permit());
369
370
  }
370
- this.stop();
371
- void this.queueOutboxMutation(async () => {
372
- await this.outbox.withDeliveryLock(async () => {
373
- const fence = this.outbox.prepareFence({
374
- maxPayloadBytes: this.maxPayloadBytes,
375
- createMetadata: () => this.client.createBatchEnvelopeMetadata(),
376
- measure: (events, metadata) => this.client.measurePayloadBytes([...events], metadata),
377
- });
378
- this.recordPendingBatchCount();
379
- const batch = this.outbox.headThrough(fence.targetOrdinal);
380
- if (!batch)
381
- return;
382
- const sendController = new AbortController();
383
- this.activeSendController = sendController;
384
- try {
385
- await this.client.send(batch.events.map((event) => event.value()), {
386
- keepalive: true,
387
- useBeacon: false,
388
- requireCachedBrowserToken: true,
389
- maxPayloadBytes: this.maxPayloadBytes,
390
- metadata: batch.metadata,
391
- signal: sendController.signal,
392
- });
393
- }
394
- finally {
395
- if (this.activeSendController === sendController) {
396
- this.activeSendController = null;
397
- }
398
- }
399
- });
400
- });
401
371
  }
402
- clear() {
403
- this.activeSendController?.abort();
404
- this.activeSendController = null;
405
- this.outbox.clearMemory();
406
- void this.queueOutboxMutation(async () => {
407
- await this.outbox.withDeliveryLock(async () => this.outbox.clear());
408
- });
409
- this.recordPendingBatchCount();
372
+ async ensureDeliveryToken() {
373
+ const result = await this.client.ensureDeliveryToken();
374
+ if (!result.ok) {
375
+ this.metrics?.increment("send_failure_count");
376
+ this.tokenRefusedForGood = result.retryStrategy === "stop_and_report";
377
+ return false;
378
+ }
379
+ this.tokenRefusedForGood = false;
380
+ this.pipeline.rememberBrowserToken(result.token);
381
+ return true;
382
+ }
383
+ envelopeOverheadBytes() {
384
+ return this.client.measurePayloadBytes([], this.client.createBatchEnvelopeMetadata());
385
+ }
386
+ scheduleFlushDeadline() {
387
+ if (!this.enabled || this.idleHandle || this.tokenRefusedForGood)
388
+ return;
389
+ this.idleHandle = setTimeout(() => {
390
+ this.idleHandle = null;
391
+ void this.flush();
392
+ }, this.flushIntervalMs);
410
393
  }
411
394
  abortActiveSendForConsent() {
412
- this.activeSendController?.abort(CONSENT_DENIED_ABORT_REASON);
413
- this.activeSendController = null;
395
+ this.client.abortAttempt("consent_revoked");
414
396
  }
415
397
  recordPendingBatchCount() {
416
- this.metrics?.set("pending_batch_count", this.outbox.pendingEventCount());
398
+ this.metrics?.set("pending_batch_count", this.pipeline.pendingCount());
417
399
  }
418
- clearCloseDeadline(signal) {
419
- if (this.closeDeadlineController?.signal !== signal)
420
- return;
421
- if (this.closeDeadlineTimer) {
400
+ clearCloseDeadline() {
401
+ if (this.closeDeadlineTimer)
422
402
  clearTimeout(this.closeDeadlineTimer);
423
- this.closeDeadlineTimer = null;
424
- }
425
- this.closeDeadlineController = null;
403
+ this.closeDeadlineTimer = null;
426
404
  this.closeDeadlineAt = null;
427
- this.closePermitOrdinal = null;
428
- }
429
- currentDeadline() {
430
- if (this.closeDeadlineController && this.closeDeadlineAt !== null) {
431
- return {
432
- signal: this.closeDeadlineController.signal,
433
- deadlineAt: this.closeDeadlineAt,
434
- };
435
- }
436
- if (this.flushDeadlineController && this.flushDeadlineAt !== null) {
437
- return {
438
- signal: this.flushDeadlineController.signal,
439
- deadlineAt: this.flushDeadlineAt,
440
- };
441
- }
442
- return null;
443
405
  }
444
406
  queueOutboxMutation(operation) {
445
407
  const next = this.enqueueTail.then(operation, operation);
446
408
  this.enqueueTail = next.catch(() => undefined);
447
409
  return next;
448
410
  }
449
- shouldRetainFailedBatch(retryStrategy) {
450
- return retryStrategy === null || retryStrategy === "retry_same_batch";
451
- }
452
411
  }
453
- BatchTransport.DEDUP_CAPACITY = 1024;
454
412
  const monotonicNow = () => typeof performance !== "undefined" ? performance.now() : Date.now();