@teamlearners/clawops 0.32.1 → 0.34.0

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.
@@ -0,0 +1,662 @@
1
+ 'use strict';
2
+
3
+ var chunkTVDUOKJD_cjs = require('../chunk-TVDUOKJD.cjs');
4
+
5
+ // src/solapi/_concurrency.ts
6
+ var DEFAULT_CONCURRENCY = 10;
7
+ async function mapWithConcurrency(items, limit, run) {
8
+ const results = new Array(items.length);
9
+ let cursor = 0;
10
+ const worker = async () => {
11
+ for (let index = cursor++; index < items.length; index = cursor++) {
12
+ results[index] = await run(items[index]);
13
+ }
14
+ };
15
+ const workers = Math.min(Math.max(1, limit), items.length);
16
+ await Promise.all(Array.from({ length: workers }, worker));
17
+ return results;
18
+ }
19
+
20
+ // src/solapi/_message-type.ts
21
+ var SMS_MAX_EUCKR_BYTES = 90;
22
+ function euckrByteLength(text) {
23
+ let bytes = 0;
24
+ for (const ch of text) bytes += ch.codePointAt(0) < 128 ? 1 : 2;
25
+ return bytes;
26
+ }
27
+ var normalizePhone = (value) => value.replace(/-/g, "");
28
+ var upperType = (message) => message.type === void 0 ? "" : String(message.type).toUpperCase();
29
+ function clawopsType(message) {
30
+ const type = upperType(message);
31
+ if (type === "SMS") return "sms";
32
+ if (type === "LMS") return "lms";
33
+ if (message.subject !== void 0 && message.subject !== null && message.subject !== "") {
34
+ return "lms";
35
+ }
36
+ return euckrByteLength(message.text ?? "") > SMS_MAX_EUCKR_BYTES ? "lms" : "sms";
37
+ }
38
+
39
+ // src/solapi/fallback-text.ts
40
+ var VARIABLE_KEY = /^#\{.+\}$/;
41
+ var LEFTOVER = /#\{[^}]*\}/g;
42
+ var asKey = (key) => VARIABLE_KEY.test(key) ? key : `#{${key}}`;
43
+ function render(content, variables = {}) {
44
+ let out = content;
45
+ for (const [key, value] of Object.entries(variables)) {
46
+ out = out.split(asKey(key)).join(value);
47
+ }
48
+ return out;
49
+ }
50
+ var leftovers = (text) => text.match(LEFTOVER) ?? [];
51
+ function templateRef(kakaoOptions) {
52
+ if (kakaoOptions === null || typeof kakaoOptions !== "object") return {};
53
+ const options = kakaoOptions;
54
+ const templateId = typeof options.templateId === "string" ? options.templateId : void 0;
55
+ let variables;
56
+ const raw = options.variables;
57
+ if (raw !== null && typeof raw === "object") {
58
+ const entries = Object.entries(raw).filter(
59
+ (entry) => typeof entry[1] === "string"
60
+ );
61
+ if (entries.length > 0) variables = Object.fromEntries(entries);
62
+ }
63
+ return { templateId, variables };
64
+ }
65
+ var DEFAULT_FALLBACK_FIELD = "clawopsFallbackText";
66
+ async function resolveFallbackText(message, solapi, options = {}) {
67
+ const field = options.field ?? DEFAULT_FALLBACK_FIELD;
68
+ const explicit = message.customFields?.[field];
69
+ if (explicit !== void 0) return finish(explicit, "customFields");
70
+ const { templateId, variables } = templateRef(message.kakaoOptions);
71
+ if (templateId !== void 0) {
72
+ const cache = options.cache;
73
+ let pending = cache?.get(templateId);
74
+ if (pending === void 0) {
75
+ pending = solapi.getKakaoAlimtalkTemplate(templateId).then((template) => template.content);
76
+ cache?.set(templateId, pending);
77
+ pending.catch(() => cache?.delete(templateId));
78
+ }
79
+ const content = await pending;
80
+ if (content === void 0) {
81
+ return { ok: false, reason: "no_template_content", source: "template" };
82
+ }
83
+ return finish(render(content, variables), "template");
84
+ }
85
+ if (message.text === void 0 || message.text === null) {
86
+ return { ok: false, reason: "no_text", source: "text" };
87
+ }
88
+ return finish(message.text, "text");
89
+ }
90
+ function finish(text, source) {
91
+ const unresolved = leftovers(text);
92
+ return unresolved.length > 0 ? { ok: false, reason: "unresolved_variables", unresolved, source } : { ok: true, text, source };
93
+ }
94
+
95
+ // src/solapi/_fallback.ts
96
+ async function deliverFallback(request, solapi, context) {
97
+ let resolved;
98
+ try {
99
+ resolved = await resolveFallbackText(request.source, solapi, {
100
+ field: context.field,
101
+ cache: context.cache
102
+ });
103
+ } catch {
104
+ resolved = { ok: false, reason: "no_template_content", source: "template" };
105
+ }
106
+ if (!resolved.ok) return { status: "blocked", reason: resolved };
107
+ const subject = request.source.subject ?? void 0;
108
+ try {
109
+ const created = await context.clawops.messages.create({
110
+ to: normalizePhone(request.to),
111
+ from: normalizePhone(request.from),
112
+ body: resolved.text,
113
+ type: clawopsType({ text: resolved.text, subject }),
114
+ subject,
115
+ idempotencyKey: request.idempotencyKey
116
+ });
117
+ return {
118
+ status: "sent",
119
+ source: resolved.source,
120
+ text: resolved.text,
121
+ messageId: created.messageId
122
+ };
123
+ } catch (error) {
124
+ return { status: "send_failed", source: resolved.source, text: resolved.text, error };
125
+ }
126
+ }
127
+
128
+ // src/solapi/sweep.ts
129
+ var FALLBACK_MARKER_FIELD = "clawopsFallback";
130
+ var FALLBACK_MARKER_VALUE = "1";
131
+ var DEFAULT_FALLBACK_CODES = ["3104", "3107", "3102"];
132
+ var DEFAULT_SWEEP_TYPES = ["ATA"];
133
+ var DELIVERY_FAILURE = /^3\d{3}$/;
134
+ var IN_FLIGHT_CODE = "3000";
135
+ var DEFAULT_LOOKBACK_MS = 60 * 60 * 1e3;
136
+ var PAGE_SIZE = 200;
137
+ var MAX_PAGES = 50;
138
+ var firstRecipient = (to) => Array.isArray(to) ? to[0] : to;
139
+ var timeOf = (row) => row.dateUpdated ?? row.dateCreated;
140
+ async function sweepFailedAlimtalk(options) {
141
+ const targets = new Set(options.on ?? DEFAULT_FALLBACK_CODES);
142
+ const boundary = new Set(options.cursor?.seen ?? []);
143
+ const templateCache = options.templateCache ?? /* @__PURE__ */ new Map();
144
+ const window = options.lookbackMs ?? DEFAULT_LOOKBACK_MS;
145
+ const startDate = options.cursor ? new Date(options.cursor.updatedAt) : new Date(Date.now() - window);
146
+ const endDate = new Date(Math.min(Date.now(), startDate.getTime() + window));
147
+ const candidates = [];
148
+ const ineligible = [];
149
+ let scanned = 0;
150
+ let maxTime = startDate.getTime();
151
+ let atMax = new Set(boundary);
152
+ let truncated = false;
153
+ for (const type of options.types ?? DEFAULT_SWEEP_TYPES) {
154
+ let startKey;
155
+ for (let page = 0; page < MAX_PAGES; page += 1) {
156
+ const response = await options.solapi.getMessages({
157
+ type,
158
+ dateType: "UPDATED",
159
+ startDate,
160
+ endDate,
161
+ limit: PAGE_SIZE,
162
+ startKey
163
+ });
164
+ const rows = Object.values(response.messageList ?? {});
165
+ if (rows.length === 0) break;
166
+ scanned += rows.length;
167
+ for (const row of rows) {
168
+ const at = timeOf(row);
169
+ const id = row.messageId;
170
+ if (at && id) {
171
+ const time = new Date(at).getTime();
172
+ if (time > maxTime) {
173
+ maxTime = time;
174
+ atMax = /* @__PURE__ */ new Set([id]);
175
+ } else if (time === maxTime) {
176
+ atMax.add(id);
177
+ }
178
+ }
179
+ if (!id || boundary.has(id)) continue;
180
+ if (options.skip?.(id)) continue;
181
+ if (row.customFields?.[FALLBACK_MARKER_FIELD] !== FALLBACK_MARKER_VALUE) continue;
182
+ const code = String(row.statusCode ?? "");
183
+ if (code === IN_FLIGHT_CODE || !DELIVERY_FAILURE.test(code)) continue;
184
+ if (!targets.has(code)) {
185
+ ineligible.push(row);
186
+ continue;
187
+ }
188
+ candidates.push(row);
189
+ }
190
+ startKey = response.nextKey ?? void 0;
191
+ if (!startKey) break;
192
+ if (page === MAX_PAGES - 1) truncated = true;
193
+ }
194
+ }
195
+ const deliveries = await mapWithConcurrency(
196
+ candidates,
197
+ options.concurrency ?? DEFAULT_CONCURRENCY,
198
+ async (row) => {
199
+ const to = firstRecipient(row.to);
200
+ if (to === void 0) return void 0;
201
+ return deliverFallback(
202
+ { source: row, to, from: options.from, idempotencyKey: `solapi:${row.messageId}` },
203
+ options.solapi,
204
+ { clawops: options.clawops, field: options.fallbackField, cache: templateCache }
205
+ );
206
+ }
207
+ );
208
+ const processed = [];
209
+ let blocked = 0;
210
+ for (const row of ineligible) {
211
+ blocked += 1;
212
+ options.onBlocked?.({
213
+ messageId: row.messageId ?? "",
214
+ to: firstRecipient(row.to) ?? "",
215
+ statusCode: String(row.statusCode ?? ""),
216
+ ok: false,
217
+ reason: "code_not_eligible"
218
+ });
219
+ }
220
+ deliveries.forEach((delivery, index) => {
221
+ const row = candidates[index];
222
+ const messageId = row.messageId ?? "";
223
+ const to = firstRecipient(row.to) ?? "";
224
+ const statusCode = String(row.statusCode ?? "");
225
+ if (delivery === void 0) return;
226
+ if (delivery.status !== "sent") {
227
+ blocked += 1;
228
+ options.onBlocked?.(
229
+ delivery.status === "blocked" ? { messageId, to, statusCode, ...delivery.reason } : (
230
+ // 문구는 만들었는데 ClawOps 가 거절했다. 다음 스윕이 다시 시도하고,
231
+ // 멱등키가 같아 중복 발송은 되지 않는다
232
+ { messageId, to, statusCode, ok: false, reason: "send_rejected" }
233
+ )
234
+ );
235
+ return;
236
+ }
237
+ processed.push(messageId);
238
+ options.onFallback?.({
239
+ messageId,
240
+ to,
241
+ statusCode,
242
+ source: delivery.source,
243
+ text: delivery.text
244
+ });
245
+ });
246
+ const cursor = truncated ? options.cursor ?? { updatedAt: startDate.toISOString(), seen: [] } : { updatedAt: new Date(maxTime).toISOString(), seen: [...atMax] };
247
+ return { cursor, scanned, sent: processed.length, blocked, truncated, processed };
248
+ }
249
+
250
+ // src/solapi/index.ts
251
+ var ZERO_CASH = { requested: 0, replacement: 0, refund: 0, sum: 0 };
252
+ var CHARGE_KEYS = [
253
+ "sms",
254
+ "lms",
255
+ "mms",
256
+ "ata",
257
+ "cta",
258
+ "cti",
259
+ "nsa",
260
+ "rcs_sms",
261
+ "rcs_lms",
262
+ "rcs_mms",
263
+ "rcs_tpl"
264
+ ];
265
+ var NO_CHARGE = Object.fromEntries(
266
+ CHARGE_KEYS.map((k) => [k, {}])
267
+ );
268
+ var NO_PROFIT = Object.fromEntries(CHARGE_KEYS.map((k) => [k, 0]));
269
+ var OURS = /* @__PURE__ */ new Set(["SMS", "LMS", "MMS"]);
270
+ var VENDOR_OPTION_FIELDS = [
271
+ "kakaoOptions",
272
+ "rcsOptions",
273
+ "naverOptions",
274
+ "voiceOptions",
275
+ "faxOptions"
276
+ ];
277
+ var hasVendorOptions = (message) => VENDOR_OPTION_FIELDS.some((field) => message[field] != null);
278
+ var recipientsOf = (to) => Array.isArray(to) ? to : [to];
279
+ var CLAWOPS_FAILURE_STATUS_CODE = "CLAWOPS";
280
+ function normalizeFallback(config) {
281
+ if (config === void 0 || config === true) return { enabled: true };
282
+ if (config === false || !config.enabled) return void 0;
283
+ return config;
284
+ }
285
+ function isMessageNotReceived(error) {
286
+ return typeof error === "object" && error !== null && "failedMessageList" in error && Array.isArray(error.failedMessageList);
287
+ }
288
+ function assertConfigSupported(config) {
289
+ if (!config) return;
290
+ const unsupported = [];
291
+ if (config.scheduledDate !== void 0) unsupported.push("scheduledDate");
292
+ if (config.allowDuplicates === false) unsupported.push("allowDuplicates: false");
293
+ if (unsupported.length > 0) {
294
+ throw new chunkTVDUOKJD_cjs.SolapiBridgeError(
295
+ `ClawOps \uB85C \uBCF4\uB0B4\uB294 \uBA54\uC2DC\uC9C0\uC5D0\uB294 ${unsupported.join(", ")} \uB97C \uC801\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD574\uB2F9 \uC124\uC815\uC774 \uD544\uC694\uD55C \uBC1C\uC1A1\uC740 \uC194\uB77C\uD53C\uB85C \uBCF4\uB0B4\uAC70\uB098 \uD638\uCD9C\uC744 \uBD84\uB9AC\uD558\uC2ED\uC2DC\uC624.`
296
+ );
297
+ }
298
+ }
299
+ function makeGroupInfo(base, counts) {
300
+ if (base) {
301
+ return {
302
+ ...base,
303
+ count: {
304
+ ...base.count,
305
+ total: base.count.total + counts.ok + counts.failed,
306
+ registeredSuccess: base.count.registeredSuccess + counts.ok,
307
+ registeredFailed: base.count.registeredFailed + counts.failed,
308
+ sentReplacement: base.count.sentReplacement + counts.replacement
309
+ }
310
+ };
311
+ }
312
+ const registered = {
313
+ total: counts.ok + counts.failed,
314
+ registeredSuccess: counts.ok,
315
+ registeredFailed: counts.failed
316
+ };
317
+ const now = (/* @__PURE__ */ new Date()).toISOString();
318
+ return {
319
+ count: {
320
+ ...registered,
321
+ sentTotal: 0,
322
+ sentFailed: 0,
323
+ sentSuccess: 0,
324
+ sentPending: 0,
325
+ sentReplacement: counts.replacement,
326
+ refund: 0
327
+ },
328
+ countForCharge: NO_CHARGE,
329
+ balance: ZERO_CASH,
330
+ point: ZERO_CASH,
331
+ app: { profit: NO_PROFIT, appId: null },
332
+ log: [],
333
+ status: "PENDING",
334
+ allowDuplicates: false,
335
+ isRefunded: false,
336
+ accountId: "",
337
+ masterAccountId: null,
338
+ apiVersion: "4",
339
+ // 솔라피 그룹이 아니므로 getGroupMessages 로 조회되지 않는다. 접두사로 구분한다
340
+ groupId: `CLAWOPS-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
341
+ price: {},
342
+ dateCreated: now,
343
+ dateUpdated: now,
344
+ scheduledDate: null,
345
+ dateSent: null,
346
+ dateCompleted: null
347
+ };
348
+ }
349
+ function partition(list) {
350
+ const forSolapi = [];
351
+ const forClawOps = [];
352
+ for (const message of list) {
353
+ const type = upperType(message);
354
+ if (type !== "" && !OURS.has(type) || hasVendorOptions(message)) {
355
+ forSolapi.push(message);
356
+ continue;
357
+ }
358
+ if (message.imageId !== void 0) {
359
+ throw new chunkTVDUOKJD_cjs.SolapiBridgeError(
360
+ "imageId \uB294 \uC194\uB77C\uD53C\uC5D0 \uC5C5\uB85C\uB4DC\uB41C \uD30C\uC77C ID \uB77C ClawOps \uB85C \uC804\uB2EC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBBF8\uC9C0 \uCCA8\uBD80\uAC00 \uD544\uC694\uD558\uBA74 ClawOps \uC758 mediaUrl \uB85C \uC9C1\uC811 \uBC1C\uC1A1\uD558\uC2ED\uC2DC\uC624."
361
+ );
362
+ }
363
+ for (const to of recipientsOf(message.to)) forClawOps.push({ to, message });
364
+ }
365
+ return { forSolapi, forClawOps };
366
+ }
367
+ function fallbackFrom(message) {
368
+ const kakaoOptions = message?.kakaoOptions;
369
+ if (!kakaoOptions || kakaoOptions.disableSms === true) return void 0;
370
+ return message?.from;
371
+ }
372
+ function clawopsFailure(to, from, error) {
373
+ return {
374
+ to: normalizePhone(to),
375
+ from: normalizePhone(from),
376
+ type: "SMS",
377
+ statusMessage: error instanceof Error ? error.message : String(error),
378
+ country: "82",
379
+ messageId: "",
380
+ statusCode: CLAWOPS_FAILURE_STATUS_CODE,
381
+ accountId: ""
382
+ };
383
+ }
384
+ function buildPlans(messages, fallbackEnabled) {
385
+ return messages.map((message) => {
386
+ const kakaoOptions = message.kakaoOptions;
387
+ if (!kakaoOptions || !fallbackEnabled || kakaoOptions.bms) {
388
+ return { outgoing: message, source: message };
389
+ }
390
+ const fields = message.customFields ?? {};
391
+ const canMark = FALLBACK_MARKER_FIELD in fields || Object.keys(fields).length < 10;
392
+ if (!canMark) return { outgoing: message, source: message };
393
+ const { from: _from, ...rest } = message;
394
+ let customFields = message.customFields;
395
+ if (fallbackFrom(message) !== void 0) {
396
+ customFields = { ...fields, [FALLBACK_MARKER_FIELD]: FALLBACK_MARKER_VALUE };
397
+ }
398
+ return {
399
+ outgoing: {
400
+ ...rest,
401
+ ...customFields ? { customFields } : {},
402
+ kakaoOptions: { ...kakaoOptions, disableSms: true }
403
+ },
404
+ source: message
405
+ };
406
+ });
407
+ }
408
+ function matchFailures(plans, failures) {
409
+ const byRecipient = /* @__PURE__ */ new Map();
410
+ plans.forEach((plan, index) => {
411
+ for (const to of recipientsOf(plan.outgoing.to)) {
412
+ const key = normalizePhone(to);
413
+ const bucket = byRecipient.get(key);
414
+ if (bucket) bucket.push(index);
415
+ else byRecipient.set(key, [index]);
416
+ }
417
+ });
418
+ return failures.map((failure) => {
419
+ const index = byRecipient.get(normalizePhone(failure.to))?.shift();
420
+ return { failure, plan: index === void 0 ? void 0 : plans[index] };
421
+ });
422
+ }
423
+ function makeSend(options, fallbackConfig, templateCache) {
424
+ const fallbackEnabled = fallbackConfig !== void 0;
425
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
426
+ const viaClawOps = async (to, message, statusMessage = "\uC815\uC0C1 \uC811\uC218") => {
427
+ const from = normalizePhone(message.from ?? options.from);
428
+ const type = clawopsType(message);
429
+ try {
430
+ const created = await options.clawops.messages.create({
431
+ to: normalizePhone(to),
432
+ from,
433
+ body: message.text ?? "",
434
+ type,
435
+ subject: message.subject
436
+ });
437
+ return {
438
+ ok: true,
439
+ sent: { messageId: created.messageId, statusCode: "2000", statusMessage }
440
+ };
441
+ } catch (error) {
442
+ return {
443
+ ok: false,
444
+ failed: {
445
+ to: normalizePhone(to),
446
+ from,
447
+ type: type.toUpperCase(),
448
+ statusMessage: error instanceof Error ? error.message : String(error),
449
+ country: "82",
450
+ messageId: "",
451
+ statusCode: CLAWOPS_FAILURE_STATUS_CODE,
452
+ accountId: ""
453
+ }
454
+ };
455
+ }
456
+ };
457
+ const runFallbacks = async (plans, failures, solapi) => {
458
+ const jobs = matchFailures(plans, failures);
459
+ const deliveries = await mapWithConcurrency(jobs, concurrency, async (job) => {
460
+ const from = fallbackFrom(job.plan?.source);
461
+ if (from === void 0) return void 0;
462
+ return deliverFallback({ source: job.plan.source, to: job.failure.to, from }, solapi, {
463
+ clawops: options.clawops,
464
+ field: fallbackConfig?.field,
465
+ cache: templateCache
466
+ });
467
+ });
468
+ const sent = [];
469
+ const failed = [];
470
+ let replacement = 0;
471
+ let failedCount = 0;
472
+ deliveries.forEach((delivery, index) => {
473
+ const { failure } = jobs[index];
474
+ if (delivery === void 0) {
475
+ failed.push(failure);
476
+ return;
477
+ }
478
+ if (delivery.status === "blocked") {
479
+ fallbackConfig?.onBlocked?.({ to: failure.to, ...delivery.reason });
480
+ failed.push(failure);
481
+ return;
482
+ }
483
+ if (delivery.status === "send_failed") {
484
+ failed.push(clawopsFailure(failure.to, options.from, delivery.error));
485
+ failedCount += 1;
486
+ return;
487
+ }
488
+ sent.push({
489
+ messageId: delivery.messageId,
490
+ statusCode: "2000",
491
+ statusMessage: "\uC815\uC0C1 \uC811\uC218(\uB300\uCCB4\uBC1C\uC1A1)"
492
+ });
493
+ replacement += 1;
494
+ fallbackConfig?.onFallback?.({
495
+ to: failure.to,
496
+ source: delivery.source,
497
+ text: delivery.text
498
+ });
499
+ });
500
+ return { sent, failed, replacement, failedCount };
501
+ };
502
+ return async (messages, config) => {
503
+ const list = Array.isArray(messages) ? messages : [messages];
504
+ const { forSolapi, forClawOps } = partition(list);
505
+ const sent = [];
506
+ const failed = [];
507
+ let replacement = 0;
508
+ let ourOk = 0;
509
+ let ourFailed = 0;
510
+ const willFallback = fallbackEnabled && forSolapi.some((message) => fallbackFrom(message) !== void 0);
511
+ if (forClawOps.length > 0 || willFallback) assertConfigSupported(config);
512
+ if (forClawOps.length > 0) {
513
+ for (const outcome of await mapWithConcurrency(
514
+ forClawOps,
515
+ concurrency,
516
+ (item) => viaClawOps(item.to, item.message)
517
+ )) {
518
+ if (outcome.ok) {
519
+ sent.push(outcome.sent);
520
+ ourOk += 1;
521
+ } else {
522
+ failed.push(outcome.failed);
523
+ ourFailed += 1;
524
+ }
525
+ }
526
+ }
527
+ let base = null;
528
+ if (forSolapi.length > 0) {
529
+ const solapi = options.solapi;
530
+ if (!solapi) {
531
+ throw new chunkTVDUOKJD_cjs.SolapiBridgeError(
532
+ "\uC54C\uB9BC\uD1A1\xB7RCS \uB97C \uBCF4\uB0B4\uB824\uBA74 solapi \uC778\uC2A4\uD134\uC2A4\uB97C \uB118\uACA8\uC57C \uD569\uB2C8\uB2E4. new ClawOpsMessageService({ solapi: new SolapiMessageService(key, secret), \u2026 })"
533
+ );
534
+ }
535
+ const plans = buildPlans(forSolapi, fallbackEnabled);
536
+ let solapiFailed = [];
537
+ try {
538
+ const response = await solapi.send(
539
+ plans.map((plan) => plan.outgoing),
540
+ config
541
+ );
542
+ base = response.groupInfo;
543
+ sent.push(...response.messageList ?? []);
544
+ solapiFailed = response.failedMessageList;
545
+ } catch (error) {
546
+ if (!isMessageNotReceived(error)) throw error;
547
+ solapiFailed = error.failedMessageList;
548
+ }
549
+ if (solapiFailed.length > 0) {
550
+ if (!fallbackEnabled) {
551
+ failed.push(...solapiFailed);
552
+ } else {
553
+ const result = await runFallbacks(plans, solapiFailed, solapi);
554
+ sent.push(...result.sent);
555
+ failed.push(...result.failed);
556
+ replacement += result.replacement;
557
+ ourFailed += result.failedCount;
558
+ }
559
+ }
560
+ }
561
+ return {
562
+ groupInfo: makeGroupInfo(base, { ok: ourOk, failed: ourFailed, replacement }),
563
+ messageList: sent,
564
+ failedMessageList: failed
565
+ };
566
+ };
567
+ }
568
+ var NON_METHOD_KEYS = /* @__PURE__ */ new Set(["then", "toJSON", "inspect"]);
569
+ var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
570
+ var PROCESSED_LIMIT = 1e4;
571
+ function startSweepLoop(options, sweep, solapi, templateCache) {
572
+ const processed = /* @__PURE__ */ new Set();
573
+ let cursor = sweep.initialCursor;
574
+ let running = false;
575
+ const tick = async () => {
576
+ if (running) return;
577
+ running = true;
578
+ try {
579
+ const result = await sweepFailedAlimtalk({
580
+ clawops: options.clawops,
581
+ solapi,
582
+ from: options.from,
583
+ cursor,
584
+ lookbackMs: sweep.lookbackMs,
585
+ on: sweep.on,
586
+ types: sweep.types,
587
+ fallbackField: sweep.field,
588
+ concurrency: options.concurrency,
589
+ templateCache,
590
+ skip: (messageId) => processed.has(messageId),
591
+ onFallback: sweep.onFallback,
592
+ onBlocked: sweep.onBlocked
593
+ });
594
+ cursor = result.cursor;
595
+ for (const messageId of result.processed) processed.add(messageId);
596
+ if (processed.size > PROCESSED_LIMIT) {
597
+ for (const messageId of [...processed].slice(0, processed.size - PROCESSED_LIMIT / 2)) {
598
+ processed.delete(messageId);
599
+ }
600
+ }
601
+ if (result.scanned > 0) sweep.onCursor?.(result.cursor);
602
+ } catch (error) {
603
+ sweep.onError?.(error);
604
+ } finally {
605
+ running = false;
606
+ }
607
+ };
608
+ const timer = setInterval(() => void tick(), sweep.intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS);
609
+ timer.unref?.();
610
+ }
611
+ function create(options) {
612
+ const fallbackConfig = normalizeFallback(options.fallback);
613
+ const templateCache = /* @__PURE__ */ new Map();
614
+ const send = makeSend(options, fallbackConfig, templateCache);
615
+ if (fallbackConfig?.mode === "sweep") {
616
+ if (!options.solapi) {
617
+ throw new chunkTVDUOKJD_cjs.SolapiBridgeError(
618
+ "mode: 'sweep' \uC740 \uC194\uB77C\uD53C \uB9AC\uD3EC\uD2B8\uB97C \uD6D1\uB294 \uAE30\uB2A5\uC774\uB77C solapi \uC778\uC2A4\uD134\uC2A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uBB38\uC790\uB9CC \uBCF4\uB0B8\uB2E4\uBA74 \uC774 \uC124\uC815\uC740 \uD560 \uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
619
+ );
620
+ }
621
+ startSweepLoop(options, fallbackConfig, options.solapi, templateCache);
622
+ }
623
+ const solapi = options.solapi;
624
+ const handler = solapi ? {
625
+ get(instance, property) {
626
+ if (property === "send") return send;
627
+ const value = Reflect.get(instance, property, instance);
628
+ return typeof value === "function" ? value.bind(instance) : value;
629
+ }
630
+ } : {
631
+ get(_instance, property) {
632
+ if (property === "send") return send;
633
+ if (typeof property === "symbol" || NON_METHOD_KEYS.has(property)) return void 0;
634
+ if (property in Object.prototype) {
635
+ return Reflect.get(Object.prototype, property);
636
+ }
637
+ return () => {
638
+ throw new chunkTVDUOKJD_cjs.SolapiBridgeError(
639
+ `${String(property)}() \uB294 \uC194\uB77C\uD53C \uAE30\uB2A5\uC785\uB2C8\uB2E4. solapi \uC778\uC2A4\uD134\uC2A4\uB97C \uB118\uACA8\uC8FC\uC138\uC694.`
640
+ );
641
+ };
642
+ }
643
+ };
644
+ return new Proxy(solapi ?? {}, handler);
645
+ }
646
+ var ClawOpsMessageService = function(options) {
647
+ return create(options);
648
+ };
649
+
650
+ Object.defineProperty(exports, "SolapiBridgeError", {
651
+ enumerable: true,
652
+ get: function () { return chunkTVDUOKJD_cjs.SolapiBridgeError; }
653
+ });
654
+ exports.CLAWOPS_FAILURE_STATUS_CODE = CLAWOPS_FAILURE_STATUS_CODE;
655
+ exports.ClawOpsMessageService = ClawOpsMessageService;
656
+ exports.DEFAULT_FALLBACK_CODES = DEFAULT_FALLBACK_CODES;
657
+ exports.DEFAULT_FALLBACK_FIELD = DEFAULT_FALLBACK_FIELD;
658
+ exports.FALLBACK_MARKER_FIELD = FALLBACK_MARKER_FIELD;
659
+ exports.resolveFallbackText = resolveFallbackText;
660
+ exports.sweepFailedAlimtalk = sweepFailedAlimtalk;
661
+ //# sourceMappingURL=index.cjs.map
662
+ //# sourceMappingURL=index.cjs.map