deepline 0.3.16 → 0.3.18

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 (27) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +7 -0
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/shared_libs/integrations/bettercontact-execution-policy.ts +29 -0
  4. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +45 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +19 -3
  6. package/dist/bundling-sources/shared_libs/play-runtime/bettercontact-batching.ts +726 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +45 -24
  8. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +254 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +6 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +28 -3
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +3 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +5 -2
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +344 -26
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +50 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +1 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +378 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/runtime-reliability-policy.ts +391 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runtime-traffic-policy.ts +125 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +21 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +36 -39
  22. package/dist/cli/index.js +265 -1
  23. package/dist/cli/index.mjs +265 -1
  24. package/dist/index.js +265 -1
  25. package/dist/index.mjs +265 -1
  26. package/dist/install-integrity.json +6 -0
  27. package/package.json +1 -1
@@ -0,0 +1,726 @@
1
+ import {
2
+ defineBatchStrategyMap,
3
+ type BatchOperationStrategy,
4
+ } from './batching-types';
5
+
6
+ const BETTERCONTACT_BATCH_ITEM_KEY = 'deepline_batch_item_key';
7
+ const BETTERCONTACT_BATCH_SIZE = 100;
8
+ const BETTERCONTACT_SCOPED_REQUEST_PREFIX = 'deepline-bc-batch-v1:';
9
+ const BETTERCONTACT_DEFAULT_POLL_INTERVAL_MS = 2_000;
10
+ const BETTERCONTACT_MAX_SYNC_WAIT_MS = 30_000;
11
+ const BETTERCONTACT_SINGLE_PAYLOAD_FIELDS = new Set([
12
+ 'first_name',
13
+ 'last_name',
14
+ 'company_domain',
15
+ 'company',
16
+ 'linkedin_url',
17
+ 'custom_fields',
18
+ 'enrich_email_address',
19
+ 'enrich_phone_number',
20
+ 'wait_for_completion',
21
+ 'poll_interval_ms',
22
+ 'max_wait_ms',
23
+ ]);
24
+
25
+ type BetterContactCustomFields = Record<string, unknown>;
26
+
27
+ type BetterContactContact = {
28
+ first_name: string;
29
+ last_name: string;
30
+ company_domain?: string;
31
+ company?: string;
32
+ linkedin_url?: string;
33
+ custom_fields?: BetterContactCustomFields;
34
+ };
35
+
36
+ type BetterContactControls = {
37
+ enrich_email_address?: boolean;
38
+ enrich_phone_number?: boolean;
39
+ wait_for_completion?: boolean;
40
+ poll_interval_ms?: number;
41
+ max_wait_ms?: number;
42
+ };
43
+
44
+ type BetterContactSinglePayload = BetterContactContact & BetterContactControls;
45
+
46
+ type BetterContactBulkPayload = BetterContactControls & {
47
+ contacts: BetterContactContact[];
48
+ };
49
+
50
+ type BetterContactResult = Record<string, unknown> & {
51
+ data?: unknown[];
52
+ };
53
+
54
+ type BetterContactBatchResult = BetterContactResult | unknown[];
55
+
56
+ export type BetterContactScopedRequest = {
57
+ requestId: string;
58
+ correlationField: string;
59
+ correlationValue: string;
60
+ };
61
+
62
+ // Fields BetterContact's AsyncGetResponse reports once for the whole batch
63
+ // request, never per contact: `summary` (a batch-wide breakdown),
64
+ // `credits_consumed`/`credits_left` (the request's total spend/remaining
65
+ // balance). Billing settlement reads these from BetterContact's own async
66
+ // status endpoint independently (see billing.ts's fetchTruth), never from
67
+ // this per-row public shape, so stripping them here only affects what a
68
+ // caller sees on each row -- it cannot desync actual charging. Left
69
+ // unstripped, a 20-contact batch that spent 20 credits total would report
70
+ // `credits_consumed: 20` on every one of the 20 rows, misrepresenting a
71
+ // per-request total as a per-row cost.
72
+ const BETTERCONTACT_BATCH_LEVEL_ONLY_FIELDS = [
73
+ 'summary',
74
+ 'credits_consumed',
75
+ 'credits_left',
76
+ ] as const;
77
+
78
+ function withoutBatchLevelSummary(
79
+ result: BetterContactResult,
80
+ logicalItemCount: number,
81
+ ): BetterContactResult {
82
+ if (logicalItemCount <= 1) {
83
+ return result;
84
+ }
85
+ const perItemResult = { ...result };
86
+ for (const field of BETTERCONTACT_BATCH_LEVEL_ONLY_FIELDS) {
87
+ delete perItemResult[field];
88
+ }
89
+ return perItemResult;
90
+ }
91
+
92
+ function stableValue(value: unknown): unknown {
93
+ if (Array.isArray(value)) {
94
+ return value.map(stableValue);
95
+ }
96
+ if (value && typeof value === 'object') {
97
+ return Object.fromEntries(
98
+ Object.entries(value as Record<string, unknown>)
99
+ .filter(([, entry]) => entry !== undefined)
100
+ .sort(([left], [right]) => left.localeCompare(right))
101
+ .map(([key, entry]) => [key, stableValue(entry)]),
102
+ );
103
+ }
104
+ return value;
105
+ }
106
+
107
+ function stableStringify(value: unknown): string {
108
+ return JSON.stringify(stableValue(value));
109
+ }
110
+
111
+ function shortStableHash(value: string): string {
112
+ let hash = 0x811c9dc5;
113
+ for (let index = 0; index < value.length; index += 1) {
114
+ hash ^= value.charCodeAt(index);
115
+ hash = Math.imul(hash, 0x01000193) >>> 0;
116
+ }
117
+ return hash.toString(36);
118
+ }
119
+
120
+ function normalizedControls(payload: BetterContactControls) {
121
+ const waitForCompletion = payload.wait_for_completion !== false;
122
+ return {
123
+ enrich_phone_number: payload.enrich_phone_number === true,
124
+ wait_for_completion: waitForCompletion,
125
+ ...(waitForCompletion
126
+ ? {
127
+ poll_interval_ms:
128
+ payload.poll_interval_ms ?? BETTERCONTACT_DEFAULT_POLL_INTERVAL_MS,
129
+ max_wait_ms: Math.min(
130
+ payload.max_wait_ms ?? BETTERCONTACT_MAX_SYNC_WAIT_MS,
131
+ BETTERCONTACT_MAX_SYNC_WAIT_MS,
132
+ ),
133
+ }
134
+ : {}),
135
+ };
136
+ }
137
+
138
+ function controlsKey(payload: BetterContactControls): string {
139
+ return stableStringify(normalizedControls(payload));
140
+ }
141
+
142
+ function copyControls(payload: BetterContactControls): BetterContactControls {
143
+ return {
144
+ ...(payload.enrich_email_address !== undefined
145
+ ? { enrich_email_address: payload.enrich_email_address }
146
+ : {}),
147
+ ...(payload.enrich_phone_number !== undefined
148
+ ? { enrich_phone_number: payload.enrich_phone_number }
149
+ : {}),
150
+ ...(payload.wait_for_completion !== undefined
151
+ ? { wait_for_completion: payload.wait_for_completion }
152
+ : {}),
153
+ ...(payload.poll_interval_ms !== undefined
154
+ ? { poll_interval_ms: payload.poll_interval_ms }
155
+ : {}),
156
+ ...(payload.max_wait_ms !== undefined
157
+ ? { max_wait_ms: payload.max_wait_ms }
158
+ : {}),
159
+ };
160
+ }
161
+
162
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
163
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
164
+ }
165
+
166
+ function hasValidOptionalBoolean(
167
+ payload: Record<string, unknown>,
168
+ field: string,
169
+ ): boolean {
170
+ return payload[field] === undefined || typeof payload[field] === 'boolean';
171
+ }
172
+
173
+ function hasValidOptionalInteger(
174
+ payload: Record<string, unknown>,
175
+ field: string,
176
+ minimum: number,
177
+ maximum: number,
178
+ ): boolean {
179
+ const value = payload[field];
180
+ return (
181
+ value === undefined ||
182
+ (typeof value === 'number' &&
183
+ Number.isInteger(value) &&
184
+ value >= minimum &&
185
+ value <= maximum)
186
+ );
187
+ }
188
+
189
+ function hasValidControls(payload: BetterContactControls): boolean {
190
+ const record = payload as Record<string, unknown>;
191
+ return (
192
+ hasValidOptionalBoolean(record, 'enrich_email_address') &&
193
+ hasValidOptionalBoolean(record, 'enrich_phone_number') &&
194
+ hasValidOptionalBoolean(record, 'wait_for_completion') &&
195
+ hasValidOptionalInteger(record, 'poll_interval_ms', 250, 10_000) &&
196
+ hasValidOptionalInteger(record, 'max_wait_ms', 1_000, 120_000)
197
+ );
198
+ }
199
+
200
+ function hasValidCustomFields(contact: BetterContactContact): boolean {
201
+ const customFields = (contact as Record<string, unknown>).custom_fields;
202
+ return customFields === undefined || isPlainRecord(customFields);
203
+ }
204
+
205
+ function isBatchableContact(contact: BetterContactContact): boolean {
206
+ return hasRequiredContactFields(contact) && hasValidCustomFields(contact);
207
+ }
208
+
209
+ function hasOnlyKnownSinglePayloadFields(
210
+ payload: BetterContactSinglePayload,
211
+ ): boolean {
212
+ return Object.keys(payload).every((field) =>
213
+ BETTERCONTACT_SINGLE_PAYLOAD_FIELDS.has(field),
214
+ );
215
+ }
216
+
217
+ function isBatchableSinglePayload(
218
+ payload: BetterContactSinglePayload,
219
+ ): boolean {
220
+ return (
221
+ isBatchableContact(payload) &&
222
+ hasValidControls(payload) &&
223
+ hasOnlyKnownSinglePayloadFields(payload)
224
+ );
225
+ }
226
+
227
+ function contactIdentity(contact: BetterContactContact): string {
228
+ return stableStringify({
229
+ first_name: contact.first_name,
230
+ last_name: contact.last_name,
231
+ company_domain: contact.company_domain ?? null,
232
+ company: contact.company ?? null,
233
+ linkedin_url: contact.linkedin_url ?? null,
234
+ custom_fields: contact.custom_fields ?? null,
235
+ });
236
+ }
237
+
238
+ function buildItemKey(contact: BetterContactContact, index = 0): string {
239
+ return `dl_bc_${index}_${shortStableHash(contactIdentity(contact))}`;
240
+ }
241
+
242
+ function withItemKey(
243
+ contact: BetterContactContact,
244
+ itemKey: string,
245
+ ): BetterContactContact {
246
+ const customFields = contact.custom_fields ?? {};
247
+ const correlationFieldBase = `${BETTERCONTACT_BATCH_ITEM_KEY}_${shortStableHash(
248
+ itemKey,
249
+ )}`;
250
+ let correlationField = correlationFieldBase;
251
+ let suffix = 1;
252
+ while (Object.prototype.hasOwnProperty.call(customFields, correlationField)) {
253
+ correlationField = `${correlationFieldBase}_${suffix}`;
254
+ suffix += 1;
255
+ }
256
+ return {
257
+ ...contact,
258
+ custom_fields: {
259
+ ...customFields,
260
+ [correlationField]: itemKey,
261
+ },
262
+ };
263
+ }
264
+
265
+ function resultCustomFieldRecords(
266
+ row: unknown,
267
+ ): Array<Record<string, unknown>> {
268
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
269
+ return [];
270
+ }
271
+ const customFields = (row as Record<string, unknown>).custom_fields;
272
+ return (Array.isArray(customFields) ? customFields : [customFields]).filter(
273
+ (candidate): candidate is Record<string, unknown> =>
274
+ Boolean(candidate) &&
275
+ typeof candidate === 'object' &&
276
+ !Array.isArray(candidate),
277
+ );
278
+ }
279
+
280
+ function encodeScopedRequest(scope: BetterContactScopedRequest): string {
281
+ return `${BETTERCONTACT_SCOPED_REQUEST_PREFIX}${encodeURIComponent(
282
+ JSON.stringify([
283
+ scope.requestId,
284
+ scope.correlationField,
285
+ scope.correlationValue,
286
+ ]),
287
+ )}`;
288
+ }
289
+
290
+ export function parseBetterContactScopedRequest(
291
+ requestId: string,
292
+ ): BetterContactScopedRequest | null {
293
+ if (!requestId.startsWith(BETTERCONTACT_SCOPED_REQUEST_PREFIX)) {
294
+ return null;
295
+ }
296
+ try {
297
+ const parsed = JSON.parse(
298
+ decodeURIComponent(
299
+ requestId.slice(BETTERCONTACT_SCOPED_REQUEST_PREFIX.length),
300
+ ),
301
+ ) as unknown;
302
+ if (
303
+ !Array.isArray(parsed) ||
304
+ parsed.length !== 3 ||
305
+ parsed.some((value) => typeof value !== 'string' || value.length === 0)
306
+ ) {
307
+ return null;
308
+ }
309
+ return {
310
+ requestId: parsed[0] as string,
311
+ correlationField: parsed[1] as string,
312
+ correlationValue: parsed[2] as string,
313
+ };
314
+ } catch {
315
+ return null;
316
+ }
317
+ }
318
+
319
+ function rowMatchesCorrelation(
320
+ row: unknown,
321
+ correlationField: string,
322
+ correlationValue: string,
323
+ ): boolean {
324
+ return resultCustomFieldRecords(row).some(
325
+ (fields) => fields[correlationField] === correlationValue,
326
+ );
327
+ }
328
+
329
+ function withoutCorrelationSelector(
330
+ row: unknown,
331
+ correlationField: string,
332
+ ): unknown {
333
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
334
+ return row;
335
+ }
336
+ const record = row as Record<string, unknown>;
337
+ const customFields = record.custom_fields;
338
+ const cleanRecord = (fields: Record<string, unknown>) => {
339
+ const cleaned = { ...fields };
340
+ delete cleaned[correlationField];
341
+ return cleaned;
342
+ };
343
+ return {
344
+ ...record,
345
+ custom_fields: Array.isArray(customFields)
346
+ ? customFields
347
+ .filter(isPlainRecord)
348
+ .map(cleanRecord)
349
+ .filter((fields) => Object.keys(fields).length > 0)
350
+ : isPlainRecord(customFields)
351
+ ? cleanRecord(customFields)
352
+ : customFields,
353
+ };
354
+ }
355
+
356
+ export function isolateBetterContactScopedResult(
357
+ result: unknown,
358
+ scopedRequestId: string,
359
+ ): unknown {
360
+ const scope = parseBetterContactScopedRequest(scopedRequestId);
361
+ if (!scope || !isPlainRecord(result)) {
362
+ return result;
363
+ }
364
+ const scopedResult: BetterContactResult = { ...result, id: scopedRequestId };
365
+ delete scopedResult.summary;
366
+ if (!Array.isArray(result.data)) {
367
+ return scopedResult;
368
+ }
369
+ if (result.data.length === 0) {
370
+ return { ...scopedResult, data: [] };
371
+ }
372
+ const matches = result.data.filter((row) =>
373
+ rowMatchesCorrelation(row, scope.correlationField, scope.correlationValue),
374
+ );
375
+ if (matches.length !== 1) {
376
+ throw new Error(
377
+ matches.length === 0
378
+ ? 'BetterContact scoped result has missing or unmatched correlation identity.'
379
+ : 'BetterContact scoped result has ambiguous correlation identity.',
380
+ );
381
+ }
382
+ return {
383
+ ...scopedResult,
384
+ data: [withoutCorrelationSelector(matches[0], scope.correlationField)],
385
+ };
386
+ }
387
+
388
+ function correlationEntryForItem(item: {
389
+ itemKey: string;
390
+ payload: BetterContactContact;
391
+ }): [string, unknown] | null {
392
+ const keyedContact = withItemKey(item.payload, item.itemKey);
393
+ const originalFields = item.payload.custom_fields ?? {};
394
+ return (
395
+ Object.entries(keyedContact.custom_fields ?? {}).find(
396
+ ([field]) => !Object.prototype.hasOwnProperty.call(originalFields, field),
397
+ ) ?? null
398
+ );
399
+ }
400
+
401
+ function resultMatchesItem(
402
+ row: unknown,
403
+ item: { itemKey: string; payload: BetterContactContact },
404
+ ): boolean {
405
+ const correlationEntry = correlationEntryForItem(item);
406
+ if (!correlationEntry) {
407
+ return false;
408
+ }
409
+ const [correlationField, correlationValue] = correlationEntry;
410
+ return rowMatchesCorrelation(row, correlationField, String(correlationValue));
411
+ }
412
+
413
+ function withoutCorrelationField(
414
+ row: unknown,
415
+ item: { itemKey: string; payload: BetterContactContact },
416
+ ): unknown {
417
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
418
+ return row;
419
+ }
420
+ const correlationField = correlationEntryForItem(item)?.[0];
421
+ if (!correlationField) {
422
+ return row;
423
+ }
424
+ return withoutCorrelationSelector(row, correlationField);
425
+ }
426
+
427
+ function scopedRequestIdForItem(
428
+ requestId: string,
429
+ item: { itemKey: string; payload: BetterContactContact },
430
+ ): string {
431
+ const correlationEntry = correlationEntryForItem(item);
432
+ if (!correlationEntry) {
433
+ throw new Error(
434
+ 'BetterContact batch item is missing correlation identity.',
435
+ );
436
+ }
437
+ return encodeScopedRequest({
438
+ requestId,
439
+ correlationField: correlationEntry[0],
440
+ correlationValue: String(correlationEntry[1]),
441
+ });
442
+ }
443
+
444
+ function withScopedRequestId<TPayload extends Record<string, unknown>>(
445
+ result: BetterContactResult,
446
+ item: { itemKey: string; payload: TPayload },
447
+ logicalItemCount: number,
448
+ ): BetterContactResult {
449
+ const requestId = typeof result.id === 'string' ? result.id.trim() : '';
450
+ if (!requestId || logicalItemCount <= 1) {
451
+ return result;
452
+ }
453
+ return {
454
+ ...result,
455
+ id: scopedRequestIdForItem(requestId, {
456
+ itemKey: item.itemKey,
457
+ payload: item.payload as unknown as BetterContactContact,
458
+ }),
459
+ };
460
+ }
461
+
462
+ function hasRequiredContactFields(contact: BetterContactContact): boolean {
463
+ const companyDomain =
464
+ typeof contact.company_domain === 'string' &&
465
+ contact.company_domain.trim().length > 0
466
+ ? contact.company_domain
467
+ : contact.company;
468
+ return (
469
+ typeof contact.first_name === 'string' &&
470
+ contact.first_name.trim().length > 0 &&
471
+ typeof contact.last_name === 'string' &&
472
+ contact.last_name.trim().length > 0 &&
473
+ typeof companyDomain === 'string' &&
474
+ companyDomain.trim().length > 0
475
+ );
476
+ }
477
+
478
+ function splitBetterContactResult<TPayload extends Record<string, unknown>>(
479
+ fullResult: BetterContactBatchResult,
480
+ compiled: {
481
+ items: Array<{ itemKey: string; payload: TPayload }>;
482
+ },
483
+ ) {
484
+ const resultRows = Array.isArray(fullResult)
485
+ ? fullResult
486
+ : Array.isArray(fullResult.data)
487
+ ? fullResult.data
488
+ : null;
489
+ if (!resultRows) {
490
+ return compiled.items.map((item) => ({
491
+ itemKey: item.itemKey,
492
+ result: {
493
+ data: Array.isArray(fullResult)
494
+ ? fullResult
495
+ : withScopedRequestId(
496
+ withoutBatchLevelSummary(fullResult, compiled.items.length),
497
+ item,
498
+ compiled.items.length,
499
+ ),
500
+ },
501
+ rawResult: fullResult,
502
+ }));
503
+ }
504
+ if (resultRows.length === 0) {
505
+ return compiled.items.map((item) => ({
506
+ itemKey: item.itemKey,
507
+ result: {
508
+ data: Array.isArray(fullResult)
509
+ ? []
510
+ : withScopedRequestId(
511
+ {
512
+ ...withoutBatchLevelSummary(fullResult, compiled.items.length),
513
+ data: [],
514
+ },
515
+ item,
516
+ compiled.items.length,
517
+ ),
518
+ },
519
+ rawResult: null,
520
+ }));
521
+ }
522
+
523
+ const remainingItems = new Map(
524
+ compiled.items.map((item) => [item.itemKey, item] as const),
525
+ );
526
+ const rowsByItemKey = new Map<string, unknown>();
527
+ for (const row of resultRows) {
528
+ const matches = [...remainingItems.values()].filter((item) =>
529
+ resultMatchesItem(row, {
530
+ itemKey: item.itemKey,
531
+ payload: item.payload as unknown as BetterContactContact,
532
+ }),
533
+ );
534
+ if (matches.length !== 1) {
535
+ throw new Error(
536
+ matches.length === 0
537
+ ? 'BetterContact bulk result has missing or unmatched correlation identity.'
538
+ : 'BetterContact bulk result has ambiguous correlation identity.',
539
+ );
540
+ }
541
+ const matchedItem = matches[0]!;
542
+ remainingItems.delete(matchedItem.itemKey);
543
+ rowsByItemKey.set(matchedItem.itemKey, row);
544
+ }
545
+
546
+ return compiled.items.map((item) => {
547
+ const matchedRow = rowsByItemKey.get(item.itemKey);
548
+ if (!matchedRow) {
549
+ throw new Error(
550
+ `BetterContact bulk result is missing result identity ${item.itemKey}.`,
551
+ );
552
+ }
553
+ const publicRow = withoutCorrelationField(matchedRow, {
554
+ itemKey: item.itemKey,
555
+ payload: item.payload as unknown as BetterContactContact,
556
+ });
557
+ const resultData = Array.isArray(fullResult)
558
+ ? [publicRow]
559
+ : withScopedRequestId(
560
+ {
561
+ ...withoutBatchLevelSummary(fullResult, compiled.items.length),
562
+ data: [publicRow],
563
+ },
564
+ item,
565
+ compiled.items.length,
566
+ );
567
+ return {
568
+ itemKey: item.itemKey,
569
+ result: { data: resultData },
570
+ rawResult: publicRow,
571
+ };
572
+ });
573
+ }
574
+
575
+ const sharedStrategyFields = {
576
+ batchOperation: 'bettercontact_bulk_enrich' as const,
577
+ kind: 'identifier_batch' as const,
578
+ maxBatchSize: BETTERCONTACT_BATCH_SIZE,
579
+ bucketKeyPayloadFields: ['enrich_phone_number'],
580
+ };
581
+
582
+ const bettercontactSingleBatchStrategy: BatchOperationStrategy<
583
+ BetterContactSinglePayload,
584
+ BetterContactBulkPayload,
585
+ BetterContactBatchResult,
586
+ { data: BetterContactBatchResult },
587
+ unknown
588
+ > = {
589
+ ...sharedStrategyFields,
590
+ sourceOperation: 'bettercontact_enrich',
591
+ canBatchWith(left, right) {
592
+ return (
593
+ isBatchableSinglePayload(left) &&
594
+ isBatchableSinglePayload(right) &&
595
+ controlsKey(left) === controlsKey(right)
596
+ );
597
+ },
598
+ toBucketKey(payload) {
599
+ if (!isBatchableSinglePayload(payload)) {
600
+ return `bettercontact_enrich:invalid:${stableStringify(payload)}`;
601
+ }
602
+ return `bettercontact_bulk_enrich:${controlsKey(payload)}`;
603
+ },
604
+ toItemKey(payload) {
605
+ return buildItemKey(payload);
606
+ },
607
+ compile(payloads) {
608
+ const first = payloads[0] ?? ({} as BetterContactSinglePayload);
609
+ const items = payloads.map((payload, index) => ({
610
+ itemKey: buildItemKey(payload, index),
611
+ payload,
612
+ }));
613
+ return {
614
+ batchOperation: 'bettercontact_bulk_enrich',
615
+ batchPayload: {
616
+ ...copyControls(first),
617
+ contacts: items.map((item) =>
618
+ isBatchableSinglePayload(item.payload)
619
+ ? withItemKey(item.payload, item.itemKey)
620
+ : item.payload,
621
+ ),
622
+ },
623
+ items,
624
+ };
625
+ },
626
+ splitResult: splitBetterContactResult,
627
+ };
628
+
629
+ function readBulkContacts(
630
+ payload: BetterContactBulkPayload,
631
+ ): BetterContactContact[] {
632
+ return Array.isArray(payload.contacts) ? payload.contacts : [];
633
+ }
634
+
635
+ function isOneContactBulkPayload(payload: BetterContactBulkPayload): boolean {
636
+ return readBulkContacts(payload).length === 1;
637
+ }
638
+
639
+ const bettercontactBulkSelfBatchStrategy: BatchOperationStrategy<
640
+ BetterContactBulkPayload,
641
+ BetterContactBulkPayload,
642
+ BetterContactBatchResult,
643
+ { data: BetterContactBatchResult },
644
+ unknown
645
+ > = {
646
+ ...sharedStrategyFields,
647
+ sourceOperation: 'bettercontact_bulk_enrich',
648
+ canBatchWith(left, right) {
649
+ return (
650
+ isOneContactBulkPayload(left) &&
651
+ isOneContactBulkPayload(right) &&
652
+ isBatchableContact(readBulkContacts(left)[0]!) &&
653
+ isBatchableContact(readBulkContacts(right)[0]!) &&
654
+ hasValidControls(left) &&
655
+ hasValidControls(right) &&
656
+ controlsKey(left) === controlsKey(right)
657
+ );
658
+ },
659
+ toBucketKey(payload) {
660
+ if (!isOneContactBulkPayload(payload)) {
661
+ return `bettercontact_bulk_enrich:passthrough:${stableStringify(payload)}`;
662
+ }
663
+ if (
664
+ !isBatchableContact(readBulkContacts(payload)[0]!) ||
665
+ !hasValidControls(payload)
666
+ ) {
667
+ return `bettercontact_bulk_enrich:invalid:${stableStringify(payload)}`;
668
+ }
669
+ return `bettercontact_bulk_enrich:${controlsKey(payload)}`;
670
+ },
671
+ toItemKey(payload) {
672
+ return buildItemKey(
673
+ readBulkContacts(payload)[0] ?? ({} as BetterContactContact),
674
+ );
675
+ },
676
+ compile(payloads) {
677
+ const first = payloads[0] ?? ({ contacts: [] } as BetterContactBulkPayload);
678
+ if (payloads.length === 1 && !isOneContactBulkPayload(first)) {
679
+ return {
680
+ batchOperation: 'bettercontact_bulk_enrich',
681
+ batchPayload: first,
682
+ items: [{ itemKey: this.toItemKey(first), payload: first }],
683
+ };
684
+ }
685
+
686
+ const items = payloads.map((payload, index) => ({
687
+ itemKey: buildItemKey(readBulkContacts(payload)[0]!, index),
688
+ payload,
689
+ }));
690
+ return {
691
+ batchOperation: 'bettercontact_bulk_enrich',
692
+ batchPayload: {
693
+ ...copyControls(first),
694
+ contacts: items.map((item) => {
695
+ const contact = readBulkContacts(item.payload)[0]!;
696
+ return isBatchableContact(contact) && hasValidControls(item.payload)
697
+ ? withItemKey(contact, item.itemKey)
698
+ : contact;
699
+ }),
700
+ },
701
+ items,
702
+ };
703
+ },
704
+ splitResult(fullResult, compiled) {
705
+ const first = compiled.items[0];
706
+ if (
707
+ compiled.items.length === 1 &&
708
+ first &&
709
+ !isOneContactBulkPayload(first.payload)
710
+ ) {
711
+ return [
712
+ {
713
+ itemKey: first.itemKey,
714
+ result: { data: fullResult },
715
+ rawResult: fullResult,
716
+ },
717
+ ];
718
+ }
719
+ return splitBetterContactResult(fullResult, compiled);
720
+ },
721
+ };
722
+
723
+ export const bettercontactBatchStrategies = defineBatchStrategyMap({
724
+ bettercontact_enrich: bettercontactSingleBatchStrategy,
725
+ bettercontact_bulk_enrich: bettercontactBulkSelfBatchStrategy,
726
+ });