@smoothbricks/lmao 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/cloudflare.d.ts +15 -0
  2. package/dist/cloudflare.d.ts.map +1 -0
  3. package/dist/cloudflare.js +14 -0
  4. package/dist/es.js +1 -0
  5. package/dist/lib/JsBufferStrategy.d.ts.map +1 -1
  6. package/dist/lib/JsBufferStrategy.js +12 -0
  7. package/dist/lib/archive/chunkEnvelope.js +1 -0
  8. package/dist/lib/archive/chunkRouting.js +1 -0
  9. package/dist/lib/archive/chunkStats.js +1 -0
  10. package/dist/lib/arrow/capacityStats.js +1 -0
  11. package/dist/lib/arrow/dictionaries.d.ts.map +1 -1
  12. package/dist/lib/buffer/Nanoseconds.d.ts.map +1 -1
  13. package/dist/lib/buffer/Nanoseconds.js +34 -0
  14. package/dist/lib/bufferStrategy.js +21 -0
  15. package/dist/lib/capacityTuning.js +26 -0
  16. package/dist/lib/cleanupDiagnostics.js +2 -2
  17. package/dist/lib/cloudflare/classSplit.d.ts +73 -0
  18. package/dist/lib/cloudflare/classSplit.d.ts.map +1 -0
  19. package/dist/lib/cloudflare/classSplit.js +101 -0
  20. package/dist/lib/cloudflare/collectorClient.d.ts +69 -0
  21. package/dist/lib/cloudflare/collectorClient.d.ts.map +1 -0
  22. package/dist/lib/cloudflare/collectorClient.js +82 -0
  23. package/dist/lib/cloudflare/diagnosticDrain.d.ts +78 -0
  24. package/dist/lib/cloudflare/diagnosticDrain.d.ts.map +1 -0
  25. package/dist/lib/cloudflare/diagnosticDrain.js +120 -0
  26. package/dist/lib/cloudflare/traceRows.d.ts +19 -0
  27. package/dist/lib/cloudflare/traceRows.d.ts.map +1 -0
  28. package/dist/lib/cloudflare/traceRows.js +51 -0
  29. package/dist/lib/codegen/evaluatorGenerator.js +2 -1
  30. package/dist/lib/codegen/fixedPositionWriterGenerator.d.ts +8 -0
  31. package/dist/lib/codegen/fixedPositionWriterGenerator.d.ts.map +1 -1
  32. package/dist/lib/codegen/fixedPositionWriterGenerator.js +18 -1
  33. package/dist/lib/codegen/spanLoggerGenerator.js +1 -0
  34. package/dist/lib/errors/Blocked.d.ts +1 -1
  35. package/dist/lib/errors/Blocked.d.ts.map +1 -1
  36. package/dist/lib/errors/Blocked.js +53 -0
  37. package/dist/lib/errors/CodeError.d.ts.map +1 -1
  38. package/dist/lib/errors/CodeError.js +31 -0
  39. package/dist/lib/errors/RetriesExhausted.d.ts +1 -1
  40. package/dist/lib/errors/RetriesExhausted.d.ts.map +1 -1
  41. package/dist/lib/errors/RetriesExhausted.js +38 -0
  42. package/dist/lib/errors/Transient.d.ts +1 -1
  43. package/dist/lib/errors/Transient.d.ts.map +1 -1
  44. package/dist/lib/errors/Transient.js +31 -0
  45. package/dist/lib/errors/retry-policy.js +1 -0
  46. package/dist/lib/flushScheduler.d.ts +0 -3
  47. package/dist/lib/flushScheduler.d.ts.map +1 -1
  48. package/dist/lib/library.js +1 -0
  49. package/dist/lib/logApi.js +11 -0
  50. package/dist/lib/logBinding.js +13 -0
  51. package/dist/lib/op.d.ts.map +1 -1
  52. package/dist/lib/op.js +15 -0
  53. package/dist/lib/opContext/contextTypes.d.ts +1 -1
  54. package/dist/lib/opContext/contextTypes.d.ts.map +1 -1
  55. package/dist/lib/opContext/createOpGroup.js +1 -0
  56. package/dist/lib/opContext/featureFlagTypes.js +13 -0
  57. package/dist/lib/opContext/opGroupTypes.d.ts +1 -1
  58. package/dist/lib/opContext/opGroupTypes.d.ts.map +1 -1
  59. package/dist/lib/opContext/opGroupTypes.js +16 -0
  60. package/dist/lib/opContext/opTypes.js +14 -0
  61. package/dist/lib/opContext/spanContextTypes.js +19 -0
  62. package/dist/lib/opContext/types.js +15 -0
  63. package/dist/lib/result.d.ts.map +1 -1
  64. package/dist/lib/schema/LogSchema.d.ts.map +1 -1
  65. package/dist/lib/schema/builder.js +1 -0
  66. package/dist/lib/schema/defineFeatureFlags.js +1 -0
  67. package/dist/lib/schema/defineLogSchema.js +10 -0
  68. package/dist/lib/schema/evaluator.d.ts.map +1 -1
  69. package/dist/lib/schema/evaluator.js +1 -0
  70. package/dist/lib/schema/systemSchema.d.ts +1 -1
  71. package/dist/lib/schema/systemSchema.d.ts.map +1 -1
  72. package/dist/lib/schema/systemSchema.js +8 -0
  73. package/dist/lib/schema/types.js +6 -0
  74. package/dist/lib/spanBuffer.d.ts.map +1 -1
  75. package/dist/lib/spanBufferHelpers.js +1 -0
  76. package/dist/lib/spanBufferStats.js +24 -0
  77. package/dist/lib/spanContext.js +1 -0
  78. package/dist/lib/sqlite/sqlite-async-writer.d.ts.map +1 -1
  79. package/dist/lib/sqlite/sqlite-common.d.ts +2 -0
  80. package/dist/lib/sqlite/sqlite-common.d.ts.map +1 -1
  81. package/dist/lib/sqlite/sqlite-common.js +4 -0
  82. package/dist/lib/sqlite/sqlite-d1.js +8 -0
  83. package/dist/lib/sqlite/sqlite-db.js +8 -0
  84. package/dist/lib/sqlite/sqlite-writer.d.ts.map +1 -1
  85. package/dist/lib/testing/bun-harness.d.ts +4 -0
  86. package/dist/lib/testing/bun-harness.d.ts.map +1 -1
  87. package/dist/lib/testing/extractFacts.js +8 -0
  88. package/dist/lib/testing/facts.d.ts.map +1 -1
  89. package/dist/lib/testing/facts.js +19 -0
  90. package/dist/lib/testing/queryable-span.js +16 -0
  91. package/dist/lib/testing/span-query.js +8 -0
  92. package/dist/lib/testing/trace-query.d.ts.map +1 -1
  93. package/dist/lib/testing/trace-query.js +247 -219
  94. package/dist/lib/traceContext.js +8 -0
  95. package/dist/lib/traceId.js +1 -0
  96. package/dist/lib/traceRoot.d.ts +5 -5
  97. package/dist/lib/traceRoot.es.d.ts.map +1 -1
  98. package/dist/lib/traceRoot.es.js +2 -1
  99. package/dist/lib/traceRoot.js +16 -1
  100. package/dist/lib/traceRoot.node.d.ts +2 -2
  101. package/dist/lib/traceRoot.node.d.ts.map +1 -1
  102. package/dist/lib/traceRoot.node.js +4 -3
  103. package/dist/lib/tracer.d.ts.map +1 -1
  104. package/dist/lib/tracer.js +61 -0
  105. package/dist/lib/tracers/ArrayQueueTracer.d.ts.map +1 -1
  106. package/dist/lib/tracers/ArrayQueueTracer.js +1 -0
  107. package/dist/lib/tracers/CompositeTracer.d.ts.map +1 -1
  108. package/dist/lib/tracers/CompositeTracer.js +1 -0
  109. package/dist/lib/tracers/NoOpTracer.d.ts.map +1 -1
  110. package/dist/lib/tracers/NoOpTracer.js +25 -0
  111. package/dist/lib/tracers/SQLiteTracer.d.ts.map +1 -1
  112. package/dist/lib/tracers/StdioTracer.d.ts.map +1 -1
  113. package/dist/lib/tracers/StdioTracer.js +39 -6
  114. package/dist/lib/tracers/TestTracer.d.ts.map +1 -1
  115. package/dist/lib/tracers/TestTracer.js +30 -0
  116. package/dist/lib/types.js +10 -0
  117. package/dist/lib/utf8Cache.d.ts.map +1 -1
  118. package/dist/lib/utf8Cache.js +1 -0
  119. package/dist/lib/wasm/WasmBufferStrategy.d.ts.map +1 -1
  120. package/dist/lib/wasm/WasmBufferStrategy.js +15 -0
  121. package/dist/lib/wasm/wasmAllocator.js +16 -0
  122. package/dist/lib/wasm/wasmSpanBuffer.js +12 -0
  123. package/dist/lib/wasm/wasmTraceRoot.d.ts.map +1 -1
  124. package/dist/lib/wasm/wasmTraceRoot.js +2 -1
  125. package/dist/node.js +1 -0
  126. package/package.json +11 -4
  127. package/src/cloudflare.ts +35 -0
  128. package/src/lib/bufferStrategy.ts +2 -2
  129. package/src/lib/cleanupDiagnostics.ts +2 -2
  130. package/src/lib/cloudflare/__tests__/classSplit.test.ts +171 -0
  131. package/src/lib/cloudflare/__tests__/collectorClient.test.ts +104 -0
  132. package/src/lib/cloudflare/__tests__/diagnosticDrain.test.ts +165 -0
  133. package/src/lib/cloudflare/classSplit.ts +139 -0
  134. package/src/lib/cloudflare/collectorClient.ts +117 -0
  135. package/src/lib/cloudflare/diagnosticDrain.ts +152 -0
  136. package/src/lib/cloudflare/traceRows.ts +60 -0
  137. package/src/lib/codegen/__tests__/fixedPositionWriter-uint64.test.ts +304 -0
  138. package/src/lib/codegen/evaluatorGenerator.ts +2 -2
  139. package/src/lib/codegen/fixedPositionWriterGenerator.ts +26 -1
  140. package/src/lib/schema/defineFeatureFlags.ts +2 -2
  141. package/src/lib/schema/evaluator.ts +2 -2
  142. package/src/lib/sqlite/sqlite-common.ts +5 -0
  143. package/src/lib/testing/__tests__/facts.test.ts +6 -6
  144. package/src/lib/traceRoot.es.ts +2 -2
  145. package/src/lib/traceRoot.node.ts +3 -3
  146. package/src/lib/traceRoot.ts +5 -5
  147. package/src/lib/tracers/ArrayQueueTracer.ts +2 -2
  148. package/src/lib/codegen/__tests__/__snapshots__/spanLoggerGenerator.test.ts.snap +0 -1965
@@ -0,0 +1,171 @@
1
+ // Configure Node.js timestamp implementation - MUST be first import
2
+ import '../../__tests__/test-helpers.js';
3
+
4
+ import { describe, expect, it } from 'bun:test';
5
+ import { createTestTracerOptions } from '../../__tests__/test-helpers.js';
6
+ import { defineLogSchema, defineOpContext, S } from '../../defineOpContext.js';
7
+ import { Err, Ok } from '../../result.js';
8
+ import { ClassSplitTracer, type DeliveryClassifier } from '../classSplit.js';
9
+ import { type CollectorSendFailure, FakeCollectorClient } from '../collectorClient.js';
10
+ import type { TraceRow } from '../traceRows.js';
11
+
12
+ const testSchema = defineLogSchema({
13
+ lane: S.category(),
14
+ });
15
+
16
+ const opContext = defineOpContext({ logSchema: testSchema });
17
+ const { defineOp } = opContext;
18
+
19
+ // Injected classifier seam — here: a row is billing-grade iff it carries the billing lane tag.
20
+ const classifyByLane: DeliveryClassifier = (row) => (row.lane === 'billing' ? 'billing-grade' : 'diagnostic');
21
+
22
+ const billableOp = defineOp('billable', (ctx) => {
23
+ ctx.tag.lane('billing');
24
+ return ctx.ok('charged');
25
+ });
26
+
27
+ const diagnosticOp = defineOp('diagnostic', (ctx) => ctx.ok('observed'));
28
+
29
+ interface Harness {
30
+ tracer: ClassSplitTracer<typeof opContext>;
31
+ collector: FakeCollectorClient;
32
+ diagnosticBatches: (readonly TraceRow[])[];
33
+ diagnosticErrors: unknown[];
34
+ lateFailures: CollectorSendFailure[];
35
+ }
36
+
37
+ function makeHarness(options?: { diagnosticsFail?: boolean }): Harness {
38
+ const collector = new FakeCollectorClient();
39
+ const diagnosticBatches: (readonly TraceRow[])[] = [];
40
+ const diagnosticErrors: unknown[] = [];
41
+ const lateFailures: CollectorSendFailure[] = [];
42
+ const tracer = new ClassSplitTracer(opContext, {
43
+ ...createTestTracerOptions(),
44
+ collector,
45
+ diagnostics: {
46
+ send: async (rows) => {
47
+ if (options?.diagnosticsFail) throw new Error('diagnostic lane down');
48
+ diagnosticBatches.push(rows);
49
+ },
50
+ },
51
+ classify: classifyByLane,
52
+ onDiagnosticSendError: (error) => {
53
+ diagnosticErrors.push(error);
54
+ },
55
+ onLateBillableSendFailure: (failure) => {
56
+ lateFailures.push(failure);
57
+ },
58
+ });
59
+ return { tracer, collector, diagnosticBatches, diagnosticErrors, lateFailures };
60
+ }
61
+
62
+ function tick(): Promise<void> {
63
+ return new Promise((resolve) => setTimeout(resolve, 0));
64
+ }
65
+
66
+ describe('ClassSplitTracer', () => {
67
+ describe('settleBillable (awaited within the request)', () => {
68
+ it('returns Ok(null) when the request produced no billing-grade rows', async () => {
69
+ const { tracer, collector, diagnosticBatches } = makeHarness();
70
+ await tracer.trace('req', diagnosticOp);
71
+
72
+ const settled = await tracer.settleBillable();
73
+ expect(settled).toBeInstanceOf(Ok);
74
+ if (settled instanceof Ok) expect(settled.value).toBeNull();
75
+ expect(collector.acked).toHaveLength(0);
76
+ // Diagnostic rows are staged, not sent yet — that is flush()'s lazy job.
77
+ expect(diagnosticBatches).toHaveLength(0);
78
+ });
79
+
80
+ it('sends billing-grade rows to the collector and resolves only on its ack', async () => {
81
+ const { tracer, collector } = makeHarness();
82
+ collector.holdSends();
83
+ await tracer.trace('req', billableOp);
84
+
85
+ let settledFlag = false;
86
+ const pending = tracer.settleBillable().then((result) => {
87
+ settledFlag = true;
88
+ return result;
89
+ });
90
+
91
+ await tick();
92
+ // Send-before-response: the request must still be blocked here.
93
+ expect(settledFlag).toBe(false);
94
+ expect(collector.pendingCount).toBe(1);
95
+
96
+ collector.ackNext();
97
+ const settled = await pending;
98
+ expect(settled).toBeInstanceOf(Ok);
99
+ expect(collector.acked).toHaveLength(1);
100
+ expect(collector.acked[0].every((row) => row.lane === 'billing')).toBe(true);
101
+ // No billing-grade rows may remain buffered in the isolate.
102
+ expect(tracer.queue).toHaveLength(0);
103
+ });
104
+
105
+ it('returns Err when the collector is unavailable — the billable request must fail', async () => {
106
+ const { tracer, collector } = makeHarness();
107
+ collector.failNext({ code: 'collector-unavailable', message: 'shard down' });
108
+ await tracer.trace('req', billableOp);
109
+
110
+ const settled = await tracer.settleBillable();
111
+ expect(settled).toBeInstanceOf(Err);
112
+ if (settled instanceof Err) expect(settled.error.code).toBe('collector-unavailable');
113
+ });
114
+
115
+ it('routes rows of one trace to both lanes per the injected classifier', async () => {
116
+ const { tracer, collector, diagnosticBatches } = makeHarness();
117
+ await tracer.trace('req', billableOp);
118
+ await tracer.trace('req', diagnosticOp);
119
+
120
+ const settled = await tracer.settleBillable();
121
+ expect(settled).toBeInstanceOf(Ok);
122
+ expect(collector.acked).toHaveLength(1);
123
+
124
+ await tracer.flush();
125
+ expect(diagnosticBatches).toHaveLength(1);
126
+ const diagnosticRows = diagnosticBatches[0];
127
+ expect(diagnosticRows.length).toBeGreaterThan(0);
128
+ expect(diagnosticRows.every((row) => row.lane !== 'billing')).toBe(true);
129
+ });
130
+ });
131
+
132
+ describe('flush (lazy, waitUntil-schedulable)', () => {
133
+ it('sends staged + newly drained diagnostic rows and never rejects on transport failure', async () => {
134
+ const { tracer, diagnosticErrors } = makeHarness({ diagnosticsFail: true });
135
+ await tracer.trace('req', diagnosticOp);
136
+
137
+ await tracer.flush();
138
+ expect(diagnosticErrors).toHaveLength(1);
139
+ expect(diagnosticErrors[0]).toBeInstanceOf(Error);
140
+ });
141
+
142
+ it('still delivers billing rows found at flush time (settle skipped), awaited', async () => {
143
+ const { tracer, collector, lateFailures } = makeHarness();
144
+ await tracer.trace('req', billableOp);
145
+
146
+ await tracer.flush();
147
+ expect(collector.acked).toHaveLength(1);
148
+ expect(lateFailures).toHaveLength(0);
149
+ });
150
+
151
+ it('escalates late billable send failures via onLateBillableSendFailure without rejecting', async () => {
152
+ const { tracer, collector, lateFailures } = makeHarness();
153
+ collector.failNext({ code: 'collector-rejected', message: 'commit failed' });
154
+ await tracer.trace('req', billableOp);
155
+
156
+ await tracer.flush();
157
+ expect(lateFailures).toHaveLength(1);
158
+ expect(lateFailures[0].code).toBe('collector-rejected');
159
+ });
160
+
161
+ it('does not resend diagnostic rows on a second flush', async () => {
162
+ const { tracer, diagnosticBatches } = makeHarness();
163
+ await tracer.trace('req', diagnosticOp);
164
+
165
+ await tracer.settleBillable();
166
+ await tracer.flush();
167
+ await tracer.flush();
168
+ expect(diagnosticBatches).toHaveLength(1);
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,104 @@
1
+ // Configure Node.js timestamp implementation - MUST be first import
2
+ import '../../__tests__/test-helpers.js';
3
+
4
+ import { describe, expect, it } from 'bun:test';
5
+ import { Err, Ok } from '../../result.js';
6
+ import { COLLECTOR_ACK, FakeCollectorClient } from '../collectorClient.js';
7
+ import type { TraceRow } from '../traceRows.js';
8
+
9
+ const rows: TraceRow[] = [
10
+ {
11
+ trace_id: 't1',
12
+ span_id: 1,
13
+ parent_span_id: 0,
14
+ row_index: 0,
15
+ entry_type: 1,
16
+ timestamp_ns: 1_000_000,
17
+ message: 'op',
18
+ },
19
+ ];
20
+
21
+ function tick(): Promise<void> {
22
+ return new Promise((resolve) => setTimeout(resolve, 0));
23
+ }
24
+
25
+ describe('CollectorClient contract (via FakeCollectorClient)', () => {
26
+ describe('ack semantics', () => {
27
+ it('resolves Ok with the durability ack and records the batch', async () => {
28
+ const client = new FakeCollectorClient();
29
+ const result = await client.send(rows);
30
+
31
+ expect(result).toBeInstanceOf(Ok);
32
+ if (result instanceof Ok) expect(result.value).toBe(COLLECTOR_ACK);
33
+ expect(client.acked).toEqual([rows]);
34
+ });
35
+
36
+ it('send resolves ONLY when the collector acks (durability point)', async () => {
37
+ const client = new FakeCollectorClient();
38
+ client.holdSends();
39
+
40
+ let settled = false;
41
+ const pending = client.send(rows).then((result) => {
42
+ settled = true;
43
+ return result;
44
+ });
45
+
46
+ await tick();
47
+ // No ack yet — the send-before-response contract means the caller is still blocked here.
48
+ expect(settled).toBe(false);
49
+ expect(client.pendingCount).toBe(1);
50
+ expect(client.acked).toHaveLength(0);
51
+
52
+ client.ackNext();
53
+ const result = await pending;
54
+ expect(settled).toBe(true);
55
+ expect(result).toBeInstanceOf(Ok);
56
+ expect(client.acked).toEqual([rows]);
57
+ });
58
+
59
+ it('supports injected ack latency', async () => {
60
+ const client = new FakeCollectorClient();
61
+ client.setLatency(20);
62
+
63
+ const start = Date.now();
64
+ const result = await client.send(rows);
65
+ expect(result).toBeInstanceOf(Ok);
66
+ expect(Date.now() - start).toBeGreaterThanOrEqual(15);
67
+ });
68
+ });
69
+
70
+ describe('failure semantics', () => {
71
+ it('returns Err (never throws) on injected failure — the billable request must fail', async () => {
72
+ const client = new FakeCollectorClient();
73
+ client.failNext({ code: 'collector-unavailable', message: 'shard down' });
74
+
75
+ const result = await client.send(rows);
76
+ expect(result).toBeInstanceOf(Err);
77
+ if (result instanceof Err) expect(result.error.code).toBe('collector-unavailable');
78
+ // A failed send is NOT durable — nothing may be recorded as acked.
79
+ expect(client.acked).toHaveLength(0);
80
+ });
81
+
82
+ it('held sends can be rejected with an operational failure', async () => {
83
+ const client = new FakeCollectorClient();
84
+ client.holdSends();
85
+
86
+ const pending = client.send(rows);
87
+ client.rejectNext({ code: 'collector-rejected', message: 'storage commit failed' });
88
+
89
+ const result = await pending;
90
+ expect(result).toBeInstanceOf(Err);
91
+ if (result instanceof Err) expect(result.error.code).toBe('collector-rejected');
92
+ expect(client.acked).toHaveLength(0);
93
+ });
94
+
95
+ it('recovers after a failure: subsequent sends ack normally', async () => {
96
+ const client = new FakeCollectorClient();
97
+ client.failNext({ code: 'collector-unavailable', message: 'blip' });
98
+
99
+ expect(await client.send(rows)).toBeInstanceOf(Err);
100
+ expect(await client.send(rows)).toBeInstanceOf(Ok);
101
+ expect(client.acked).toEqual([rows]);
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,165 @@
1
+ // Configure Node.js timestamp implementation - MUST be first import
2
+ import '../../__tests__/test-helpers.js';
3
+
4
+ import { describe, expect, it } from 'bun:test';
5
+ import { createTestTracerOptions } from '../../__tests__/test-helpers.js';
6
+ import { defineLogSchema, defineOpContext, S } from '../../defineOpContext.js';
7
+ import {
8
+ DiagnosticDrainTracer,
9
+ PipelinesStreamTransport,
10
+ QueuesFallbackTransport,
11
+ type TraceChunkQueueMessage,
12
+ } from '../diagnosticDrain.js';
13
+ import type { TraceRow } from '../traceRows.js';
14
+
15
+ const testSchema = defineLogSchema({
16
+ userId: S.category(),
17
+ });
18
+
19
+ const opContext = defineOpContext({ logSchema: testSchema });
20
+ const { defineOp } = opContext;
21
+
22
+ function makeRows(count: number): TraceRow[] {
23
+ const rows: TraceRow[] = [];
24
+ for (let i = 0; i < count; i++) {
25
+ rows.push({
26
+ trace_id: 'trace-x',
27
+ span_id: 1,
28
+ parent_span_id: 0,
29
+ row_index: i,
30
+ entry_type: 1,
31
+ timestamp_ns: (i + 1) * 1_000_000,
32
+ message: null,
33
+ });
34
+ }
35
+ return rows;
36
+ }
37
+
38
+ describe('DiagnosticDrainTracer', () => {
39
+ it('flush drains completed roots and sends them as rows through the Pipelines-shaped binding', async () => {
40
+ const sentBatches: TraceRow[][] = [];
41
+ // env.TRACE_STREAM.send-shaped fn — array of JSON-serializable records, resolves on ingest.
42
+ const transport = new PipelinesStreamTransport(async (rows) => {
43
+ sentBatches.push(rows);
44
+ });
45
+ const tracer = new DiagnosticDrainTracer(opContext, { ...createTestTracerOptions(), transport });
46
+ const { trace, flush } = tracer;
47
+
48
+ const op = defineOp('work', (ctx) => {
49
+ ctx.tag.userId('user-1');
50
+ return ctx.ok('done');
51
+ });
52
+ await trace('first', op);
53
+ await trace('second', op);
54
+
55
+ expect(tracer.queue).toHaveLength(2);
56
+ await flush();
57
+
58
+ // One send per completed root buffer, queue fully drained.
59
+ expect(tracer.queue).toHaveLength(0);
60
+ expect(sentBatches).toHaveLength(2);
61
+
62
+ const firstRows = sentBatches[0];
63
+ expect(firstRows.length).toBeGreaterThan(0);
64
+ expect(firstRows[0].trace_id).toBeString();
65
+ expect(firstRows[0].message).toBe('first');
66
+ expect(firstRows[0].entry_type).toBeNumber();
67
+ expect(firstRows[0].timestamp_ns).toBeNumber();
68
+ // User schema columns ride flat on the row.
69
+ expect(firstRows.some((row) => row.userId === 'user-1')).toBe(true);
70
+ // Rows must be JSON-serializable records.
71
+ expect(JSON.parse(JSON.stringify(firstRows))).toEqual(firstRows);
72
+ });
73
+
74
+ it('flush never rejects on transport failure — errors surface via onSendError only', async () => {
75
+ const errors: { error: unknown; rows: readonly TraceRow[] }[] = [];
76
+ const tracer = new DiagnosticDrainTracer(opContext, {
77
+ ...createTestTracerOptions(),
78
+ transport: {
79
+ send: () => Promise.reject(new Error('pipeline unavailable')),
80
+ },
81
+ onSendError: (error, rows) => {
82
+ errors.push({ error, rows });
83
+ },
84
+ });
85
+ const { trace, flush } = tracer;
86
+
87
+ const op = defineOp('work', (ctx) => ctx.ok('done'));
88
+ await trace('doomed', op);
89
+
90
+ await flush();
91
+
92
+ expect(errors).toHaveLength(1);
93
+ expect(errors[0].error).toBeInstanceOf(Error);
94
+ expect(errors[0].rows.length).toBeGreaterThan(0);
95
+ // Buffers are consumed even on failure — no unbounded isolate accumulation.
96
+ expect(tracer.queue).toHaveLength(0);
97
+ });
98
+
99
+ it('flush with an empty queue is a no-op', async () => {
100
+ const sentBatches: TraceRow[][] = [];
101
+ const tracer = new DiagnosticDrainTracer(opContext, {
102
+ ...createTestTracerOptions(),
103
+ transport: new PipelinesStreamTransport(async (rows) => {
104
+ sentBatches.push(rows);
105
+ }),
106
+ });
107
+
108
+ await tracer.flush();
109
+ expect(sentBatches).toHaveLength(0);
110
+ });
111
+ });
112
+
113
+ describe('QueuesFallbackTransport', () => {
114
+ it('ships batched chunk-envelope messages, never per-span messages', async () => {
115
+ const messages: TraceChunkQueueMessage[] = [];
116
+ const transport = new QueuesFallbackTransport({
117
+ fileRef: 'queue://test-worker',
118
+ maxRowsPerMessage: 2,
119
+ sendMessage: async (message) => {
120
+ messages.push(message);
121
+ },
122
+ });
123
+
124
+ await transport.send(makeRows(5));
125
+
126
+ expect(messages).toHaveLength(3);
127
+ expect(messages.map((m) => m.rows.length)).toEqual([2, 2, 1]);
128
+ for (const message of messages) {
129
+ expect(message.envelope.chunk_id).toStartWith('chunk_');
130
+ expect(message.envelope.file_ref).toBe('queue://test-worker');
131
+ expect(message.envelope.row_count).toBe(message.rows.length);
132
+ expect(message.envelope.started_at_ms).toBeLessThanOrEqual(message.envelope.ended_at_ms);
133
+ }
134
+ });
135
+
136
+ it('chunk_id is deterministic for the same batch — redelivery dedupes by chunk_id', async () => {
137
+ const chunkIds: string[] = [];
138
+ const transport = new QueuesFallbackTransport({
139
+ fileRef: 'queue://test-worker',
140
+ maxRowsPerMessage: 10,
141
+ sendMessage: async (message) => {
142
+ chunkIds.push(message.envelope.chunk_id);
143
+ },
144
+ });
145
+
146
+ const batch = makeRows(3);
147
+ await transport.send(batch);
148
+ await transport.send(batch);
149
+ await transport.send(makeRows(4));
150
+
151
+ expect(chunkIds[0]).toBe(chunkIds[1]);
152
+ expect(chunkIds[2]).not.toBe(chunkIds[0]);
153
+ });
154
+
155
+ it('rejects a non-positive batch size at construction', () => {
156
+ expect(
157
+ () =>
158
+ new QueuesFallbackTransport({
159
+ fileRef: 'queue://test-worker',
160
+ maxRowsPerMessage: 0,
161
+ sendMessage: async () => {},
162
+ }),
163
+ ).toThrow('maxRowsPerMessage');
164
+ });
165
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Class-split flush adapter — routes each completed root buffer's rows by delivery class.
3
+ *
4
+ * Per specs/lmao/01u_cloudflare_trace_segments.md §Delivery Classes every trace row belongs to
5
+ * exactly one class:
6
+ * - billing-grade rows go to the CollectorClient, AWAITED within the request via
7
+ * `settleBillable()` before/alongside response completion — an Err fails the billable request
8
+ * (backpressure ownership: never silently queue billing-grade rows in the isolate)
9
+ * - diagnostic rows go to the drain transport lazily, on the waitUntil-scheduled `flush()`
10
+ *
11
+ * The classifier is an injected function: the spec fixes the CLASSES, not the classification
12
+ * mechanism, so the mapping from row to class stays a seam (constructor-injected like the
13
+ * transports).
14
+ *
15
+ * Canonical request flow (01s wrapper seam):
16
+ * const settled = await tracer.settleBillable(); // before returning the response
17
+ * if (settled instanceof Err) return failBillableRequest(settled.error);
18
+ * ctx.waitUntil(tracer.flush()); // diagnostics, fire-and-forget
19
+ */
20
+
21
+ import type { OpContextBinding } from '../opContext/types.js';
22
+ import { Err, Ok, type Result } from '../result.js';
23
+ import type { TracerOptions } from '../tracer.js';
24
+ import { ArrayQueueTracer } from '../tracers/ArrayQueueTracer.js';
25
+ import type { CollectorAck, CollectorClient, CollectorSendFailure } from './collectorClient.js';
26
+ import type { DiagnosticTransport } from './diagnosticDrain.js';
27
+ import { spanBufferToTraceRows, type TraceRow } from './traceRows.js';
28
+
29
+ export type DeliveryClass = 'billing-grade' | 'diagnostic';
30
+
31
+ /** Injected classification seam — decides which lane one row rides. */
32
+ export type DeliveryClassifier = (row: TraceRow) => DeliveryClass;
33
+
34
+ /** Singleton for the common "nothing billable in this request" settle outcome. */
35
+ const SETTLED_NOTHING_BILLABLE: Ok<null> = new Ok(null);
36
+
37
+ export interface ClassSplitOptions<
38
+ T extends import('../schema/LogSchema.js').LogSchema = import('../schema/LogSchema.js').LogSchema,
39
+ > extends TracerOptions<T> {
40
+ collector: CollectorClient;
41
+ diagnostics: DiagnosticTransport;
42
+ classify: DeliveryClassifier;
43
+ /** Diagnostic-lane failure hook: emit metrics; drop is within the class's loss budget. */
44
+ onDiagnosticSendError?: (error: unknown, rows: readonly TraceRow[]) => void;
45
+ /**
46
+ * Billing rows found only at flush() time (the wrapper skipped settleBillable) whose late
47
+ * awaited send failed. This is a broken-integration alarm — page on it; the rows are lost to
48
+ * this isolate and only replay/reconciliation (01u §Zero-Loss Sources) can heal the gap.
49
+ */
50
+ onLateBillableSendFailure?: (failure: CollectorSendFailure, rows: readonly TraceRow[]) => void;
51
+ }
52
+
53
+ interface PartitionedRows {
54
+ billing: TraceRow[];
55
+ diagnostic: TraceRow[];
56
+ }
57
+
58
+ /**
59
+ * Tracer tying the two lanes together over onTraceEnd (queue) + settleBillable/flush (drain).
60
+ *
61
+ * Diagnostic rows discovered during settleBillable are held for the same request's flush() —
62
+ * request-scoped staging only, never cross-request accumulation of billing-grade data.
63
+ */
64
+ export class ClassSplitTracer<B extends OpContextBinding = OpContextBinding> extends ArrayQueueTracer<B> {
65
+ private readonly collector: CollectorClient;
66
+ private readonly diagnostics: DiagnosticTransport;
67
+ private readonly classify: DeliveryClassifier;
68
+ private readonly onDiagnosticSendError: ((error: unknown, rows: readonly TraceRow[]) => void) | undefined;
69
+ private readonly onLateBillableSendFailure:
70
+ | ((failure: CollectorSendFailure, rows: readonly TraceRow[]) => void)
71
+ | undefined;
72
+ private pendingDiagnosticRows: TraceRow[] = [];
73
+
74
+ constructor(binding: B, options: ClassSplitOptions<B['logBinding']['logSchema']>) {
75
+ super(binding, options);
76
+ this.collector = options.collector;
77
+ this.diagnostics = options.diagnostics;
78
+ this.classify = options.classify;
79
+ this.onDiagnosticSendError = options.onDiagnosticSendError;
80
+ this.onLateBillableSendFailure = options.onLateBillableSendFailure;
81
+ }
82
+
83
+ private partitionDrained(): PartitionedRows {
84
+ const billing: TraceRow[] = [];
85
+ const diagnostic: TraceRow[] = [];
86
+ for (const buffer of this.drain()) {
87
+ let rows: TraceRow[];
88
+ try {
89
+ rows = spanBufferToTraceRows(buffer);
90
+ } finally {
91
+ this.bufferStrategy.releaseBuffer(buffer);
92
+ }
93
+ for (const row of rows) {
94
+ (this.classify(row) === 'billing-grade' ? billing : diagnostic).push(row);
95
+ }
96
+ }
97
+ return { billing, diagnostic };
98
+ }
99
+
100
+ /**
101
+ * Drain completed traces and durably deliver the billing-grade rows — awaited within the
102
+ * request. Ok(null) means no billing rows were produced; Err means the collector could not
103
+ * make the revenue records durable and the billable request must fail. Diagnostic rows are
104
+ * staged for this request's flush().
105
+ */
106
+ async settleBillable(): Promise<Result<CollectorAck | null, CollectorSendFailure>> {
107
+ const { billing, diagnostic } = this.partitionDrained();
108
+ this.pendingDiagnosticRows.push(...diagnostic);
109
+ if (billing.length === 0) return SETTLED_NOTHING_BILLABLE;
110
+ return this.collector.send(billing);
111
+ }
112
+
113
+ /**
114
+ * Lazy, waitUntil-schedulable drain: never rejects. Diagnostic rows (staged + newly drained)
115
+ * go to the drain transport; failures hit onDiagnosticSendError per the class's loss budget.
116
+ * Billing rows found here mean settleBillable was skipped — they are still sent awaited (never
117
+ * silently dropped or left buffered), with failures escalated via onLateBillableSendFailure.
118
+ */
119
+ override async flush(): Promise<void> {
120
+ const { billing, diagnostic } = this.partitionDrained();
121
+ const diagnosticRows = this.pendingDiagnosticRows.concat(diagnostic);
122
+ this.pendingDiagnosticRows = [];
123
+
124
+ if (billing.length > 0) {
125
+ const result = await this.collector.send(billing);
126
+ if (result instanceof Err) {
127
+ this.onLateBillableSendFailure?.(result.error, billing);
128
+ }
129
+ }
130
+
131
+ if (diagnosticRows.length > 0) {
132
+ try {
133
+ await this.diagnostics.send(diagnosticRows);
134
+ } catch (error) {
135
+ this.onDiagnosticSendError?.(error, diagnosticRows);
136
+ }
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Billing-grade collector client.
3
+ *
4
+ * Per specs/lmao/01u_cloudflare_trace_segments.md §Delivery Classes / §Zero-Loss Sources:
5
+ * `send(rows)` resolves Ok only when the collector has ACKED — and the ack means the collector
6
+ * DO's transactional-storage commit completed, which IS the durability point. From that moment
7
+ * the rows are durable and replay-owned.
8
+ *
9
+ * Send-before-response contract: the caller awaits `send` within the request; an Err fails the
10
+ * billable request (same principle as any payment-gateway outage). Implementations and callers
11
+ * must NEVER buffer billing-grade rows in the isolate across requests — an isolate eviction
12
+ * between requests silently destroys revenue records.
13
+ *
14
+ * The collector DO itself is not deployed yet; this module ships the interface plus an in-memory
15
+ * fake with ack/fail/latency injection so lane composition is testable today.
16
+ */
17
+
18
+ import { Err, Ok, type Result } from '../result.js';
19
+ import type { TraceRow } from './traceRows.js';
20
+
21
+ /** Proof of collector durability: the DO transactional-storage commit completed. */
22
+ export interface CollectorAck {
23
+ readonly durable: true;
24
+ }
25
+
26
+ /** Singleton ack — the ack carries no data, so never allocate it per send. */
27
+ export const COLLECTOR_ACK: CollectorAck = { durable: true };
28
+
29
+ /** Operational send failure — the caller must fail the billable request on this. */
30
+ export interface CollectorSendFailure {
31
+ readonly code: 'collector-unavailable' | 'collector-rejected';
32
+ readonly message: string;
33
+ }
34
+
35
+ /**
36
+ * Transport to the billing-grade collector shard.
37
+ *
38
+ * `send` resolves Ok(CollectorAck) only after the collector's DO transactional-storage commit;
39
+ * operational failures come back as Err (never throw) so the request path can fail deliberately.
40
+ */
41
+ export interface CollectorClient {
42
+ send(rows: readonly TraceRow[]): Promise<Result<CollectorAck, CollectorSendFailure>>;
43
+ }
44
+
45
+ interface HeldSend {
46
+ readonly rows: readonly TraceRow[];
47
+ readonly resolve: (result: Result<CollectorAck, CollectorSendFailure>) => void;
48
+ }
49
+
50
+ /**
51
+ * In-memory CollectorClient fake with ack/fail/latency injection.
52
+ *
53
+ * Modes:
54
+ * - default: every send acks immediately (after optional injected latency)
55
+ * - `failNext(failure)`: queue an Err for upcoming sends
56
+ * - `holdSends()`: sends stay pending until the test calls `ackNext()`/`rejectNext()` — this is
57
+ * how contract tests prove "resolves only when the collector acks"
58
+ */
59
+ export class FakeCollectorClient implements CollectorClient {
60
+ /** Row batches the fake has durably acked, in ack order. */
61
+ readonly acked: (readonly TraceRow[])[] = [];
62
+
63
+ private readonly failures: CollectorSendFailure[] = [];
64
+ private readonly held: HeldSend[] = [];
65
+ private holding = false;
66
+ private latencyMs = 0;
67
+
68
+ /** Queue an Err result for the next send (FIFO across multiple calls). */
69
+ failNext(failure: CollectorSendFailure): void {
70
+ this.failures.push(failure);
71
+ }
72
+
73
+ /** Inject artificial ack latency for subsequent sends. */
74
+ setLatency(ms: number): void {
75
+ this.latencyMs = ms;
76
+ }
77
+
78
+ /** Switch to manual mode: sends stay pending until ackNext()/rejectNext(). */
79
+ holdSends(): void {
80
+ this.holding = true;
81
+ }
82
+
83
+ /** Number of sends currently awaiting a manual ack. */
84
+ get pendingCount(): number {
85
+ return this.held.length;
86
+ }
87
+
88
+ /** Ack the oldest held send (DO commit happened). */
89
+ ackNext(): void {
90
+ const pending = this.held.shift();
91
+ if (!pending) throw new Error('FakeCollectorClient.ackNext: no held send');
92
+ this.acked.push(pending.rows);
93
+ pending.resolve(new Ok(COLLECTOR_ACK));
94
+ }
95
+
96
+ /** Fail the oldest held send with an operational failure. */
97
+ rejectNext(failure: CollectorSendFailure): void {
98
+ const pending = this.held.shift();
99
+ if (!pending) throw new Error('FakeCollectorClient.rejectNext: no held send');
100
+ pending.resolve(new Err(failure));
101
+ }
102
+
103
+ async send(rows: readonly TraceRow[]): Promise<Result<CollectorAck, CollectorSendFailure>> {
104
+ if (this.latencyMs > 0) {
105
+ await new Promise((resolve) => setTimeout(resolve, this.latencyMs));
106
+ }
107
+ if (this.holding) {
108
+ return new Promise((resolve) => {
109
+ this.held.push({ rows, resolve });
110
+ });
111
+ }
112
+ const failure = this.failures.shift();
113
+ if (failure) return new Err(failure);
114
+ this.acked.push(rows);
115
+ return new Ok(COLLECTOR_ACK);
116
+ }
117
+ }