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.
- package/dist/bundling-sources/sdk/src/client.ts +7 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/integrations/bettercontact-execution-policy.ts +29 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +19 -3
- package/dist/bundling-sources/shared_libs/play-runtime/bettercontact-batching.ts +726 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +45 -24
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +254 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +5 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +344 -26
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +50 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-reliability-policy.ts +391 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-traffic-policy.ts +125 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +36 -39
- package/dist/cli/index.js +265 -1
- package/dist/cli/index.mjs +265 -1
- package/dist/index.js +265 -1
- package/dist/index.mjs +265 -1
- package/dist/install-integrity.json +6 -0
- 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
|
+
});
|