@sfmc-bds/db-server 0.2.0-beta.5 → 0.2.0-beta.6

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 (143) hide show
  1. package/LICENSE +663 -0
  2. package/dist/domain/bridge.d.ts +57 -23
  3. package/dist/domain/bridge.d.ts.map +1 -1
  4. package/dist/domain/bridge.js +77 -6
  5. package/dist/domain/bridge.js.map +2 -2
  6. package/dist/domain/bridge.test.d.ts +2 -0
  7. package/dist/domain/bridge.test.d.ts.map +1 -0
  8. package/dist/domain/bridge.test.js +87 -0
  9. package/dist/domain/bridge.test.js.map +7 -0
  10. package/dist/domain/economy.d.ts +38 -7
  11. package/dist/domain/economy.d.ts.map +1 -1
  12. package/dist/domain/economy.js.map +2 -2
  13. package/dist/domain/messages-bridge-filter.test.d.ts +5 -0
  14. package/dist/domain/messages-bridge-filter.test.d.ts.map +1 -0
  15. package/dist/domain/messages-bridge-filter.test.js +20 -0
  16. package/dist/domain/messages-bridge-filter.test.js.map +7 -0
  17. package/dist/domain/qq-bind-status.test.d.ts +2 -0
  18. package/dist/domain/qq-bind-status.test.d.ts.map +1 -0
  19. package/dist/domain/qq-bind-status.test.js +211 -0
  20. package/dist/domain/qq-bind-status.test.js.map +7 -0
  21. package/dist/domain/qq-events.d.ts +84 -0
  22. package/dist/domain/qq-events.d.ts.map +1 -0
  23. package/dist/domain/qq-events.js +193 -0
  24. package/dist/domain/qq-events.js.map +7 -0
  25. package/dist/domain/qq-events.test.d.ts +5 -0
  26. package/dist/domain/qq-events.test.d.ts.map +1 -0
  27. package/dist/domain/qq-events.test.js +170 -0
  28. package/dist/domain/qq-events.test.js.map +7 -0
  29. package/dist/domain/qq-join.test.d.ts +2 -0
  30. package/dist/domain/qq-join.test.d.ts.map +1 -0
  31. package/dist/domain/qq-join.test.js +353 -0
  32. package/dist/domain/qq-join.test.js.map +7 -0
  33. package/dist/domain/schema.d.ts +17 -19
  34. package/dist/domain/schema.d.ts.map +1 -1
  35. package/dist/domain/schema.js +69 -0
  36. package/dist/domain/schema.js.map +2 -2
  37. package/dist/domain/system-status.d.ts +75 -0
  38. package/dist/domain/system-status.d.ts.map +1 -0
  39. package/dist/domain/system-status.js +181 -0
  40. package/dist/domain/system-status.js.map +7 -0
  41. package/dist/domain/system-status.test.d.ts +5 -0
  42. package/dist/domain/system-status.test.d.ts.map +1 -0
  43. package/dist/domain/system-status.test.js +19 -0
  44. package/dist/domain/system-status.test.js.map +7 -0
  45. package/dist/domain/transaction.d.ts +5 -4
  46. package/dist/domain/transaction.d.ts.map +1 -1
  47. package/dist/domain/transaction.js.map +2 -2
  48. package/dist/env.d.ts +7 -0
  49. package/dist/env.d.ts.map +1 -1
  50. package/dist/env.js +55 -3
  51. package/dist/env.js.map +2 -2
  52. package/dist/index.d.ts +0 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +105 -19
  55. package/dist/index.js.map +2 -2
  56. package/dist/lib/idempotency-store.d.ts +17 -11
  57. package/dist/lib/idempotency-store.d.ts.map +1 -1
  58. package/dist/lib/idempotency-store.js.map +2 -2
  59. package/dist/lib/idempotency.d.ts +3 -4
  60. package/dist/lib/idempotency.d.ts.map +1 -1
  61. package/dist/lib/idempotency.js.map +2 -2
  62. package/dist/lib/module-state.d.ts +38 -8
  63. package/dist/lib/module-state.d.ts.map +1 -1
  64. package/dist/lib/module-state.js.map +2 -2
  65. package/dist/lib/order-by.d.ts +8 -6
  66. package/dist/lib/order-by.d.ts.map +1 -1
  67. package/dist/lib/order-by.js.map +2 -2
  68. package/dist/lib/sql-helpers.d.ts +25 -13
  69. package/dist/lib/sql-helpers.d.ts.map +1 -1
  70. package/dist/lib/sql-helpers.js.map +2 -2
  71. package/dist/lib/sqlite.d.ts +14 -5
  72. package/dist/lib/sqlite.d.ts.map +1 -1
  73. package/dist/lib/sqlite.js.map +2 -2
  74. package/dist/manifest-loader.d.ts +5 -2
  75. package/dist/manifest-loader.d.ts.map +1 -1
  76. package/dist/manifest-loader.js +16 -6
  77. package/dist/manifest-loader.js.map +2 -2
  78. package/dist/manifest.d.ts +1 -2
  79. package/dist/manifest.d.ts.map +1 -1
  80. package/dist/manifest.js +13 -8
  81. package/dist/manifest.js.map +2 -2
  82. package/dist/project-root.d.ts +1 -1
  83. package/dist/project-root.d.ts.map +1 -1
  84. package/dist/project-root.js +2 -2
  85. package/dist/project-root.js.map +2 -2
  86. package/dist/routes/_shared.d.ts +14 -5
  87. package/dist/routes/_shared.d.ts.map +1 -1
  88. package/dist/routes/_shared.js.map +2 -2
  89. package/dist/routes/config.d.ts +8 -11
  90. package/dist/routes/config.d.ts.map +1 -1
  91. package/dist/routes/config.js +17 -23
  92. package/dist/routes/config.js.map +2 -2
  93. package/dist/routes/db-routes.d.ts +6 -0
  94. package/dist/routes/db-routes.d.ts.map +1 -1
  95. package/dist/routes/db-routes.js.map +2 -2
  96. package/dist/routes/messages.d.ts +6 -2
  97. package/dist/routes/messages.d.ts.map +1 -1
  98. package/dist/routes/messages.js +17 -3
  99. package/dist/routes/messages.js.map +2 -2
  100. package/dist/routes/module-config-routes.d.ts +13 -9
  101. package/dist/routes/module-config-routes.d.ts.map +1 -1
  102. package/dist/routes/module-config-routes.js.map +2 -2
  103. package/dist/routes/modules.d.ts +6 -0
  104. package/dist/routes/modules.d.ts.map +1 -1
  105. package/dist/routes/modules.js.map +2 -2
  106. package/dist/routes/qq-bind.d.ts +24 -0
  107. package/dist/routes/qq-bind.d.ts.map +1 -0
  108. package/dist/routes/qq-bind.js +179 -0
  109. package/dist/routes/qq-bind.js.map +7 -0
  110. package/dist/routes/qq-events.d.ts +20 -0
  111. package/dist/routes/qq-events.d.ts.map +1 -0
  112. package/dist/routes/qq-events.js +39 -0
  113. package/dist/routes/qq-events.js.map +7 -0
  114. package/dist/routes/qq-join.d.ts +46 -0
  115. package/dist/routes/qq-join.d.ts.map +1 -0
  116. package/dist/routes/qq-join.js +277 -0
  117. package/dist/routes/qq-join.js.map +7 -0
  118. package/dist/routes/service-routes.d.ts +6 -0
  119. package/dist/routes/service-routes.d.ts.map +1 -1
  120. package/dist/routes/service-routes.js.map +2 -2
  121. package/dist/routes/status.d.ts +19 -0
  122. package/dist/routes/status.d.ts.map +1 -0
  123. package/dist/routes/status.js +74 -0
  124. package/dist/routes/status.js.map +7 -0
  125. package/dist/schema-registry.d.ts +16 -15
  126. package/dist/schema-registry.d.ts.map +1 -1
  127. package/dist/schema-registry.js +7 -1
  128. package/dist/schema-registry.js.map +2 -2
  129. package/dist/server.d.ts +19 -0
  130. package/dist/server.d.ts.map +1 -1
  131. package/dist/server.js.map +2 -2
  132. package/dist/service-registry.d.ts +39 -9
  133. package/dist/service-registry.d.ts.map +1 -1
  134. package/dist/service-registry.js +30 -2
  135. package/dist/service-registry.js.map +2 -2
  136. package/dist/services/builtin-handlers.d.ts +27 -7
  137. package/dist/services/builtin-handlers.d.ts.map +1 -1
  138. package/dist/services/builtin-handlers.js.map +2 -2
  139. package/dist/tx-runner.d.ts +29 -3
  140. package/dist/tx-runner.d.ts.map +1 -1
  141. package/dist/tx-runner.js +29 -3
  142. package/dist/tx-runner.js.map +2 -2
  143. package/package.json +17 -15
@@ -0,0 +1,193 @@
1
+ import {
2
+ DEFAULT_QQ_EVENTS
3
+ } from "@sfmc-bds/sdk/node/config";
4
+ import { sendGroupOutbound } from "./bridge.js";
5
+ const WINDOW_TYPES = /* @__PURE__ */ new Set(["join", "leave", "death"]);
6
+ const IMMEDIATE_TYPES = /* @__PURE__ */ new Set(["crash", "start"]);
7
+ const MAX_WINDOW_EVENTS = 20;
8
+ const CAUSE_ZH = {
9
+ anvil: "\u94C1\u7827",
10
+ blockExplosion: "\u65B9\u5757\u7206\u70B8",
11
+ campfire: "\u8425\u706B",
12
+ drowning: "\u6EBA\u6C34",
13
+ entityAttack: "\u751F\u7269\u653B\u51FB",
14
+ entityExplosion: "\u5B9E\u4F53\u7206\u70B8",
15
+ fall: "\u5760\u843D",
16
+ fallingBlock: "\u843D\u77F3",
17
+ fire: "\u706B\u7130",
18
+ fireTick: "\u707C\u70E7",
19
+ flyIntoWall: "\u649E\u5899",
20
+ freezing: "\u51BB\u7ED3",
21
+ lava: "\u5CA9\u6D46",
22
+ lightning: "\u96F7\u51FB",
23
+ magic: "\u9B54\u6CD5",
24
+ magma: "\u5CA9\u6D46\u5757",
25
+ none: "\u672A\u77E5",
26
+ override: "\u5F3A\u5236",
27
+ piston: "\u6D3B\u585E",
28
+ projectile: "\u5F39\u5C04\u7269",
29
+ stalactite: "\u949F\u4E73\u77F3",
30
+ stalagmite: "\u77F3\u7B0B",
31
+ starve: "\u9965\u997F",
32
+ suffocation: "\u7A92\u606F",
33
+ suicide: "\u81EA\u6740",
34
+ thorns: "\u8346\u68D8",
35
+ void: "\u865A\u7A7A",
36
+ wither: "\u51CB\u96F6",
37
+ // 常见生物名(若上层将实体名填充至 cause 字段)
38
+ zombie: "\u50F5\u5C38",
39
+ skeleton: "\u9AB7\u9AC5",
40
+ creeper: "\u82E6\u529B\u6015",
41
+ player: "\u73A9\u5BB6"
42
+ };
43
+ function resolveQqEventsConfig(raw) {
44
+ const o = raw && typeof raw === "object" ? raw : {};
45
+ const windowSec = Number(o.window_sec ?? DEFAULT_QQ_EVENTS.window_sec);
46
+ return {
47
+ enabled: o.enabled === void 0 ? DEFAULT_QQ_EVENTS.enabled : o.enabled === true,
48
+ window_sec: Number.isFinite(windowSec) && windowSec >= 5 ? Math.min(600, Math.floor(windowSec)) : DEFAULT_QQ_EVENTS.window_sec,
49
+ join: o.join === void 0 ? DEFAULT_QQ_EVENTS.join : o.join === true,
50
+ leave: o.leave === void 0 ? DEFAULT_QQ_EVENTS.leave : o.leave === true,
51
+ death: o.death === void 0 ? DEFAULT_QQ_EVENTS.death : o.death === true,
52
+ crash: o.crash === void 0 ? DEFAULT_QQ_EVENTS.crash : o.crash === true,
53
+ start: o.start === void 0 ? DEFAULT_QQ_EVENTS.start : o.start === true
54
+ };
55
+ }
56
+ function localizeCause(cause) {
57
+ if (!cause) return "";
58
+ const key = cause.trim();
59
+ if (!key) return "";
60
+ return CAUSE_ZH[key] ?? CAUSE_ZH[key.toLowerCase()] ?? key;
61
+ }
62
+ function isEventTypeEnabled(cfg, type) {
63
+ if (!cfg.enabled) return false;
64
+ return cfg[type] === true;
65
+ }
66
+ function formatWindowBody(events) {
67
+ const joins = [];
68
+ const leaves = [];
69
+ const deaths = [];
70
+ for (const e of events) {
71
+ if (e.type === "join") joins.push(e.player);
72
+ else if (e.type === "leave") leaves.push(e.player);
73
+ else {
74
+ const c = localizeCause(e.cause);
75
+ deaths.push(c ? `${e.player}\uFF08${c}\uFF09` : e.player);
76
+ }
77
+ }
78
+ const lines = ["[MC\u4E8B\u4EF6]"];
79
+ if (joins.length) lines.push(`\u4E0A\u7EBF\uFF1A${joins.join("\u3001")}`);
80
+ if (leaves.length) lines.push(`\u4E0B\u7EBF\uFF1A${leaves.join("\u3001")}`);
81
+ if (deaths.length) lines.push(`\u6B7B\u4EA1\uFF1A${deaths.join("\u3001")}`);
82
+ return lines.join("\n");
83
+ }
84
+ function formatImmediateBody(ev) {
85
+ if (ev.type === "crash") {
86
+ const d = String(ev.detail ?? "").trim();
87
+ return d ? `[MC\u4E8B\u4EF6] BDS \u610F\u5916\u9000\u51FA (${d})` : "[MC\u4E8B\u4EF6] BDS \u610F\u5916\u9000\u51FA";
88
+ }
89
+ if (ev.type === "start") {
90
+ const d = String(ev.detail ?? "").trim();
91
+ return d ? `[MC\u4E8B\u4EF6] BDS \u5DF2\u542F\u52A8 (${d})` : "[MC\u4E8B\u4EF6] BDS \u5DF2\u542F\u52A8";
92
+ }
93
+ return `[MC\u4E8B\u4EF6] ${ev.type}`;
94
+ }
95
+ function normalizeEventPayload(raw) {
96
+ if (!raw || typeof raw !== "object") return null;
97
+ const o = raw;
98
+ const type = String(o.type ?? "").trim();
99
+ if (!WINDOW_TYPES.has(type) && !IMMEDIATE_TYPES.has(type)) return null;
100
+ const out = { type };
101
+ if (o.player !== void 0) {
102
+ const player = String(o.player).trim();
103
+ if (player) out.player = player;
104
+ }
105
+ if (o.cause !== void 0) {
106
+ const cause = String(o.cause).trim();
107
+ if (cause) out.cause = cause;
108
+ }
109
+ if (o.detail !== void 0) {
110
+ const detail = String(o.detail).trim();
111
+ if (detail) out.detail = detail;
112
+ }
113
+ return out;
114
+ }
115
+ function createQqEventsAggregator(deps) {
116
+ const setTimeoutFn = deps.setTimeoutFn ?? setTimeout;
117
+ const clearTimeoutFn = deps.clearTimeoutFn ?? clearTimeout;
118
+ const send = deps.send ?? ((text) => {
119
+ sendGroupOutbound(deps.getOutbound(), text);
120
+ });
121
+ let buffer = [];
122
+ let timer = null;
123
+ function clearTimer() {
124
+ if (timer != null) {
125
+ clearTimeoutFn(timer);
126
+ timer = null;
127
+ }
128
+ }
129
+ function flushWindow() {
130
+ clearTimer();
131
+ if (buffer.length === 0) return;
132
+ const snapshot = buffer;
133
+ buffer = [];
134
+ const text = formatWindowBody(snapshot);
135
+ if (text.split("\n").length > 1) send(text);
136
+ }
137
+ function scheduleFlush(windowSec) {
138
+ if (timer != null) return;
139
+ timer = setTimeoutFn(() => {
140
+ timer = null;
141
+ flushWindow();
142
+ }, windowSec * 1e3);
143
+ }
144
+ function ingestOne(ev) {
145
+ const cfg = deps.getConfig();
146
+ if (!isEventTypeEnabled(cfg, ev.type)) {
147
+ return { accepted: false, reason: "disabled" };
148
+ }
149
+ if (IMMEDIATE_TYPES.has(ev.type)) {
150
+ flushWindow();
151
+ send(formatImmediateBody(ev));
152
+ return { accepted: true };
153
+ }
154
+ const player = String(ev.player ?? "").trim();
155
+ if (!player) return { accepted: false, reason: "missing_player" };
156
+ buffer.push({
157
+ type: ev.type,
158
+ player,
159
+ ...ev.cause ? { cause: ev.cause } : {}
160
+ });
161
+ scheduleFlush(cfg.window_sec);
162
+ if (buffer.length >= MAX_WINDOW_EVENTS) flushWindow();
163
+ return { accepted: true };
164
+ }
165
+ function ingestMany(items) {
166
+ let accepted = 0;
167
+ let rejected = 0;
168
+ for (const ev of items) {
169
+ const r = ingestOne(ev);
170
+ if (r.accepted) accepted += 1;
171
+ else rejected += 1;
172
+ }
173
+ return { accepted, rejected };
174
+ }
175
+ function dispose() {
176
+ flushWindow();
177
+ }
178
+ function pendingCount() {
179
+ return buffer.length;
180
+ }
181
+ return { ingestOne, ingestMany, flushWindow, dispose, pendingCount };
182
+ }
183
+ export {
184
+ MAX_WINDOW_EVENTS,
185
+ createQqEventsAggregator,
186
+ formatImmediateBody,
187
+ formatWindowBody,
188
+ isEventTypeEnabled,
189
+ localizeCause,
190
+ normalizeEventPayload,
191
+ resolveQqEventsConfig
192
+ };
193
+ //# sourceMappingURL=qq-events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/qq-events.ts"],
4
+ "sourcesContent": ["/**\n * domain/qq-events.ts \u2014 Minecraft \u670D\u52A1\u5668\u4E8B\u4EF6\u7FA4\u63A8\u9001\u805A\u5408\u8282\u6D41\u5668\n *\n * \u805A\u5408\u7B56\u7565\uFF1A\n * - \u73A9\u5BB6\u8FDB\u9000\u4E0E\u6B7B\u4EA1\uFF08join / leave / death\uFF09\uFF1A\u5728\u65F6\u95F4\u7A97\u53E3\u5185\u6536\u96C6\u7F13\u51B2\uFF0C\u5408\u5E76\u4E3A\u5355\u6761\u6D88\u606F\u53D1\u9001\uFF0C\u907F\u514D\u5237\u5C4F\n * - \u5173\u952E\u8FD0\u884C\u6001\u4E8B\u4EF6\uFF08crash / start\uFF09\uFF1A\u5148\u7ACB\u5373\u51B2\u5237\uFF08flush\uFF09\u5F53\u524D\u7A97\u53E3\u5185\u7684\u5F85\u53D1\u9001\u4E8B\u4EF6\uFF0C\u968F\u540E\u7ACB\u5373\u5355\u72EC\u63A8\u9001\n */\n\nimport {\n DEFAULT_QQ_EVENTS,\n type QqEventsConfig,\n} from \"@sfmc-bds/sdk/node/config\";\n\nimport type { OutboundConfig } from \"./bridge.js\";\nimport { sendGroupOutbound } from \"./bridge.js\";\n\nexport type QqEventType = \"join\" | \"leave\" | \"death\" | \"crash\" | \"start\";\n\nexport type QqEventPayload = {\n type: QqEventType;\n player?: string;\n cause?: string;\n detail?: string;\n};\n\nexport type ResolvedQqEventsConfig = Required<QqEventsConfig>;\n\nconst WINDOW_TYPES = new Set<QqEventType>([\"join\", \"leave\", \"death\"]);\nconst IMMEDIATE_TYPES = new Set<QqEventType>([\"crash\", \"start\"]);\n\n/** \u7A97\u53E3\u5185\u6761\u6570\u4E0A\u9650\uFF1A\u8FBE\u5230\u4E0A\u9650\u65F6\u63D0\u524D\u89E6\u53D1 flush\uFF0C\u907F\u514D\u5355\u6B21\u79EF\u538B\u8FC7\u591A\u5185\u5BB9\u3002 */\nexport const MAX_WINDOW_EVENTS = 20;\n\n/** SAPI damageSource.cause \u2192 \u4E2D\u6587\u6620\u5C04\u5B57\u5178\uFF08\u672A\u6536\u5F55\u65F6\u4FDD\u7559\u539F\u82F1\u6587\u6807\u8BC6\uFF09\u3002 */\nconst CAUSE_ZH: Record<string, string> = {\n anvil: \"\u94C1\u7827\",\n blockExplosion: \"\u65B9\u5757\u7206\u70B8\",\n campfire: \"\u8425\u706B\",\n drowning: \"\u6EBA\u6C34\",\n entityAttack: \"\u751F\u7269\u653B\u51FB\",\n entityExplosion: \"\u5B9E\u4F53\u7206\u70B8\",\n fall: \"\u5760\u843D\",\n fallingBlock: \"\u843D\u77F3\",\n fire: \"\u706B\u7130\",\n fireTick: \"\u707C\u70E7\",\n flyIntoWall: \"\u649E\u5899\",\n freezing: \"\u51BB\u7ED3\",\n lava: \"\u5CA9\u6D46\",\n lightning: \"\u96F7\u51FB\",\n magic: \"\u9B54\u6CD5\",\n magma: \"\u5CA9\u6D46\u5757\",\n none: \"\u672A\u77E5\",\n override: \"\u5F3A\u5236\",\n piston: \"\u6D3B\u585E\",\n projectile: \"\u5F39\u5C04\u7269\",\n stalactite: \"\u949F\u4E73\u77F3\",\n stalagmite: \"\u77F3\u7B0B\",\n starve: \"\u9965\u997F\",\n suffocation: \"\u7A92\u606F\",\n suicide: \"\u81EA\u6740\",\n thorns: \"\u8346\u68D8\",\n void: \"\u865A\u7A7A\",\n wither: \"\u51CB\u96F6\",\n // \u5E38\u89C1\u751F\u7269\u540D\uFF08\u82E5\u4E0A\u5C42\u5C06\u5B9E\u4F53\u540D\u586B\u5145\u81F3 cause \u5B57\u6BB5\uFF09\n zombie: \"\u50F5\u5C38\",\n skeleton: \"\u9AB7\u9AC5\",\n creeper: \"\u82E6\u529B\u6015\",\n player: \"\u73A9\u5BB6\",\n};\n\n/**\n * \u89E3\u6790\u5E76\u586B\u5145 QQ \u4E8B\u4EF6\u63A8\u9001\u914D\u7F6E\u7684\u9ED8\u8BA4\u503C\u3002\n *\n * @param raw \u539F\u59CB\u914D\u7F6E\u5BF9\u8C61\u3002\n * @returns \u89C4\u8303\u5316\u7684\u5B8C\u6574\u914D\u7F6E\u5BF9\u8C61\u3002\n */\nexport function resolveQqEventsConfig(raw: unknown): ResolvedQqEventsConfig {\n const o = raw && typeof raw === \"object\" ? (raw as Record<string, unknown>) : {};\n const windowSec = Number(o.window_sec ?? DEFAULT_QQ_EVENTS.window_sec);\n return {\n enabled: o.enabled === undefined ? DEFAULT_QQ_EVENTS.enabled : o.enabled === true,\n window_sec:\n Number.isFinite(windowSec) && windowSec >= 5\n ? Math.min(600, Math.floor(windowSec))\n : DEFAULT_QQ_EVENTS.window_sec,\n join: o.join === undefined ? DEFAULT_QQ_EVENTS.join : o.join === true,\n leave: o.leave === undefined ? DEFAULT_QQ_EVENTS.leave : o.leave === true,\n death: o.death === undefined ? DEFAULT_QQ_EVENTS.death : o.death === true,\n crash: o.crash === undefined ? DEFAULT_QQ_EVENTS.crash : o.crash === true,\n start: o.start === undefined ? DEFAULT_QQ_EVENTS.start : o.start === true,\n };\n}\n\n/**\n * \u5C06 SAPI \u539F\u751F\u4F24\u5BB3\u6765\u6E90\u4EE3\u7801\uFF08damageSource.cause\uFF09\u8F6C\u6362\u4E3A\u6613\u8BFB\u7684\u4E2D\u6587\u6587\u672C\u3002\n *\n * @param cause \u4F24\u5BB3\u6765\u6E90\u6807\u8BC6\u3002\n * @returns \u4E2D\u6587\u4F24\u5BB3\u63CF\u8FF0\uFF08\u672A\u6536\u5F55\u65F6\u4FDD\u7559\u539F\u82F1\u6587\u4EE3\u7801\uFF09\u3002\n */\nexport function localizeCause(cause: string | undefined): string {\n if (!cause) return \"\";\n const key = cause.trim();\n if (!key) return \"\";\n return CAUSE_ZH[key] ?? CAUSE_ZH[key.toLowerCase()] ?? key;\n}\n\nexport function isEventTypeEnabled(cfg: ResolvedQqEventsConfig, type: QqEventType): boolean {\n if (!cfg.enabled) return false;\n return cfg[type] === true;\n}\n\ntype Buffered = {\n type: \"join\" | \"leave\" | \"death\";\n player: string;\n cause?: string;\n};\n\nexport type QqEventsAggregatorDeps = {\n getConfig: () => ResolvedQqEventsConfig;\n getOutbound: () => OutboundConfig;\n /** \u53EF\u9009\u6CE8\u5165\uFF1A\u4F9B\u5355\u5143\u6D4B\u8BD5\u6A21\u62DF\u65F6\u949F\u4E0E\u5B9A\u65F6\u5668\u3002 */\n setTimeoutFn?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;\n clearTimeoutFn?: (id: ReturnType<typeof setTimeout>) => void;\n send?: (text: string) => void;\n};\n\n/**\n * \u683C\u5F0F\u5316\u65F6\u95F4\u7A97\u53E3\u5185\u7684\u805A\u5408\u4E8B\u4EF6\u6587\u672C\u3002\n *\n * @param events \u7F13\u51B2\u533A\u5185\u7684\u4E8B\u4EF6\u5217\u8868\u3002\n * @returns \u683C\u5F0F\u5316\u540E\u7684\u591A\u884C\u6D88\u606F\u5B57\u7B26\u4E32\u3002\n */\nexport function formatWindowBody(events: Buffered[]): string {\n const joins: string[] = [];\n const leaves: string[] = [];\n const deaths: string[] = [];\n for (const e of events) {\n if (e.type === \"join\") joins.push(e.player);\n else if (e.type === \"leave\") leaves.push(e.player);\n else {\n const c = localizeCause(e.cause);\n deaths.push(c ? `${e.player}\uFF08${c}\uFF09` : e.player);\n }\n }\n const lines: string[] = [\"[MC\u4E8B\u4EF6]\"];\n if (joins.length) lines.push(`\u4E0A\u7EBF\uFF1A${joins.join(\"\u3001\")}`);\n if (leaves.length) lines.push(`\u4E0B\u7EBF\uFF1A${leaves.join(\"\u3001\")}`);\n if (deaths.length) lines.push(`\u6B7B\u4EA1\uFF1A${deaths.join(\"\u3001\")}`);\n return lines.join(\"\\n\");\n}\n\n/**\n * \u683C\u5F0F\u5316\u9AD8\u4F18\u5148\u7EA7\u7684\u975E\u805A\u5408\u5373\u65F6\u4E8B\u4EF6\u6587\u672C\uFF08\u5982 BDS \u5D29\u6E83\u6216\u542F\u52A8\u5C31\u7EEA\uFF09\u3002\n *\n * @param ev \u5373\u65F6\u4E8B\u4EF6\u8F7D\u8377\u3002\n * @returns \u683C\u5F0F\u5316\u540E\u7684\u4E8B\u4EF6\u901A\u77E5\u6587\u672C\u3002\n */\nexport function formatImmediateBody(ev: QqEventPayload): string {\n if (ev.type === \"crash\") {\n const d = String(ev.detail ?? \"\").trim();\n return d ? `[MC\u4E8B\u4EF6] BDS \u610F\u5916\u9000\u51FA (${d})` : \"[MC\u4E8B\u4EF6] BDS \u610F\u5916\u9000\u51FA\";\n }\n if (ev.type === \"start\") {\n const d = String(ev.detail ?? \"\").trim();\n return d ? `[MC\u4E8B\u4EF6] BDS \u5DF2\u542F\u52A8 (${d})` : \"[MC\u4E8B\u4EF6] BDS \u5DF2\u542F\u52A8\";\n }\n return `[MC\u4E8B\u4EF6] ${ev.type}`;\n}\n\nexport function normalizeEventPayload(raw: unknown): QqEventPayload | null {\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n const type = String(o.type ?? \"\").trim() as QqEventType;\n if (!WINDOW_TYPES.has(type) && !IMMEDIATE_TYPES.has(type)) return null;\n const out: QqEventPayload = { type };\n if (o.player !== undefined) {\n const player = String(o.player).trim();\n if (player) out.player = player;\n }\n if (o.cause !== undefined) {\n const cause = String(o.cause).trim();\n if (cause) out.cause = cause;\n }\n if (o.detail !== undefined) {\n const detail = String(o.detail).trim();\n if (detail) out.detail = detail;\n }\n return out;\n}\n\n/**\n * \u521B\u5EFA QQ \u670D\u52A1\u5668\u4E8B\u4EF6\u805A\u5408\u8282\u6D41\u5668\u5B9E\u4F8B\u3002\n *\n * @param deps \u4F9D\u8D56\u6CE8\u5165\u5BF9\u8C61\uFF08\u5305\u542B\u914D\u7F6E\u83B7\u53D6\u3001\u51FA\u7AD9\u53D1\u9001\u53CA\u5B9A\u65F6\u5668\u5B9E\u73B0\uFF09\u3002\n * @returns \u5305\u542B push, flush, cancel, getPendingCount \u7B49\u65B9\u6CD5\u7684\u805A\u5408\u5668\u63A7\u5236\u5668\u3002\n */\nexport function createQqEventsAggregator(deps: QqEventsAggregatorDeps) {\n\n const setTimeoutFn = deps.setTimeoutFn ?? setTimeout;\n const clearTimeoutFn = deps.clearTimeoutFn ?? clearTimeout;\n const send =\n deps.send ??\n ((text: string) => {\n sendGroupOutbound(deps.getOutbound(), text);\n });\n\n let buffer: Buffered[] = [];\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function clearTimer(): void {\n if (timer != null) {\n clearTimeoutFn(timer);\n timer = null;\n }\n }\n\n function flushWindow(): void {\n clearTimer();\n if (buffer.length === 0) return;\n const snapshot = buffer;\n buffer = [];\n const text = formatWindowBody(snapshot);\n if (text.split(\"\\n\").length > 1) send(text);\n }\n\n function scheduleFlush(windowSec: number): void {\n if (timer != null) return;\n timer = setTimeoutFn(() => {\n timer = null;\n flushWindow();\n }, windowSec * 1000);\n }\n\n function ingestOne(ev: QqEventPayload): { accepted: boolean; reason?: string } {\n const cfg = deps.getConfig();\n if (!isEventTypeEnabled(cfg, ev.type)) {\n return { accepted: false, reason: \"disabled\" };\n }\n\n if (IMMEDIATE_TYPES.has(ev.type)) {\n flushWindow();\n send(formatImmediateBody(ev));\n return { accepted: true };\n }\n\n const player = String(ev.player ?? \"\").trim();\n if (!player) return { accepted: false, reason: \"missing_player\" };\n\n buffer.push({\n type: ev.type as \"join\" | \"leave\" | \"death\",\n player,\n ...(ev.cause ? { cause: ev.cause } : {}),\n });\n scheduleFlush(cfg.window_sec);\n if (buffer.length >= MAX_WINDOW_EVENTS) flushWindow();\n return { accepted: true };\n }\n\n function ingestMany(items: QqEventPayload[]): {\n accepted: number;\n rejected: number;\n } {\n let accepted = 0;\n let rejected = 0;\n for (const ev of items) {\n const r = ingestOne(ev);\n if (r.accepted) accepted += 1;\n else rejected += 1;\n }\n return { accepted, rejected };\n }\n\n /** \u6D4B\u8BD5 / \u5173\u505C\uFF1A\u51B2\u5237\u5E76\u6E05\u5B9A\u65F6\u5668 */\n function dispose(): void {\n flushWindow();\n }\n\n /** \u4EC5\u6D4B\u7528\uFF1A\u5F53\u524D\u7F13\u51B2\u957F\u5EA6 */\n function pendingCount(): number {\n return buffer.length;\n }\n\n return { ingestOne, ingestMany, flushWindow, dispose, pendingCount };\n}\n\nexport type QqEventsAggregator = ReturnType<typeof createQqEventsAggregator>;\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * qq-events 聚合器单测
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=qq-events.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-events.test.d.ts","sourceRoot":"","sources":["../../src/domain/qq-events.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createQqEventsAggregator,
5
+ formatImmediateBody,
6
+ formatWindowBody,
7
+ localizeCause,
8
+ MAX_WINDOW_EVENTS,
9
+ normalizeEventPayload,
10
+ resolveQqEventsConfig
11
+ } from "./qq-events.js";
12
+ const ALL_ON = {
13
+ enabled: true,
14
+ window_sec: 60,
15
+ join: true,
16
+ leave: true,
17
+ death: true,
18
+ crash: true,
19
+ start: true
20
+ };
21
+ const fakeOutbound = {
22
+ backend: "official",
23
+ creds: { appId: "a", appSecret: "s" },
24
+ groupOpenid: "G",
25
+ prefix: "[MC]"
26
+ };
27
+ test("resolveQqEventsConfig \u7F3A\u7701\u4E0E\u603B\u5F00\u5173", () => {
28
+ const d = resolveQqEventsConfig(void 0);
29
+ assert.equal(d.enabled, true);
30
+ assert.equal(d.window_sec, 60);
31
+ assert.equal(d.join, true);
32
+ const off = resolveQqEventsConfig({ enabled: false, join: false });
33
+ assert.equal(off.enabled, false);
34
+ assert.equal(off.join, false);
35
+ assert.equal(off.death, true);
36
+ });
37
+ test("localizeCause \u6620\u5C04\u4E0E\u539F\u6587\u56DE\u9000", () => {
38
+ assert.equal(localizeCause("fall"), "\u5760\u843D");
39
+ assert.equal(localizeCause("drowning"), "\u6EBA\u6C34");
40
+ assert.equal(localizeCause("custom_xyz"), "custom_xyz");
41
+ assert.equal(localizeCause(""), "");
42
+ });
43
+ test("normalizeEventPayload \u8FC7\u6EE4\u975E\u6CD5 type", () => {
44
+ assert.equal(normalizeEventPayload({ type: "chat" }), null);
45
+ assert.deepEqual(normalizeEventPayload({ type: "join", player: "Steve" }), {
46
+ type: "join",
47
+ player: "Steve"
48
+ });
49
+ });
50
+ test("formatWindowBody \u805A\u5408\u6587\u6848", () => {
51
+ const text = formatWindowBody([
52
+ { type: "join", player: "Steve" },
53
+ { type: "join", player: "Alex" },
54
+ { type: "leave", player: "Bob" },
55
+ { type: "death", player: "Steve", cause: "fall" }
56
+ ]);
57
+ assert.match(text, /^\[MC事件\]/);
58
+ assert.match(text, /上线:Steve、Alex/);
59
+ assert.match(text, /下线:Bob/);
60
+ assert.match(text, /死亡:Steve(坠落)/);
61
+ });
62
+ test("formatImmediateBody crash/start", () => {
63
+ assert.equal(formatImmediateBody({ type: "crash", detail: "code=1" }), "[MC\u4E8B\u4EF6] BDS \u610F\u5916\u9000\u51FA (code=1)");
64
+ assert.equal(formatImmediateBody({ type: "start", detail: "pid=9" }), "[MC\u4E8B\u4EF6] BDS \u5DF2\u542F\u52A8 (pid=9)");
65
+ });
66
+ test("\u7A97\u53E3\u5185 join+death \u5230\u671F flush \u6210\u4E00\u6761", () => {
67
+ const sent = [];
68
+ const timers = [];
69
+ const agg = createQqEventsAggregator({
70
+ getConfig: () => ({ ...ALL_ON, window_sec: 60 }),
71
+ getOutbound: () => fakeOutbound,
72
+ send: (t) => sent.push(t),
73
+ setTimeoutFn: (fn, ms) => {
74
+ timers.push({ fn, ms });
75
+ return timers.length;
76
+ },
77
+ clearTimeoutFn: () => {
78
+ }
79
+ });
80
+ agg.ingestOne({ type: "join", player: "A" });
81
+ agg.ingestOne({ type: "join", player: "B" });
82
+ agg.ingestOne({ type: "death", player: "A", cause: "fall" });
83
+ assert.equal(sent.length, 0);
84
+ assert.equal(agg.pendingCount(), 3);
85
+ assert.equal(timers.length, 1);
86
+ assert.equal(timers[0].ms, 6e4);
87
+ timers[0].fn();
88
+ assert.equal(sent.length, 1);
89
+ assert.match(sent[0], /上线:A、B/);
90
+ assert.match(sent[0], /死亡:A(坠落)/);
91
+ assert.equal(agg.pendingCount(), 0);
92
+ agg.dispose();
93
+ });
94
+ test("crash \u7ACB\u5373\u63A8\u4E14\u5148\u51B2\u5237\u7A97\u53E3", () => {
95
+ const sent = [];
96
+ const timers = [];
97
+ const agg = createQqEventsAggregator({
98
+ getConfig: () => ALL_ON,
99
+ getOutbound: () => fakeOutbound,
100
+ send: (t) => sent.push(t),
101
+ setTimeoutFn: (fn) => {
102
+ timers.push({ fn });
103
+ return 1;
104
+ },
105
+ clearTimeoutFn: () => {
106
+ }
107
+ });
108
+ agg.ingestOne({ type: "join", player: "Steve" });
109
+ agg.ingestOne({ type: "crash", detail: "code=1" });
110
+ assert.equal(sent.length, 2);
111
+ assert.match(sent[0], /上线:Steve/);
112
+ assert.match(sent[1], /BDS 意外退出 \(code=1\)/);
113
+ assert.equal(agg.pendingCount(), 0);
114
+ agg.dispose();
115
+ });
116
+ test("enabled:false \u5168\u4E22", () => {
117
+ const sent = [];
118
+ const agg = createQqEventsAggregator({
119
+ getConfig: () => ({ ...ALL_ON, enabled: false }),
120
+ getOutbound: () => fakeOutbound,
121
+ send: (t) => sent.push(t)
122
+ });
123
+ const r = agg.ingestMany([
124
+ { type: "join", player: "A" },
125
+ { type: "crash", detail: "x" }
126
+ ]);
127
+ assert.equal(r.accepted, 0);
128
+ assert.equal(r.rejected, 2);
129
+ assert.equal(sent.length, 0);
130
+ agg.dispose();
131
+ });
132
+ test("\u5355\u7C7B\u5173\u95ED\uFF1Adeath \u5173\u5219\u4E22\u5F03", () => {
133
+ const sent = [];
134
+ const timers = [];
135
+ const agg = createQqEventsAggregator({
136
+ getConfig: () => ({ ...ALL_ON, death: false }),
137
+ getOutbound: () => fakeOutbound,
138
+ send: (t) => sent.push(t),
139
+ setTimeoutFn: (fn) => {
140
+ timers.push({ fn });
141
+ return 1;
142
+ },
143
+ clearTimeoutFn: () => {
144
+ }
145
+ });
146
+ assert.equal(agg.ingestOne({ type: "death", player: "A", cause: "fall" }).accepted, false);
147
+ assert.equal(agg.ingestOne({ type: "join", player: "A" }).accepted, true);
148
+ timers[0].fn();
149
+ assert.equal(sent.length, 1);
150
+ assert.doesNotMatch(sent[0], /死亡/);
151
+ agg.dispose();
152
+ });
153
+ test("\u7A97\u53E3\u6EE1 MAX_WINDOW_EVENTS \u63D0\u524D flush", () => {
154
+ const sent = [];
155
+ const agg = createQqEventsAggregator({
156
+ getConfig: () => ({ ...ALL_ON, window_sec: 600 }),
157
+ getOutbound: () => fakeOutbound,
158
+ send: (t) => sent.push(t),
159
+ setTimeoutFn: () => 1,
160
+ clearTimeoutFn: () => {
161
+ }
162
+ });
163
+ for (let i = 0; i < MAX_WINDOW_EVENTS; i++) {
164
+ agg.ingestOne({ type: "join", player: `P${i}` });
165
+ }
166
+ assert.equal(sent.length, 1);
167
+ assert.equal(agg.pendingCount(), 0);
168
+ agg.dispose();
169
+ });
170
+ //# sourceMappingURL=qq-events.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/qq-events.test.ts"],
4
+ "sourcesContent": ["/**\n * qq-events \u805A\u5408\u5668\u5355\u6D4B\n */\n\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport {\n createQqEventsAggregator,\n formatImmediateBody,\n formatWindowBody,\n localizeCause,\n MAX_WINDOW_EVENTS,\n normalizeEventPayload,\n resolveQqEventsConfig,\n type ResolvedQqEventsConfig,\n} from \"./qq-events.js\";\nimport type { OutboundConfig } from \"./bridge.js\";\n\nconst ALL_ON: ResolvedQqEventsConfig = {\n enabled: true,\n window_sec: 60,\n join: true,\n leave: true,\n death: true,\n crash: true,\n start: true,\n};\n\nconst fakeOutbound: OutboundConfig = {\n backend: \"official\",\n creds: { appId: \"a\", appSecret: \"s\" },\n groupOpenid: \"G\",\n prefix: \"[MC]\",\n};\n\ntest(\"resolveQqEventsConfig \u7F3A\u7701\u4E0E\u603B\u5F00\u5173\", () => {\n const d = resolveQqEventsConfig(undefined);\n assert.equal(d.enabled, true);\n assert.equal(d.window_sec, 60);\n assert.equal(d.join, true);\n\n const off = resolveQqEventsConfig({ enabled: false, join: false });\n assert.equal(off.enabled, false);\n assert.equal(off.join, false);\n assert.equal(off.death, true);\n});\n\ntest(\"localizeCause \u6620\u5C04\u4E0E\u539F\u6587\u56DE\u9000\", () => {\n assert.equal(localizeCause(\"fall\"), \"\u5760\u843D\");\n assert.equal(localizeCause(\"drowning\"), \"\u6EBA\u6C34\");\n assert.equal(localizeCause(\"custom_xyz\"), \"custom_xyz\");\n assert.equal(localizeCause(\"\"), \"\");\n});\n\ntest(\"normalizeEventPayload \u8FC7\u6EE4\u975E\u6CD5 type\", () => {\n assert.equal(normalizeEventPayload({ type: \"chat\" }), null);\n assert.deepEqual(normalizeEventPayload({ type: \"join\", player: \"Steve\" }), {\n type: \"join\",\n player: \"Steve\",\n });\n});\n\ntest(\"formatWindowBody \u805A\u5408\u6587\u6848\", () => {\n const text = formatWindowBody([\n { type: \"join\", player: \"Steve\" },\n { type: \"join\", player: \"Alex\" },\n { type: \"leave\", player: \"Bob\" },\n { type: \"death\", player: \"Steve\", cause: \"fall\" },\n ]);\n assert.match(text, /^\\[MC\u4E8B\u4EF6\\]/);\n assert.match(text, /\u4E0A\u7EBF\uFF1ASteve\u3001Alex/);\n assert.match(text, /\u4E0B\u7EBF\uFF1ABob/);\n assert.match(text, /\u6B7B\u4EA1\uFF1ASteve\uFF08\u5760\u843D\uFF09/);\n});\n\ntest(\"formatImmediateBody crash/start\", () => {\n assert.equal(formatImmediateBody({ type: \"crash\", detail: \"code=1\" }), \"[MC\u4E8B\u4EF6] BDS \u610F\u5916\u9000\u51FA (code=1)\");\n assert.equal(formatImmediateBody({ type: \"start\", detail: \"pid=9\" }), \"[MC\u4E8B\u4EF6] BDS \u5DF2\u542F\u52A8 (pid=9)\");\n});\n\ntest(\"\u7A97\u53E3\u5185 join+death \u5230\u671F flush \u6210\u4E00\u6761\", () => {\n const sent: string[] = [];\n const timers: Array<{ fn: () => void; ms: number }> = [];\n const agg = createQqEventsAggregator({\n getConfig: () => ({ ...ALL_ON, window_sec: 60 }),\n getOutbound: () => fakeOutbound,\n send: (t) => sent.push(t),\n setTimeoutFn: (fn, ms) => {\n timers.push({ fn, ms });\n return timers.length as unknown as ReturnType<typeof setTimeout>;\n },\n clearTimeoutFn: () => {},\n });\n\n agg.ingestOne({ type: \"join\", player: \"A\" });\n agg.ingestOne({ type: \"join\", player: \"B\" });\n agg.ingestOne({ type: \"death\", player: \"A\", cause: \"fall\" });\n assert.equal(sent.length, 0);\n assert.equal(agg.pendingCount(), 3);\n assert.equal(timers.length, 1);\n assert.equal(timers[0]!.ms, 60_000);\n\n timers[0]!.fn();\n assert.equal(sent.length, 1);\n assert.match(sent[0]!, /\u4E0A\u7EBF\uFF1AA\u3001B/);\n assert.match(sent[0]!, /\u6B7B\u4EA1\uFF1AA\uFF08\u5760\u843D\uFF09/);\n assert.equal(agg.pendingCount(), 0);\n agg.dispose();\n});\n\ntest(\"crash \u7ACB\u5373\u63A8\u4E14\u5148\u51B2\u5237\u7A97\u53E3\", () => {\n const sent: string[] = [];\n const timers: Array<{ fn: () => void }> = [];\n const agg = createQqEventsAggregator({\n getConfig: () => ALL_ON,\n getOutbound: () => fakeOutbound,\n send: (t) => sent.push(t),\n setTimeoutFn: (fn) => {\n timers.push({ fn });\n return 1 as unknown as ReturnType<typeof setTimeout>;\n },\n clearTimeoutFn: () => {},\n });\n\n agg.ingestOne({ type: \"join\", player: \"Steve\" });\n agg.ingestOne({ type: \"crash\", detail: \"code=1\" });\n assert.equal(sent.length, 2);\n assert.match(sent[0]!, /\u4E0A\u7EBF\uFF1ASteve/);\n assert.match(sent[1]!, /BDS \u610F\u5916\u9000\u51FA \\(code=1\\)/);\n assert.equal(agg.pendingCount(), 0);\n agg.dispose();\n});\n\ntest(\"enabled:false \u5168\u4E22\", () => {\n const sent: string[] = [];\n const agg = createQqEventsAggregator({\n getConfig: () => ({ ...ALL_ON, enabled: false }),\n getOutbound: () => fakeOutbound,\n send: (t) => sent.push(t),\n });\n const r = agg.ingestMany([\n { type: \"join\", player: \"A\" },\n { type: \"crash\", detail: \"x\" },\n ]);\n assert.equal(r.accepted, 0);\n assert.equal(r.rejected, 2);\n assert.equal(sent.length, 0);\n agg.dispose();\n});\n\ntest(\"\u5355\u7C7B\u5173\u95ED\uFF1Adeath \u5173\u5219\u4E22\u5F03\", () => {\n const sent: string[] = [];\n const timers: Array<{ fn: () => void }> = [];\n const agg = createQqEventsAggregator({\n getConfig: () => ({ ...ALL_ON, death: false }),\n getOutbound: () => fakeOutbound,\n send: (t) => sent.push(t),\n setTimeoutFn: (fn) => {\n timers.push({ fn });\n return 1 as unknown as ReturnType<typeof setTimeout>;\n },\n clearTimeoutFn: () => {},\n });\n assert.equal(agg.ingestOne({ type: \"death\", player: \"A\", cause: \"fall\" }).accepted, false);\n assert.equal(agg.ingestOne({ type: \"join\", player: \"A\" }).accepted, true);\n timers[0]!.fn();\n assert.equal(sent.length, 1);\n assert.doesNotMatch(sent[0]!, /\u6B7B\u4EA1/);\n agg.dispose();\n});\n\ntest(\"\u7A97\u53E3\u6EE1 MAX_WINDOW_EVENTS \u63D0\u524D flush\", () => {\n const sent: string[] = [];\n const agg = createQqEventsAggregator({\n getConfig: () => ({ ...ALL_ON, window_sec: 600 }),\n getOutbound: () => fakeOutbound,\n send: (t) => sent.push(t),\n setTimeoutFn: () => 1 as unknown as ReturnType<typeof setTimeout>,\n clearTimeoutFn: () => {},\n });\n for (let i = 0; i < MAX_WINDOW_EVENTS; i++) {\n agg.ingestOne({ type: \"join\", player: `P${i}` });\n }\n assert.equal(sent.length, 1);\n assert.equal(agg.pendingCount(), 0);\n agg.dispose();\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=qq-join.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-join.test.d.ts","sourceRoot":"","sources":["../../src/domain/qq-join.test.ts"],"names":[],"mappings":""}