gitx.do 0.0.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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/durable-object/object-store.d.ts +113 -0
  4. package/dist/durable-object/object-store.d.ts.map +1 -0
  5. package/dist/durable-object/object-store.js +387 -0
  6. package/dist/durable-object/object-store.js.map +1 -0
  7. package/dist/durable-object/schema.d.ts +17 -0
  8. package/dist/durable-object/schema.d.ts.map +1 -0
  9. package/dist/durable-object/schema.js +43 -0
  10. package/dist/durable-object/schema.js.map +1 -0
  11. package/dist/durable-object/wal.d.ts +111 -0
  12. package/dist/durable-object/wal.d.ts.map +1 -0
  13. package/dist/durable-object/wal.js +200 -0
  14. package/dist/durable-object/wal.js.map +1 -0
  15. package/dist/index.d.ts +24 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +101 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/mcp/adapter.d.ts +231 -0
  20. package/dist/mcp/adapter.d.ts.map +1 -0
  21. package/dist/mcp/adapter.js +502 -0
  22. package/dist/mcp/adapter.js.map +1 -0
  23. package/dist/mcp/sandbox.d.ts +261 -0
  24. package/dist/mcp/sandbox.d.ts.map +1 -0
  25. package/dist/mcp/sandbox.js +983 -0
  26. package/dist/mcp/sandbox.js.map +1 -0
  27. package/dist/mcp/sdk-adapter.d.ts +413 -0
  28. package/dist/mcp/sdk-adapter.d.ts.map +1 -0
  29. package/dist/mcp/sdk-adapter.js +672 -0
  30. package/dist/mcp/sdk-adapter.js.map +1 -0
  31. package/dist/mcp/tools.d.ts +133 -0
  32. package/dist/mcp/tools.d.ts.map +1 -0
  33. package/dist/mcp/tools.js +1604 -0
  34. package/dist/mcp/tools.js.map +1 -0
  35. package/dist/ops/blame.d.ts +148 -0
  36. package/dist/ops/blame.d.ts.map +1 -0
  37. package/dist/ops/blame.js +754 -0
  38. package/dist/ops/blame.js.map +1 -0
  39. package/dist/ops/branch.d.ts +215 -0
  40. package/dist/ops/branch.d.ts.map +1 -0
  41. package/dist/ops/branch.js +608 -0
  42. package/dist/ops/branch.js.map +1 -0
  43. package/dist/ops/commit-traversal.d.ts +209 -0
  44. package/dist/ops/commit-traversal.d.ts.map +1 -0
  45. package/dist/ops/commit-traversal.js +755 -0
  46. package/dist/ops/commit-traversal.js.map +1 -0
  47. package/dist/ops/commit.d.ts +221 -0
  48. package/dist/ops/commit.d.ts.map +1 -0
  49. package/dist/ops/commit.js +606 -0
  50. package/dist/ops/commit.js.map +1 -0
  51. package/dist/ops/merge-base.d.ts +223 -0
  52. package/dist/ops/merge-base.d.ts.map +1 -0
  53. package/dist/ops/merge-base.js +581 -0
  54. package/dist/ops/merge-base.js.map +1 -0
  55. package/dist/ops/merge.d.ts +385 -0
  56. package/dist/ops/merge.d.ts.map +1 -0
  57. package/dist/ops/merge.js +1203 -0
  58. package/dist/ops/merge.js.map +1 -0
  59. package/dist/ops/tag.d.ts +182 -0
  60. package/dist/ops/tag.d.ts.map +1 -0
  61. package/dist/ops/tag.js +608 -0
  62. package/dist/ops/tag.js.map +1 -0
  63. package/dist/ops/tree-builder.d.ts +82 -0
  64. package/dist/ops/tree-builder.d.ts.map +1 -0
  65. package/dist/ops/tree-builder.js +246 -0
  66. package/dist/ops/tree-builder.js.map +1 -0
  67. package/dist/ops/tree-diff.d.ts +243 -0
  68. package/dist/ops/tree-diff.d.ts.map +1 -0
  69. package/dist/ops/tree-diff.js +657 -0
  70. package/dist/ops/tree-diff.js.map +1 -0
  71. package/dist/pack/delta.d.ts +68 -0
  72. package/dist/pack/delta.d.ts.map +1 -0
  73. package/dist/pack/delta.js +343 -0
  74. package/dist/pack/delta.js.map +1 -0
  75. package/dist/pack/format.d.ts +84 -0
  76. package/dist/pack/format.d.ts.map +1 -0
  77. package/dist/pack/format.js +261 -0
  78. package/dist/pack/format.js.map +1 -0
  79. package/dist/pack/full-generation.d.ts +327 -0
  80. package/dist/pack/full-generation.d.ts.map +1 -0
  81. package/dist/pack/full-generation.js +1159 -0
  82. package/dist/pack/full-generation.js.map +1 -0
  83. package/dist/pack/generation.d.ts +118 -0
  84. package/dist/pack/generation.d.ts.map +1 -0
  85. package/dist/pack/generation.js +459 -0
  86. package/dist/pack/generation.js.map +1 -0
  87. package/dist/pack/index.d.ts +181 -0
  88. package/dist/pack/index.d.ts.map +1 -0
  89. package/dist/pack/index.js +552 -0
  90. package/dist/pack/index.js.map +1 -0
  91. package/dist/refs/branch.d.ts +224 -0
  92. package/dist/refs/branch.d.ts.map +1 -0
  93. package/dist/refs/branch.js +170 -0
  94. package/dist/refs/branch.js.map +1 -0
  95. package/dist/refs/storage.d.ts +208 -0
  96. package/dist/refs/storage.d.ts.map +1 -0
  97. package/dist/refs/storage.js +421 -0
  98. package/dist/refs/storage.js.map +1 -0
  99. package/dist/refs/tag.d.ts +230 -0
  100. package/dist/refs/tag.d.ts.map +1 -0
  101. package/dist/refs/tag.js +188 -0
  102. package/dist/refs/tag.js.map +1 -0
  103. package/dist/storage/lru-cache.d.ts +188 -0
  104. package/dist/storage/lru-cache.d.ts.map +1 -0
  105. package/dist/storage/lru-cache.js +410 -0
  106. package/dist/storage/lru-cache.js.map +1 -0
  107. package/dist/storage/object-index.d.ts +140 -0
  108. package/dist/storage/object-index.d.ts.map +1 -0
  109. package/dist/storage/object-index.js +166 -0
  110. package/dist/storage/object-index.js.map +1 -0
  111. package/dist/storage/r2-pack.d.ts +394 -0
  112. package/dist/storage/r2-pack.d.ts.map +1 -0
  113. package/dist/storage/r2-pack.js +1062 -0
  114. package/dist/storage/r2-pack.js.map +1 -0
  115. package/dist/tiered/cdc-pipeline.d.ts +316 -0
  116. package/dist/tiered/cdc-pipeline.d.ts.map +1 -0
  117. package/dist/tiered/cdc-pipeline.js +771 -0
  118. package/dist/tiered/cdc-pipeline.js.map +1 -0
  119. package/dist/tiered/migration.d.ts +242 -0
  120. package/dist/tiered/migration.d.ts.map +1 -0
  121. package/dist/tiered/migration.js +592 -0
  122. package/dist/tiered/migration.js.map +1 -0
  123. package/dist/tiered/parquet-writer.d.ts +248 -0
  124. package/dist/tiered/parquet-writer.d.ts.map +1 -0
  125. package/dist/tiered/parquet-writer.js +555 -0
  126. package/dist/tiered/parquet-writer.js.map +1 -0
  127. package/dist/tiered/read-path.d.ts +141 -0
  128. package/dist/tiered/read-path.d.ts.map +1 -0
  129. package/dist/tiered/read-path.js +204 -0
  130. package/dist/tiered/read-path.js.map +1 -0
  131. package/dist/types/objects.d.ts +53 -0
  132. package/dist/types/objects.d.ts.map +1 -0
  133. package/dist/types/objects.js +291 -0
  134. package/dist/types/objects.js.map +1 -0
  135. package/dist/types/storage.d.ts +117 -0
  136. package/dist/types/storage.d.ts.map +1 -0
  137. package/dist/types/storage.js +8 -0
  138. package/dist/types/storage.js.map +1 -0
  139. package/dist/utils/hash.d.ts +31 -0
  140. package/dist/utils/hash.d.ts.map +1 -0
  141. package/dist/utils/hash.js +60 -0
  142. package/dist/utils/hash.js.map +1 -0
  143. package/dist/utils/sha1.d.ts +26 -0
  144. package/dist/utils/sha1.d.ts.map +1 -0
  145. package/dist/utils/sha1.js +127 -0
  146. package/dist/utils/sha1.js.map +1 -0
  147. package/dist/wire/capabilities.d.ts +236 -0
  148. package/dist/wire/capabilities.d.ts.map +1 -0
  149. package/dist/wire/capabilities.js +437 -0
  150. package/dist/wire/capabilities.js.map +1 -0
  151. package/dist/wire/pkt-line.d.ts +67 -0
  152. package/dist/wire/pkt-line.d.ts.map +1 -0
  153. package/dist/wire/pkt-line.js +145 -0
  154. package/dist/wire/pkt-line.js.map +1 -0
  155. package/dist/wire/receive-pack.d.ts +302 -0
  156. package/dist/wire/receive-pack.d.ts.map +1 -0
  157. package/dist/wire/receive-pack.js +885 -0
  158. package/dist/wire/receive-pack.js.map +1 -0
  159. package/dist/wire/smart-http.d.ts +321 -0
  160. package/dist/wire/smart-http.d.ts.map +1 -0
  161. package/dist/wire/smart-http.js +654 -0
  162. package/dist/wire/smart-http.js.map +1 -0
  163. package/dist/wire/upload-pack.d.ts +333 -0
  164. package/dist/wire/upload-pack.d.ts.map +1 -0
  165. package/dist/wire/upload-pack.js +850 -0
  166. package/dist/wire/upload-pack.js.map +1 -0
  167. package/package.json +61 -0
@@ -0,0 +1,771 @@
1
+ /**
2
+ * CDC (Change Data Capture) Pipeline for Git Operations
3
+ *
4
+ * Provides functionality to capture, transform, batch, and output git operation events:
5
+ * - Event capture from git operations (push, fetch, commits, etc.)
6
+ * - Parquet transformation for analytics storage
7
+ * - Batching with size and time-based flushing
8
+ * - Error handling with retry policies
9
+ *
10
+ * gitdo: CDC pipeline implementation
11
+ */
12
+ // ============================================================================
13
+ // Error Classes
14
+ // ============================================================================
15
+ /**
16
+ * Custom error class for CDC operations
17
+ */
18
+ export class CDCError extends Error {
19
+ type;
20
+ cause;
21
+ constructor(type, message, cause) {
22
+ super(message);
23
+ this.type = type;
24
+ this.cause = cause;
25
+ this.name = 'CDCError';
26
+ }
27
+ }
28
+ /**
29
+ * Retry policy with exponential backoff
30
+ */
31
+ export class CDCRetryPolicy {
32
+ config;
33
+ constructor(config) {
34
+ this.config = config;
35
+ }
36
+ shouldRetry(attemptCount) {
37
+ return attemptCount < this.config.maxRetries;
38
+ }
39
+ getDelay(attemptCount) {
40
+ let delay = this.config.initialDelayMs * Math.pow(this.config.backoffMultiplier, attemptCount);
41
+ delay = Math.min(delay, this.config.maxDelayMs);
42
+ if (this.config.jitter) {
43
+ // Add random jitter between 0.5x and 1.5x
44
+ const jitterFactor = 0.5 + Math.random();
45
+ delay = Math.floor(delay * jitterFactor);
46
+ }
47
+ return delay;
48
+ }
49
+ }
50
+ // ============================================================================
51
+ // CDC Event Capture
52
+ // ============================================================================
53
+ /**
54
+ * Captures git operations and converts them to CDC events
55
+ */
56
+ export class CDCEventCapture {
57
+ events = [];
58
+ sequenceCounter = 0;
59
+ listeners = [];
60
+ maxBufferSize;
61
+ constructor(options = {}) {
62
+ this.maxBufferSize = options.maxBufferSize ?? Infinity;
63
+ }
64
+ generateEventId() {
65
+ return `evt-${Date.now()}-${Math.random().toString(36).slice(2)}`;
66
+ }
67
+ async emitEvent(event) {
68
+ // Auto-flush if buffer is full
69
+ if (this.events.length >= this.maxBufferSize) {
70
+ await this.flush();
71
+ }
72
+ this.events.push(event);
73
+ // Notify all listeners
74
+ for (const listener of this.listeners) {
75
+ listener(event);
76
+ }
77
+ }
78
+ nextSequence() {
79
+ return ++this.sequenceCounter;
80
+ }
81
+ async onObjectPut(sha, type, data) {
82
+ const event = {
83
+ id: this.generateEventId(),
84
+ type: 'OBJECT_CREATED',
85
+ source: 'push',
86
+ timestamp: Date.now(),
87
+ payload: {
88
+ operation: 'put',
89
+ sha,
90
+ data,
91
+ metadata: { type, size: data.length }
92
+ },
93
+ sequence: this.nextSequence(),
94
+ version: 1
95
+ };
96
+ await this.emitEvent(event);
97
+ }
98
+ async onObjectDelete(sha) {
99
+ const event = {
100
+ id: this.generateEventId(),
101
+ type: 'OBJECT_DELETED',
102
+ source: 'gc',
103
+ timestamp: Date.now(),
104
+ payload: {
105
+ operation: 'delete',
106
+ sha
107
+ },
108
+ sequence: this.nextSequence(),
109
+ version: 1
110
+ };
111
+ await this.emitEvent(event);
112
+ }
113
+ async onRefUpdate(refName, oldSha, newSha) {
114
+ const event = {
115
+ id: this.generateEventId(),
116
+ type: 'REF_UPDATED',
117
+ source: 'push',
118
+ timestamp: Date.now(),
119
+ payload: {
120
+ operation: 'ref-update',
121
+ refName,
122
+ oldSha,
123
+ newSha
124
+ },
125
+ sequence: this.nextSequence(),
126
+ version: 1
127
+ };
128
+ await this.emitEvent(event);
129
+ }
130
+ async onCommitCreated(commitSha, treeSha, parentShas) {
131
+ const event = {
132
+ id: this.generateEventId(),
133
+ type: 'COMMIT_CREATED',
134
+ source: 'push',
135
+ timestamp: Date.now(),
136
+ payload: {
137
+ operation: 'commit-create',
138
+ sha: commitSha,
139
+ treeSha,
140
+ parentShas
141
+ },
142
+ sequence: this.nextSequence(),
143
+ version: 1
144
+ };
145
+ await this.emitEvent(event);
146
+ }
147
+ async onPackReceived(packData, objectCount) {
148
+ const event = {
149
+ id: this.generateEventId(),
150
+ type: 'PACK_RECEIVED',
151
+ source: 'push',
152
+ timestamp: Date.now(),
153
+ payload: {
154
+ operation: 'pack-receive',
155
+ data: packData,
156
+ objectCount
157
+ },
158
+ sequence: this.nextSequence(),
159
+ version: 1
160
+ };
161
+ await this.emitEvent(event);
162
+ }
163
+ async onBranchCreated(branchName, sha) {
164
+ const event = {
165
+ id: this.generateEventId(),
166
+ type: 'BRANCH_CREATED',
167
+ source: 'push',
168
+ timestamp: Date.now(),
169
+ payload: {
170
+ operation: 'branch-create',
171
+ branchName,
172
+ sha
173
+ },
174
+ sequence: this.nextSequence(),
175
+ version: 1
176
+ };
177
+ await this.emitEvent(event);
178
+ }
179
+ async onBranchDeleted(branchName) {
180
+ const event = {
181
+ id: this.generateEventId(),
182
+ type: 'BRANCH_DELETED',
183
+ source: 'push',
184
+ timestamp: Date.now(),
185
+ payload: {
186
+ operation: 'branch-delete',
187
+ branchName
188
+ },
189
+ sequence: this.nextSequence(),
190
+ version: 1
191
+ };
192
+ await this.emitEvent(event);
193
+ }
194
+ async onTagCreated(tagName, sha) {
195
+ const event = {
196
+ id: this.generateEventId(),
197
+ type: 'TAG_CREATED',
198
+ source: 'push',
199
+ timestamp: Date.now(),
200
+ payload: {
201
+ operation: 'tag-create',
202
+ tagName,
203
+ sha
204
+ },
205
+ sequence: this.nextSequence(),
206
+ version: 1
207
+ };
208
+ await this.emitEvent(event);
209
+ }
210
+ async onMergeCompleted(mergeSha, baseSha, headSha) {
211
+ const event = {
212
+ id: this.generateEventId(),
213
+ type: 'MERGE_COMPLETED',
214
+ source: 'push',
215
+ timestamp: Date.now(),
216
+ payload: {
217
+ operation: 'merge-complete',
218
+ sha: mergeSha,
219
+ baseSha,
220
+ headSha
221
+ },
222
+ sequence: this.nextSequence(),
223
+ version: 1
224
+ };
225
+ await this.emitEvent(event);
226
+ }
227
+ getEvents() {
228
+ return [...this.events];
229
+ }
230
+ getBufferSize() {
231
+ return this.events.length;
232
+ }
233
+ async flush() {
234
+ const flushed = [...this.events];
235
+ this.events = [];
236
+ return flushed;
237
+ }
238
+ addListener(listener) {
239
+ this.listeners.push(listener);
240
+ }
241
+ removeListener(listener) {
242
+ const index = this.listeners.indexOf(listener);
243
+ if (index !== -1) {
244
+ this.listeners.splice(index, 1);
245
+ }
246
+ }
247
+ }
248
+ // ============================================================================
249
+ // Parquet Schema
250
+ // ============================================================================
251
+ const CDC_EVENT_FIELDS = [
252
+ { name: 'event_id', type: 'STRING', nullable: false },
253
+ { name: 'event_type', type: 'STRING', nullable: false },
254
+ { name: 'source', type: 'STRING', nullable: false },
255
+ { name: 'timestamp', type: 'TIMESTAMP', nullable: false },
256
+ { name: 'sequence', type: 'INT64', nullable: false },
257
+ { name: 'version', type: 'INT64', nullable: false },
258
+ { name: 'payload_json', type: 'STRING', nullable: false },
259
+ { name: 'sha', type: 'STRING', nullable: true }
260
+ ];
261
+ /**
262
+ * Parquet schema definition for CDC events
263
+ */
264
+ export class ParquetSchema {
265
+ fields;
266
+ constructor(fields) {
267
+ this.fields = fields;
268
+ }
269
+ static forCDCEvents(customFields) {
270
+ const fields = [...CDC_EVENT_FIELDS];
271
+ if (customFields) {
272
+ fields.push(...customFields);
273
+ }
274
+ return new ParquetSchema(fields);
275
+ }
276
+ }
277
+ /**
278
+ * Transforms CDC events to Parquet format
279
+ */
280
+ export class ParquetTransformer {
281
+ compression;
282
+ constructor(options = {}) {
283
+ this.compression = options.compression ?? 'snappy';
284
+ }
285
+ eventToRow(event) {
286
+ // Create a serializable copy of the payload (Uint8Array not JSON-serializable)
287
+ const serializablePayload = {
288
+ ...event.payload,
289
+ data: event.payload.data ? Array.from(event.payload.data) : undefined
290
+ };
291
+ return {
292
+ event_id: event.id,
293
+ event_type: event.type,
294
+ source: event.source,
295
+ timestamp: event.timestamp,
296
+ sequence: event.sequence,
297
+ version: event.version,
298
+ payload_json: JSON.stringify(serializablePayload),
299
+ sha: event.payload.sha ?? null
300
+ };
301
+ }
302
+ eventsToBatch(events) {
303
+ const rows = events.map(e => this.eventToRow(e));
304
+ return {
305
+ rows,
306
+ rowCount: rows.length,
307
+ createdAt: Date.now(),
308
+ schema: ParquetSchema.forCDCEvents(),
309
+ compression: this.compression
310
+ };
311
+ }
312
+ async toParquetBuffer(batch) {
313
+ // Build a simplified Parquet-like buffer
314
+ // Real implementation would use a proper Parquet library
315
+ const encoder = new TextEncoder();
316
+ // Magic bytes
317
+ const magic = encoder.encode('PAR1');
318
+ // Serialize batch data
319
+ const dataJson = JSON.stringify({
320
+ rows: batch.rows,
321
+ rowCount: batch.rowCount,
322
+ createdAt: batch.createdAt,
323
+ schema: batch.schema,
324
+ compression: batch.compression
325
+ });
326
+ let dataBytes = encoder.encode(dataJson);
327
+ // Apply compression
328
+ if (this.compression === 'gzip') {
329
+ dataBytes = await this.gzipCompress(dataBytes);
330
+ }
331
+ else if (this.compression === 'snappy') {
332
+ // Snappy simulation (use simple compression)
333
+ dataBytes = await this.simpleCompress(dataBytes);
334
+ }
335
+ // Build final buffer: PAR1 + data + length (4 bytes) + PAR1
336
+ const lengthBytes = new Uint8Array(4);
337
+ new DataView(lengthBytes.buffer).setUint32(0, dataBytes.length, true);
338
+ const totalSize = 4 + dataBytes.length + 4 + 4;
339
+ const result = new Uint8Array(totalSize);
340
+ let offset = 0;
341
+ result.set(magic, offset);
342
+ offset += 4;
343
+ result.set(dataBytes, offset);
344
+ offset += dataBytes.length;
345
+ result.set(lengthBytes, offset);
346
+ offset += 4;
347
+ result.set(magic, offset);
348
+ return result;
349
+ }
350
+ async gzipCompress(data) {
351
+ // Use CompressionStream if available (modern browsers/Node 18+)
352
+ if (typeof CompressionStream !== 'undefined') {
353
+ const stream = new CompressionStream('gzip');
354
+ const writer = stream.writable.getWriter();
355
+ writer.write(data);
356
+ writer.close();
357
+ const reader = stream.readable.getReader();
358
+ const chunks = [];
359
+ let done = false;
360
+ while (!done) {
361
+ const result = await reader.read();
362
+ done = result.done;
363
+ if (result.value) {
364
+ chunks.push(result.value);
365
+ }
366
+ }
367
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
368
+ const result = new Uint8Array(totalLength);
369
+ let offset = 0;
370
+ for (const chunk of chunks) {
371
+ result.set(chunk, offset);
372
+ offset += chunk.length;
373
+ }
374
+ return result;
375
+ }
376
+ // Fallback: return data as-is (no compression)
377
+ return data;
378
+ }
379
+ async simpleCompress(data) {
380
+ // For snappy, we just return data as-is (real snappy compression would require a library)
381
+ // This is a simplified implementation that avoids async stream issues with fake timers
382
+ return data;
383
+ }
384
+ }
385
+ /**
386
+ * Batches CDC events for efficient processing
387
+ */
388
+ export class CDCBatcher {
389
+ config;
390
+ events = [];
391
+ batchHandlers = [];
392
+ flushTimer = null;
393
+ stopped = false;
394
+ constructor(config) {
395
+ this.config = config;
396
+ // Don't start timer in constructor - start when first event is added
397
+ }
398
+ ensureTimerRunning() {
399
+ if (this.stopped)
400
+ return;
401
+ if (this.flushTimer !== null)
402
+ return; // Already have a timer
403
+ this.flushTimer = setTimeout(() => {
404
+ this.flushTimer = null;
405
+ if (this.stopped)
406
+ return;
407
+ // Process pending events if any
408
+ if (this.events.length > 0) {
409
+ // Build batch result
410
+ const batchEvents = [...this.events];
411
+ this.events = [];
412
+ const sequences = batchEvents.map(e => e.sequence);
413
+ const timestamps = batchEvents.map(e => e.timestamp);
414
+ const result = {
415
+ events: batchEvents,
416
+ eventCount: batchEvents.length,
417
+ success: true,
418
+ minSequence: Math.min(...sequences),
419
+ maxSequence: Math.max(...sequences),
420
+ minTimestamp: Math.min(...timestamps),
421
+ maxTimestamp: Math.max(...timestamps)
422
+ };
423
+ // Notify handlers and handle promises
424
+ const handlerPromises = [];
425
+ for (const handler of this.batchHandlers) {
426
+ try {
427
+ const maybePromise = handler(result);
428
+ if (maybePromise && typeof maybePromise.then === 'function') {
429
+ handlerPromises.push(maybePromise);
430
+ }
431
+ }
432
+ catch {
433
+ // Ignore handler errors in timer context
434
+ }
435
+ }
436
+ // Execute all handlers and ignore the result
437
+ if (handlerPromises.length > 0) {
438
+ void Promise.all(handlerPromises);
439
+ }
440
+ }
441
+ // DON'T reschedule here - timer will be scheduled on next add() call
442
+ }, this.config.flushIntervalMs);
443
+ }
444
+ clearFlushTimer() {
445
+ if (this.flushTimer !== null) {
446
+ clearTimeout(this.flushTimer);
447
+ this.flushTimer = null;
448
+ }
449
+ }
450
+ async add(event) {
451
+ this.events.push(event);
452
+ // Ensure flush timer is running when we have pending events
453
+ this.ensureTimerRunning();
454
+ if (this.events.length >= this.config.batchSize) {
455
+ this.clearFlushTimer();
456
+ await this.flushInternal();
457
+ // Timer will be re-started on next add() if needed
458
+ }
459
+ }
460
+ async flushInternal() {
461
+ if (this.events.length === 0) {
462
+ return { events: [], eventCount: 0, success: true };
463
+ }
464
+ const batchEvents = [...this.events];
465
+ this.events = [];
466
+ const sequences = batchEvents.map(e => e.sequence);
467
+ const timestamps = batchEvents.map(e => e.timestamp);
468
+ const result = {
469
+ events: batchEvents,
470
+ eventCount: batchEvents.length,
471
+ success: true,
472
+ minSequence: Math.min(...sequences),
473
+ maxSequence: Math.max(...sequences),
474
+ minTimestamp: Math.min(...timestamps),
475
+ maxTimestamp: Math.max(...timestamps)
476
+ };
477
+ // Notify handlers (await async handlers)
478
+ for (const handler of this.batchHandlers) {
479
+ await handler(result);
480
+ }
481
+ return result;
482
+ }
483
+ async flush() {
484
+ this.clearFlushTimer();
485
+ const result = await this.flushInternal();
486
+ // Don't restart timer - it will be started on next add() if needed
487
+ return result;
488
+ }
489
+ getPendingCount() {
490
+ return this.events.length;
491
+ }
492
+ onBatch(handler) {
493
+ this.batchHandlers.push(handler);
494
+ }
495
+ async stop() {
496
+ this.stopped = true;
497
+ this.clearFlushTimer();
498
+ }
499
+ }
500
+ /**
501
+ * Main CDC Pipeline for processing git operation events
502
+ */
503
+ export class CDCPipeline {
504
+ config;
505
+ state = 'stopped';
506
+ batcher = null;
507
+ transformer;
508
+ outputHandlers = [];
509
+ deadLetterHandlers = [];
510
+ metrics = {
511
+ eventsProcessed: 0,
512
+ batchesGenerated: 0,
513
+ bytesWritten: 0,
514
+ errors: 0,
515
+ avgProcessingLatencyMs: 0
516
+ };
517
+ processingLatencies = [];
518
+ retryPolicy;
519
+ constructor(config) {
520
+ this.config = config;
521
+ this.transformer = new ParquetTransformer({
522
+ compression: config.parquetCompression
523
+ });
524
+ this.retryPolicy = new CDCRetryPolicy({
525
+ maxRetries: config.maxRetries,
526
+ initialDelayMs: 100,
527
+ maxDelayMs: 5000,
528
+ backoffMultiplier: 2
529
+ });
530
+ }
531
+ getState() {
532
+ return this.state;
533
+ }
534
+ async start() {
535
+ if (this.state === 'running')
536
+ return;
537
+ this.batcher = new CDCBatcher({
538
+ batchSize: this.config.batchSize,
539
+ flushIntervalMs: this.config.flushIntervalMs
540
+ });
541
+ this.batcher.onBatch(async (batch) => {
542
+ await this.handleBatch(batch);
543
+ });
544
+ this.state = 'running';
545
+ }
546
+ async stop() {
547
+ if (this.state === 'stopped') {
548
+ return { flushedCount: 0 };
549
+ }
550
+ let flushedCount = 0;
551
+ if (this.batcher) {
552
+ const result = await this.batcher.flush();
553
+ flushedCount = result.eventCount;
554
+ await this.batcher.stop();
555
+ this.batcher = null;
556
+ }
557
+ this.state = 'stopped';
558
+ return { flushedCount };
559
+ }
560
+ async process(event) {
561
+ if (this.state !== 'running') {
562
+ throw new CDCError('PROCESSING_ERROR', 'Pipeline is not running');
563
+ }
564
+ // Validate event
565
+ validateCDCEvent(event);
566
+ const startTime = Date.now();
567
+ await this.batcher.add(event);
568
+ this.metrics.eventsProcessed++;
569
+ const latency = Date.now() - startTime;
570
+ this.processingLatencies.push(latency);
571
+ this.updateAvgLatency();
572
+ return { success: true, eventId: event.id };
573
+ }
574
+ async processMany(events) {
575
+ const results = [];
576
+ for (const event of events) {
577
+ const result = await this.process(event);
578
+ results.push(result);
579
+ }
580
+ return results;
581
+ }
582
+ async flush() {
583
+ if (this.batcher) {
584
+ const result = await this.batcher.flush();
585
+ if (result.eventCount > 0) {
586
+ await this.handleBatch(result);
587
+ }
588
+ }
589
+ }
590
+ async handleBatch(batch) {
591
+ let attempts = 0;
592
+ let lastError = null;
593
+ while (attempts <= this.config.maxRetries) {
594
+ try {
595
+ const parquetBatch = this.transformer.eventsToBatch(batch.events);
596
+ const parquetBuffer = await this.transformer.toParquetBuffer(parquetBatch);
597
+ const output = {
598
+ parquetBuffer,
599
+ events: batch.events,
600
+ batchId: `batch-${Date.now()}-${Math.random().toString(36).slice(2)}`
601
+ };
602
+ // Notify output handlers
603
+ for (const handler of this.outputHandlers) {
604
+ handler(output);
605
+ }
606
+ this.metrics.batchesGenerated++;
607
+ this.metrics.bytesWritten += parquetBuffer.length;
608
+ return; // Success
609
+ }
610
+ catch (error) {
611
+ lastError = error;
612
+ attempts++;
613
+ this.metrics.errors++;
614
+ if (this.retryPolicy.shouldRetry(attempts)) {
615
+ const delay = this.retryPolicy.getDelay(attempts);
616
+ await this.sleep(delay);
617
+ }
618
+ }
619
+ }
620
+ // All retries exhausted - send to dead letter queue
621
+ if (lastError) {
622
+ for (const handler of this.deadLetterHandlers) {
623
+ handler(batch.events, lastError);
624
+ }
625
+ }
626
+ }
627
+ sleep(ms) {
628
+ return new Promise(resolve => setTimeout(resolve, ms));
629
+ }
630
+ updateAvgLatency() {
631
+ if (this.processingLatencies.length === 0)
632
+ return;
633
+ // Keep only last 1000 measurements
634
+ if (this.processingLatencies.length > 1000) {
635
+ this.processingLatencies = this.processingLatencies.slice(-1000);
636
+ }
637
+ const sum = this.processingLatencies.reduce((a, b) => a + b, 0);
638
+ this.metrics.avgProcessingLatencyMs = sum / this.processingLatencies.length;
639
+ }
640
+ getMetrics() {
641
+ return { ...this.metrics };
642
+ }
643
+ onOutput(handler) {
644
+ this.outputHandlers.push(handler);
645
+ }
646
+ onDeadLetter(handler) {
647
+ this.deadLetterHandlers.push(handler);
648
+ }
649
+ }
650
+ // ============================================================================
651
+ // Utility Functions
652
+ // ============================================================================
653
+ const VALID_EVENT_TYPES = [
654
+ 'OBJECT_CREATED',
655
+ 'OBJECT_DELETED',
656
+ 'REF_UPDATED',
657
+ 'PACK_RECEIVED',
658
+ 'COMMIT_CREATED',
659
+ 'TREE_MODIFIED',
660
+ 'BRANCH_CREATED',
661
+ 'BRANCH_DELETED',
662
+ 'TAG_CREATED',
663
+ 'MERGE_COMPLETED'
664
+ ];
665
+ /**
666
+ * Create a new CDC event
667
+ */
668
+ export function createCDCEvent(type, source, payload, options) {
669
+ return {
670
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
671
+ type,
672
+ source,
673
+ timestamp: Date.now(),
674
+ payload,
675
+ sequence: options?.sequence ?? 0,
676
+ version: 1
677
+ };
678
+ }
679
+ /**
680
+ * Serialize a CDC event to bytes
681
+ */
682
+ export function serializeEvent(event) {
683
+ // Create a serializable copy (Uint8Array is not JSON-serializable)
684
+ const serializable = {
685
+ ...event,
686
+ payload: {
687
+ ...event.payload,
688
+ data: event.payload.data ? Array.from(event.payload.data) : undefined
689
+ }
690
+ };
691
+ const json = JSON.stringify(serializable);
692
+ return new TextEncoder().encode(json);
693
+ }
694
+ /**
695
+ * Deserialize bytes to a CDC event
696
+ */
697
+ export function deserializeEvent(bytes) {
698
+ const json = new TextDecoder().decode(bytes);
699
+ const parsed = JSON.parse(json);
700
+ // Restore Uint8Array if data was serialized
701
+ if (parsed.payload?.data && Array.isArray(parsed.payload.data)) {
702
+ parsed.payload.data = new Uint8Array(parsed.payload.data);
703
+ }
704
+ return parsed;
705
+ }
706
+ /**
707
+ * Validate a CDC event
708
+ */
709
+ export function validateCDCEvent(event) {
710
+ if (!event) {
711
+ throw new CDCError('VALIDATION_ERROR', 'Event is null or undefined');
712
+ }
713
+ if (!event.id || typeof event.id !== 'string' || event.id.length === 0) {
714
+ throw new CDCError('VALIDATION_ERROR', 'Event id is missing or invalid');
715
+ }
716
+ if (!VALID_EVENT_TYPES.includes(event.type)) {
717
+ throw new CDCError('VALIDATION_ERROR', `Invalid event type: ${event.type}`);
718
+ }
719
+ if (typeof event.timestamp !== 'number' || event.timestamp < 0) {
720
+ throw new CDCError('VALIDATION_ERROR', 'Invalid timestamp');
721
+ }
722
+ if (typeof event.sequence !== 'number' || event.sequence < 0) {
723
+ throw new CDCError('VALIDATION_ERROR', 'Invalid sequence number');
724
+ }
725
+ return event;
726
+ }
727
+ // ============================================================================
728
+ // Pipeline Operations
729
+ // ============================================================================
730
+ const activePipelines = new Map();
731
+ /**
732
+ * Start a pipeline with the given configuration
733
+ */
734
+ export function startPipeline(id, config) {
735
+ const pipeline = new CDCPipeline(config);
736
+ pipeline.start();
737
+ activePipelines.set(id, pipeline);
738
+ return pipeline;
739
+ }
740
+ /**
741
+ * Stop a pipeline by ID
742
+ */
743
+ export async function stopPipeline(id) {
744
+ const pipeline = activePipelines.get(id);
745
+ if (!pipeline) {
746
+ return { flushedCount: 0 };
747
+ }
748
+ const result = await pipeline.stop();
749
+ activePipelines.delete(id);
750
+ return result;
751
+ }
752
+ /**
753
+ * Flush a pipeline by ID
754
+ */
755
+ export async function flushPipeline(id) {
756
+ const pipeline = activePipelines.get(id);
757
+ if (pipeline) {
758
+ await pipeline.flush();
759
+ }
760
+ }
761
+ /**
762
+ * Get metrics for a pipeline by ID
763
+ */
764
+ export function getPipelineMetrics(id) {
765
+ const pipeline = activePipelines.get(id);
766
+ if (!pipeline) {
767
+ return null;
768
+ }
769
+ return pipeline.getMetrics();
770
+ }
771
+ //# sourceMappingURL=cdc-pipeline.js.map