@h-ai/reach 1.0.0-beta.21 → 1.0.0-beta.23
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/README.en.md +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/index.d.ts +1 -15
- package/dist/index.js +39 -145
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.en.md
CHANGED
|
@@ -175,7 +175,7 @@ Production rejects console providers and requires SMTP, SMS or an actual API ser
|
|
|
175
175
|
|
|
176
176
|
Use `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })` for passwords, OTPs and reset tokens. expiresAt is Unix milliseconds; stable operation keys must contain no credentials. A shared ledger stores only metadata receipts/status and redacted destinations, never bodies, variables or credential hashes. There is no delayed credential queue. Success returns receiptId; DND/expired messages are not sent, and uncertain delivery is never automatically repeated. Explicit development console output is isolated from persistent observers. Production Console restrictions remain. Normal messages may use deferPolicy: reject to avoid a second queue.
|
|
177
177
|
|
|
178
|
-
`
|
|
178
|
+
`reach.init()` creates and validates the send log and its queue identifier constraint. Incompatible tables cause initialization to fail. Preserve send receipts and reconcile uncertain outcomes before attempting further credential delivery.
|
|
179
179
|
|
|
180
180
|
### API acknowledgements and destination boundaries
|
|
181
181
|
|
package/README.ja.md
CHANGED
|
@@ -175,7 +175,7 @@ if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
|
|
|
175
175
|
|
|
176
176
|
パスワード、OTP、リセットトークンは `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })` を使います。expiresAt は Unix ミリ秒、操作キーに秘密値を含めません。共有台帳は本文・変数・認証値のハッシュを保存せず、脱機密化した宛先と受領状態のみを保持します。遅延キューに入れず、DND/期限切れは送信せず、不明な配信を自動再送しません。成功時は receiptId を返します。明示した開発用コンソール出力は永続観測から分離し、本番 Console 制限は維持します。通常通知は deferPolicy: reject で二重キューを防げます。
|
|
177
177
|
|
|
178
|
-
`
|
|
178
|
+
`reach.init()` は送信ログとキュー識別子の制約を作成・検証し、構造が一致しない場合は初期化に失敗します。送信台帳を保存し、結果が不明な資格情報通知は永続レシートで状態を確認してください。
|
|
179
179
|
|
|
180
180
|
### API 応答と送信先の境界
|
|
181
181
|
|
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
|
|
|
175
175
|
|
|
176
176
|
发送密码、OTP 或重置令牌时使用 `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })`。expiresAt 是 Unix 毫秒,稳定操作键不可包含凭据。必须有共享发送账本:只持久化无载荷回执/状态,接收方脱敏,不存正文、变量或低熵凭据摘要,也不进入延迟队列。发送成功返回 receiptId;DND/过期不投递,异常或不明状态不自动重投。明确启用的开发控制台仍可显示业务允许的通知,持久采集观察器被隔离;生产 Console Provider 限制不变。普通通知可用 deferPolicy: reject 阻止二次排队。
|
|
177
177
|
|
|
178
|
-
`
|
|
178
|
+
`reach.init()` 创建并校验发送日志表及队列标识约束,结构不符时返回初始化失败。保留发送账本,按持久回执核对状态,避免重发结果不明的凭据通知。
|
|
179
179
|
|
|
180
180
|
### API 回执与目标边界
|
|
181
181
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as _h_ai_core from '@h-ai/core';
|
|
3
3
|
import { HaiResult } from '@h-ai/core';
|
|
4
|
-
import * as _h_ai_reldb from '@h-ai/reldb';
|
|
5
|
-
import { ReldbFunctions } from '@h-ai/reldb';
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* @h-ai/reach — 配置 Schema
|
|
@@ -1377,17 +1375,5 @@ interface ReachFunctions extends SendOperations {
|
|
|
1377
1375
|
*/
|
|
1378
1376
|
declare const reach: ReachFunctions;
|
|
1379
1377
|
|
|
1380
|
-
|
|
1381
|
-
* 预检或升级 Reach 队列。旧凭据相关载荷清除,待发凭据丢弃,保留发送状态和历史标识。
|
|
1382
|
-
* @example
|
|
1383
|
-
* ```ts
|
|
1384
|
-
* import { migrateReach } from '@h-ai/reach'
|
|
1385
|
-
* const plan = await migrateReach({ dryRun: true })
|
|
1386
|
-
* ```
|
|
1387
|
-
*/
|
|
1388
|
-
declare function migrateReach(options?: {
|
|
1389
|
-
dryRun?: boolean;
|
|
1390
|
-
}, db?: ReldbFunctions): Promise<_h_ai_core.HaiResult<_h_ai_reldb.SchemaMigrationPlan>>;
|
|
1391
|
-
|
|
1392
|
-
export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, migrateReach, reach };
|
|
1378
|
+
export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, reach };
|
|
1393
1379
|
export type { AliyunSmsProviderConfig, ApiProviderConfig, ConsoleProviderConfig, DndConfig, ProviderConfig, ReachChannel, ReachConfig, ReachConfigInput, ReachDeadRequeue, ReachFunctions, ReachGatewayHook, ReachInitOptions, ReachMessage, ReachQueueConfig, ReachQueueEntry, ReachQueueFailure, ReachQueueListOptions, ReachQueueOperations, ReachQueueStatus, ReachTemplate, ReachTemplateRegistry, ReachUnknownResolution, RenderedTemplate, SendOperations, SendResult, SmtpProviderConfig, TemplateConfig };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { core, err, ok } from '@h-ai/core';
|
|
3
3
|
import process from 'process';
|
|
4
4
|
import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
5
|
-
import {
|
|
5
|
+
import { createHash, randomUUID, createHmac } from 'crypto';
|
|
6
6
|
import { addAbortListener } from 'events';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import { cache } from '@h-ai/cache';
|
|
@@ -11,6 +11,7 @@ import { cache } from '@h-ai/cache';
|
|
|
11
11
|
|
|
12
12
|
// messages/en-US.json
|
|
13
13
|
var en_US_default = {
|
|
14
|
+
reach_sendLogIndexRequired: "The send log table requires a unique index on receipt IDs",
|
|
14
15
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
15
16
|
reach_notInitialized: "Reach module not initialized, call reach.init() first",
|
|
16
17
|
reach_initFailed: "Reach module initialization failed: {error}",
|
|
@@ -70,6 +71,7 @@ var en_US_default = {
|
|
|
70
71
|
|
|
71
72
|
// messages/ja-JP.json
|
|
72
73
|
var ja_JP_default = {
|
|
74
|
+
reach_sendLogIndexRequired: "\u9001\u4FE1\u30ED\u30B0\u30C6\u30FC\u30D6\u30EB\u306B\u306F\u53D7\u4ED8 ID \u306E\u4E00\u610F\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u304C\u5FC5\u8981\u3067\u3059",
|
|
73
75
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
74
76
|
reach_notInitialized: "\u901A\u77E5\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Breach.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
75
77
|
reach_initFailed: "\u901A\u77E5\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
@@ -129,6 +131,7 @@ var ja_JP_default = {
|
|
|
129
131
|
|
|
130
132
|
// messages/zh-CN.json
|
|
131
133
|
var zh_CN_default = {
|
|
134
|
+
reach_sendLogIndexRequired: "\u53D1\u9001\u65E5\u5FD7\u8868\u5FC5\u987B\u4E3A\u56DE\u6267\u7F16\u53F7\u5EFA\u7ACB\u552F\u4E00\u7D22\u5F15",
|
|
132
135
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
133
136
|
reach_notInitialized: "\u89E6\u8FBE\u6A21\u5757\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 reach.init()",
|
|
134
137
|
reach_initFailed: "\u89E6\u8FBE\u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25: {error}",
|
|
@@ -483,7 +486,7 @@ function createAliyunSmsProvider(gatewayHook) {
|
|
|
483
486
|
}
|
|
484
487
|
const templateCode = message.extra?.templateCode;
|
|
485
488
|
if (!config.gateway)
|
|
486
|
-
logger.debug("Sending SMS", {
|
|
489
|
+
logger.debug("Sending SMS", { templateCode });
|
|
487
490
|
try {
|
|
488
491
|
const params = {
|
|
489
492
|
AccessKeyId: config.accessKeyId,
|
|
@@ -525,14 +528,18 @@ function createAliyunSmsProvider(gatewayHook) {
|
|
|
525
528
|
reachM("reach_smsSendFailed")
|
|
526
529
|
);
|
|
527
530
|
}
|
|
528
|
-
logger.info("SMS sent", {
|
|
531
|
+
logger.info("SMS sent", { bizId: body.BizId });
|
|
529
532
|
return ok({ success: true, messageId: body.BizId });
|
|
530
533
|
} catch (error) {
|
|
531
534
|
if (config.gateway) {
|
|
532
|
-
logger.warn("SMS gateway request failed"
|
|
535
|
+
logger.warn("SMS gateway request failed", {
|
|
536
|
+
causeName: error instanceof Error ? error.name : void 0
|
|
537
|
+
});
|
|
533
538
|
return err(HaiReachError.SEND_FAILED, reachM("reach_smsGatewayFailed"));
|
|
534
539
|
}
|
|
535
|
-
logger.
|
|
540
|
+
logger.warn("SMS send failed", {
|
|
541
|
+
causeName: error instanceof Error ? error.name : void 0
|
|
542
|
+
});
|
|
536
543
|
return err(toReachError());
|
|
537
544
|
}
|
|
538
545
|
}
|
|
@@ -713,7 +720,6 @@ function createConsoleProvider() {
|
|
|
713
720
|
async send(message) {
|
|
714
721
|
logger3.debug("Sending message via console", {
|
|
715
722
|
provider: message.provider,
|
|
716
|
-
to: message.to,
|
|
717
723
|
hasSubject: Boolean(message.subject),
|
|
718
724
|
bodyLength: message.body?.length ?? 0,
|
|
719
725
|
template: message.template,
|
|
@@ -725,6 +731,15 @@ function createConsoleProvider() {
|
|
|
725
731
|
};
|
|
726
732
|
}
|
|
727
733
|
var logger4 = core.logger.child({ module: "reach", scope: "provider-smtp" });
|
|
734
|
+
function smtpErrorDiagnostics(error) {
|
|
735
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
736
|
+
const responseCode = error && typeof error === "object" && "responseCode" in error ? error.responseCode : void 0;
|
|
737
|
+
return {
|
|
738
|
+
causeName: error instanceof Error ? error.name : void 0,
|
|
739
|
+
causeCode: typeof code === "string" && /^[A-Z0-9_]{1,64}$/.test(code) ? code : void 0,
|
|
740
|
+
responseCode: typeof responseCode === "number" ? responseCode : void 0
|
|
741
|
+
};
|
|
742
|
+
}
|
|
728
743
|
function toReachError3() {
|
|
729
744
|
return {
|
|
730
745
|
...HaiReachError.SEND_FAILED,
|
|
@@ -759,7 +774,7 @@ function createSmtpProvider() {
|
|
|
759
774
|
logger4.info("SMTP provider connected", { host: config.host, port: config.port });
|
|
760
775
|
return ok(void 0);
|
|
761
776
|
} catch (error) {
|
|
762
|
-
logger4.error("SMTP connection failed",
|
|
777
|
+
logger4.error("SMTP connection failed", smtpErrorDiagnostics(error));
|
|
763
778
|
return err(toReachError3());
|
|
764
779
|
}
|
|
765
780
|
},
|
|
@@ -781,7 +796,7 @@ function createSmtpProvider() {
|
|
|
781
796
|
reachM("reach_notInitialized")
|
|
782
797
|
);
|
|
783
798
|
}
|
|
784
|
-
logger4.debug("Sending email"
|
|
799
|
+
logger4.debug("Sending email");
|
|
785
800
|
try {
|
|
786
801
|
const sendMail = transporter.sendMail.bind(transporter);
|
|
787
802
|
const info = await sendMail({
|
|
@@ -790,10 +805,10 @@ function createSmtpProvider() {
|
|
|
790
805
|
subject: message.subject ?? "",
|
|
791
806
|
html: message.body ?? ""
|
|
792
807
|
});
|
|
793
|
-
logger4.info("Email sent"
|
|
808
|
+
logger4.info("Email sent");
|
|
794
809
|
return ok({ success: true, messageId: info.messageId });
|
|
795
810
|
} catch (error) {
|
|
796
|
-
logger4.
|
|
811
|
+
logger4.warn("Email send failed", smtpErrorDiagnostics(error));
|
|
797
812
|
return err(toReachError3());
|
|
798
813
|
}
|
|
799
814
|
}
|
|
@@ -1313,7 +1328,7 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
|
|
|
1313
1328
|
logger6.info("Message persisted for DND delivery", { provider: providerName, queueId });
|
|
1314
1329
|
return ok({ success: true, deferred: true, queueId });
|
|
1315
1330
|
}
|
|
1316
|
-
logger6.info("Message blocked by DND (discard strategy)", { provider: providerName
|
|
1331
|
+
logger6.info("Message blocked by DND (discard strategy)", { provider: providerName });
|
|
1317
1332
|
return err(
|
|
1318
1333
|
HaiReachError.DND_BLOCKED,
|
|
1319
1334
|
reachM("reach_dndBlocked")
|
|
@@ -1322,14 +1337,12 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
|
|
|
1322
1337
|
const provider = providers2.get(providerName);
|
|
1323
1338
|
logger6.debug("Sending message", {
|
|
1324
1339
|
provider: providerName,
|
|
1325
|
-
to: preprocessed.data.to,
|
|
1326
1340
|
template: message.template
|
|
1327
1341
|
});
|
|
1328
1342
|
const result = await provider.send(preprocessed.data);
|
|
1329
1343
|
if (!result.success) {
|
|
1330
1344
|
logger6.warn("Message send failed", {
|
|
1331
1345
|
provider: providerName,
|
|
1332
|
-
to: message.to,
|
|
1333
1346
|
error: result.error.code
|
|
1334
1347
|
});
|
|
1335
1348
|
return result;
|
|
@@ -1342,133 +1355,6 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
|
|
|
1342
1355
|
}
|
|
1343
1356
|
return result;
|
|
1344
1357
|
}
|
|
1345
|
-
function migrateReach(options = {}, db = reldb) {
|
|
1346
|
-
const definitions = {
|
|
1347
|
-
queue_id: db.config?.type === "mysql" ? "VARCHAR(255)" : "TEXT",
|
|
1348
|
-
retry_safe: "BOOLEAN NOT NULL DEFAULT FALSE",
|
|
1349
|
-
attempt_count: "BIGINT NOT NULL DEFAULT 0",
|
|
1350
|
-
max_attempts: "BIGINT NOT NULL DEFAULT 5",
|
|
1351
|
-
next_attempt_at: "BIGINT NOT NULL DEFAULT 0",
|
|
1352
|
-
retry_delay_ms: "BIGINT NOT NULL DEFAULT 60000",
|
|
1353
|
-
max_retry_delay_ms: "BIGINT NOT NULL DEFAULT 3600000",
|
|
1354
|
-
last_failure: "TEXT"
|
|
1355
|
-
};
|
|
1356
|
-
const steps = [
|
|
1357
|
-
{
|
|
1358
|
-
version: 1,
|
|
1359
|
-
name: "durable-delivery-ledger",
|
|
1360
|
-
async needed(sql) {
|
|
1361
|
-
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1362
|
-
return found.success ? ok(found.data.length > 0 && Object.keys(definitions).some((name) => !found.data.includes(name))) : found;
|
|
1363
|
-
},
|
|
1364
|
-
async apply(sql) {
|
|
1365
|
-
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1366
|
-
if (!found.success)
|
|
1367
|
-
return found;
|
|
1368
|
-
for (const [name, definition] of Object.entries(definitions)) {
|
|
1369
|
-
if (found.data.includes(name))
|
|
1370
|
-
continue;
|
|
1371
|
-
const added = await sql.execute(`ALTER TABLE hai_reach_send_log ADD COLUMN ${name} ${definition}`);
|
|
1372
|
-
if (!added.success)
|
|
1373
|
-
return added;
|
|
1374
|
-
}
|
|
1375
|
-
return ok(void 0);
|
|
1376
|
-
}
|
|
1377
|
-
},
|
|
1378
|
-
{
|
|
1379
|
-
version: 2,
|
|
1380
|
-
name: "legacy-delivery-isolation",
|
|
1381
|
-
async needed(sql) {
|
|
1382
|
-
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1383
|
-
if (!found.success || !found.data.length) {
|
|
1384
|
-
return found.success ? ok(false) : found;
|
|
1385
|
-
}
|
|
1386
|
-
if (!found.data.includes("queue_id"))
|
|
1387
|
-
return ok(true);
|
|
1388
|
-
const row = await sql.get("SELECT id FROM hai_reach_send_log WHERE queue_id IS NULL AND status IN ('pending','processing','unknown') LIMIT 1");
|
|
1389
|
-
return row.success ? ok(Boolean(row.data)) : row;
|
|
1390
|
-
},
|
|
1391
|
-
async apply(sql) {
|
|
1392
|
-
const isolated = await sql.execute("UPDATE hai_reach_send_log SET status='unknown',processing_owner=NULL,locked_until=NULL WHERE status='processing' AND queue_id IS NULL");
|
|
1393
|
-
if (!isolated.success)
|
|
1394
|
-
return isolated;
|
|
1395
|
-
while (true) {
|
|
1396
|
-
const rows = await sql.query("SELECT id FROM hai_reach_send_log WHERE queue_id IS NULL AND status IN ('pending','unknown') ORDER BY id LIMIT 100");
|
|
1397
|
-
if (!rows.success)
|
|
1398
|
-
return rows;
|
|
1399
|
-
for (const row of rows.data) {
|
|
1400
|
-
const saved = await sql.execute("UPDATE hai_reach_send_log SET queue_id=? WHERE id=? AND queue_id IS NULL", [
|
|
1401
|
-
randomUUID(),
|
|
1402
|
-
row.id
|
|
1403
|
-
]);
|
|
1404
|
-
if (!saved.success)
|
|
1405
|
-
return saved;
|
|
1406
|
-
}
|
|
1407
|
-
if (rows.data.length < 100)
|
|
1408
|
-
return ok(void 0);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
},
|
|
1412
|
-
{
|
|
1413
|
-
version: 3,
|
|
1414
|
-
name: "credential-payload-retirement",
|
|
1415
|
-
async needed(sql) {
|
|
1416
|
-
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1417
|
-
if (!found.success || !found.data.length) {
|
|
1418
|
-
return found.success ? ok(false) : found;
|
|
1419
|
-
}
|
|
1420
|
-
const ledger = await db.schemaColumns("hai_schema_migrations", sql);
|
|
1421
|
-
if (!ledger.success)
|
|
1422
|
-
return ledger;
|
|
1423
|
-
const version = ledger.data.length ? await sql.get("SELECT version FROM hai_schema_migrations WHERE module=?", ["reach"]) : ok(null);
|
|
1424
|
-
if (!version.success)
|
|
1425
|
-
return version;
|
|
1426
|
-
if (Number(version.data?.version ?? 0) >= 3)
|
|
1427
|
-
return ok(false);
|
|
1428
|
-
const row = await sql.get("SELECT id FROM hai_reach_send_log WHERE subject IS NOT NULL OR body IS NOT NULL OR template IS NOT NULL OR vars_json IS NOT NULL OR extra_json IS NOT NULL LIMIT 1");
|
|
1429
|
-
return row.success ? ok(Boolean(row.data)) : row;
|
|
1430
|
-
},
|
|
1431
|
-
async apply(sql) {
|
|
1432
|
-
return sql.execute("UPDATE hai_reach_send_log SET body=NULL,subject=NULL,vars_json=NULL,extra_json=NULL,template=NULL,status=CASE WHEN status IN ('pending','processing') THEN 'discarded' WHEN status='sending' THEN 'unknown' ELSE status END,processing_owner=NULL,locked_until=NULL WHERE subject IS NOT NULL OR body IS NOT NULL OR template IS NOT NULL OR vars_json IS NOT NULL OR extra_json IS NOT NULL");
|
|
1433
|
-
}
|
|
1434
|
-
},
|
|
1435
|
-
{
|
|
1436
|
-
version: 4,
|
|
1437
|
-
name: "unique-operation-receipts",
|
|
1438
|
-
async needed(sql) {
|
|
1439
|
-
const columns = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1440
|
-
if (!columns.success || !columns.data.length) {
|
|
1441
|
-
return columns.success ? ok(false) : columns;
|
|
1442
|
-
}
|
|
1443
|
-
if (!columns.data.includes("queue_id"))
|
|
1444
|
-
return ok(true);
|
|
1445
|
-
if (db.config?.type === "sqlite") {
|
|
1446
|
-
const indexes = await sql.query("PRAGMA index_list(hai_reach_send_log)");
|
|
1447
|
-
if (!indexes.success)
|
|
1448
|
-
return indexes;
|
|
1449
|
-
for (const index of indexes.data) {
|
|
1450
|
-
if (!index.unique || !/^\w+$/.test(index.name))
|
|
1451
|
-
continue;
|
|
1452
|
-
const fields = await sql.query(`PRAGMA index_info(${index.name})`);
|
|
1453
|
-
if (!fields.success)
|
|
1454
|
-
return fields;
|
|
1455
|
-
if (fields.data.length === 1 && fields.data[0]?.name === "queue_id")
|
|
1456
|
-
return ok(false);
|
|
1457
|
-
}
|
|
1458
|
-
return ok(true);
|
|
1459
|
-
}
|
|
1460
|
-
const found = db.config?.type === "mysql" ? await sql.get("SELECT index_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND non_unique=0 GROUP BY index_name HAVING COUNT(*)=1 AND MAX(column_name)=?", ["hai_reach_send_log", "queue_id"]) : await sql.get("SELECT i.indexrelid FROM pg_index i JOIN pg_class t ON t.oid=i.indrelid JOIN pg_namespace n ON n.oid=t.relnamespace JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=ANY(i.indkey) WHERE t.relname=? AND n.nspname=current_schema() AND i.indisunique AND i.indnatts=1 AND a.attname=?", ["hai_reach_send_log", "queue_id"]);
|
|
1461
|
-
return found.success ? ok(!found.data) : found;
|
|
1462
|
-
},
|
|
1463
|
-
async apply(sql) {
|
|
1464
|
-
return sql.execute("CREATE UNIQUE INDEX uq_hai_reach_queue_v1 ON hai_reach_send_log(queue_id)");
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
];
|
|
1468
|
-
return db.migrateSchema("reach", steps, options);
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// src/repositories/reach-repository-send-log.ts
|
|
1472
1358
|
var TABLE_NAME = "hai_reach_send_log";
|
|
1473
1359
|
var DEFAULT_CLAIM_BATCH_SIZE = 100;
|
|
1474
1360
|
var DEFAULT_CLAIM_LOCK_MS = 10 * 60 * 1e3;
|
|
@@ -1692,11 +1578,8 @@ function resetSendLogRepoSingleton() {
|
|
|
1692
1578
|
async function createSendLogRepository(db) {
|
|
1693
1579
|
if (sendLogRepoInstance && sendLogRepoSqlOps === db.sql)
|
|
1694
1580
|
return ok(sendLogRepoInstance);
|
|
1695
|
-
const migration = await migrateReach({}, db);
|
|
1696
|
-
if (!migration.success)
|
|
1697
|
-
return migration;
|
|
1698
1581
|
const repo = new DbSendLogRepository(db);
|
|
1699
|
-
const initResult = await repo.
|
|
1582
|
+
const initResult = await repo.ready();
|
|
1700
1583
|
if (!initResult.success) {
|
|
1701
1584
|
return err(
|
|
1702
1585
|
HaiReachError.SEND_FAILED,
|
|
@@ -1711,6 +1594,17 @@ async function createSendLogRepository(db) {
|
|
|
1711
1594
|
return ok(repo);
|
|
1712
1595
|
}
|
|
1713
1596
|
var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
1597
|
+
/** 唯一回执约束保证多个节点不能为同一操作排入不同的发送记录。 */
|
|
1598
|
+
async ready() {
|
|
1599
|
+
const schema = await this.ensureSchema();
|
|
1600
|
+
if (!schema.success)
|
|
1601
|
+
return schema;
|
|
1602
|
+
const type = this.db.config?.type;
|
|
1603
|
+
const index = type === "sqlite" ? await this.db.sql.get('SELECT i.name FROM pragma_index_list(?) i JOIN pragma_index_info(i.name) f WHERE i."unique"=1 AND i.partial=0 GROUP BY i.name HAVING COUNT(*)=1 AND MAX(f.name)=?', [TABLE_NAME, "queue_id"]) : type === "mysql" ? await this.db.sql.get("SELECT index_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND non_unique=0 GROUP BY index_name HAVING COUNT(*)=1 AND MAX(column_name)=?", [TABLE_NAME, "queue_id"]) : await this.db.sql.get("SELECT i.indexrelid FROM pg_index i JOIN pg_class t ON t.oid=i.indrelid JOIN pg_namespace n ON n.oid=t.relnamespace JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=i.indkey[0] WHERE t.relname=? AND n.nspname=current_schema() AND i.indisunique AND i.indisvalid AND i.indpred IS NULL AND i.indnkeyatts=1 AND a.attname=?", [TABLE_NAME, "queue_id"]);
|
|
1604
|
+
if (!index.success)
|
|
1605
|
+
return index;
|
|
1606
|
+
return index.data ? ok(void 0) : err(HaiReachError.SEND_FAILED, reachM("reach_sendLogIndexRequired"));
|
|
1607
|
+
}
|
|
1714
1608
|
/** 共享后端取真正执行时钟;本地 SQLite 保留可控的单进程测试时钟。 */
|
|
1715
1609
|
clockSql(localNow) {
|
|
1716
1610
|
if (this.db.config?.type === "postgresql")
|
|
@@ -2587,6 +2481,6 @@ var reach = {
|
|
|
2587
2481
|
}
|
|
2588
2482
|
};
|
|
2589
2483
|
|
|
2590
|
-
export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema,
|
|
2484
|
+
export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, reach };
|
|
2591
2485
|
//# sourceMappingURL=index.js.map
|
|
2592
2486
|
//# sourceMappingURL=index.js.map
|