@linxin666/dsh-usage 0.4.3 → 0.4.4

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: 2397df7cc658e2f6f5f447312723de5bee7cb490
6
- README.zh.md: 34535e95511e0d66198e801d37ef5ba6b5d1a288
5
+ README.md: ce35e9cda06a1c0d34c1c37170eece47ffa2a80c
6
+ README.zh.md: 4199ceccb73a627ba4bef72c4857d4a80c144020
package/README.md CHANGED
@@ -24,7 +24,7 @@ It covers the official DeepSeek routes only — relay traffic billed elsewhere (
24
24
 
25
25
  ## Install
26
26
 
27
- Requires DSH 0.1.7-rc.2 or later: the plugin is developed against the 0.1.7-rc.2 DSH cohort (its settings page is the Host-generated page of the plugin's own Config schema) and its `@deepseek-ai/*` runtime imports are provided by the host itself.
27
+ Requires DSH 0.2.0-rc.1 or later: the plugin is developed against the 0.2.0-rc.1 DSH cohort (its settings page is the Host-generated page of the plugin's own Config schema) and its `@deepseek-ai/*` runtime imports are provided by the host itself.
28
28
 
29
29
  In your profile (e.g. `~/.dsh/profiles/web`):
30
30
 
package/README.zh.md CHANGED
@@ -24,7 +24,7 @@ dsh Web GUI 的使用统计插件:多 provider 余额与编程套餐用量检
24
24
 
25
25
  ## 安装
26
26
 
27
- 要求 DSH 0.1.7-rc.2 或更高:插件基于 0.1.7-rc.2 DSH cohort 开发(其设置页即宿主根据插件自身 Config schema 生成的页面),`@deepseek-ai/*` 运行时导入由宿主本体提供。
27
+ 要求 DSH 0.2.0-rc.1 或更高:插件基于 0.2.0-rc.1 DSH cohort 开发(其设置页即宿主根据插件自身 Config schema 生成的页面),`@deepseek-ai/*` 运行时导入由宿主本体提供。
28
28
 
29
29
  在 profile(如 `~/.dsh/profiles/web`)中:
30
30
 
package/lib/index.js CHANGED
@@ -118,7 +118,7 @@ function mountOnce(packageName, fn) {
118
118
  return mount;
119
119
  }
120
120
  //#endregion
121
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
121
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.0-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
122
122
  /**
123
123
  * Duplicate-install-safe nominal primitive helpers.
124
124
  *
@@ -141,7 +141,7 @@ function brandString(value) {
141
141
  return value;
142
142
  }
143
143
  //#endregion
144
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-inva_98fa173416f048996704a605a96a2b0e/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
144
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.2.0-rc.1_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-inva_1a647ad178a224e363f109a5f72e2237/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
145
145
  /**
146
146
  * Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
147
147
  * *references* to secrets — environment-variable names — while providers own
@@ -1098,16 +1098,71 @@ async function writeJsonAtomic(path, value) {
1098
1098
  throw error;
1099
1099
  }
1100
1100
  }
1101
+ function unwrapVolatiles(value) {
1102
+ if (value === null || value === void 0) return value;
1103
+ if (typeof value === "object") {
1104
+ if (typeof value.get === "function") return unwrapVolatiles(value.get());
1105
+ if (Array.isArray(value)) return value.map(unwrapVolatiles);
1106
+ const result = {};
1107
+ for (const [k, v] of Object.entries(value)) result[k] = unwrapVolatiles(v);
1108
+ return result;
1109
+ }
1110
+ return value;
1111
+ }
1112
+ function isPlainObject(value) {
1113
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1114
+ }
1115
+ function deepMerge(base, patch) {
1116
+ if (!isPlainObject(base) || !isPlainObject(patch)) return patch !== void 0 ? patch : base;
1117
+ const result = { ...base };
1118
+ for (const [key, value] of Object.entries(patch)) {
1119
+ if (value === void 0) continue;
1120
+ result[key] = isPlainObject(value) && isPlainObject(result[key]) ? deepMerge(result[key], value) : value;
1121
+ }
1122
+ return result;
1123
+ }
1101
1124
  /**
1102
1125
  * Read a foreign settings namespace's resolved value (the llm adapter
1103
- * profiles, the agent default model). Unregistered namespaces read as
1104
- * undefined; nothing here throws into the poll loop.
1126
+ * profiles, the agent default model). Resolves across pre-0.1.7 settings.get(),
1127
+ * 0.1.7 SettingsForms.describe(), live loader entries, and configEditor.
1128
+ * Unregistered namespaces read as undefined; nothing here throws into the poll loop.
1105
1129
  */
1106
1130
  function readNamespace(ctx, ns) {
1107
1131
  try {
1108
1132
  const settings = service(ctx, "settings");
1109
- if (settings === void 0) return void 0;
1110
- return settings.get(ns);
1133
+ if (settings !== void 0) {
1134
+ if (typeof settings.get === "function") {
1135
+ const value = settings.get(ns);
1136
+ if (value !== void 0) return unwrapVolatiles(value);
1137
+ }
1138
+ if (typeof settings.describe === "function") {
1139
+ const descriptors = settings.describe({ redactSecrets: false });
1140
+ if (Array.isArray(descriptors)) {
1141
+ const match = descriptors.find((d) => d.ns === ns || ns === "llm-pi-ai" && (d.ns === "llm" || isPlainObject(d.value?.providers)) || ns === "llm-deepseek" && (d.ns === "llm-deepseek" || d.ns === "llm-deepseek-api-key"));
1142
+ if (match !== void 0 && match.value !== void 0) return unwrapVolatiles(match.value);
1143
+ }
1144
+ }
1145
+ }
1146
+ const loader = ctx.root?.loader ?? ctx.loader;
1147
+ if (loader !== void 0 && typeof loader.entries === "function") for (const entry of loader.entries()) {
1148
+ const id = entry.options?.id;
1149
+ const name = entry.options?.name;
1150
+ if (id === ns || name === `@deepseek-ai/dsh-${ns}` || name?.endsWith(`/${ns}`) || ns === "llm-pi-ai" && (id === "llm" || name === "@deepseek-ai/dsh-llm-pi-ai") || ns === "llm-deepseek" && (id === "llm-deepseek-api-key" || name === "@deepseek-ai/dsh-llm-deepseek-api-key")) {
1151
+ if (entry.fiber?.config !== void 0) return unwrapVolatiles(entry.fiber.config);
1152
+ }
1153
+ }
1154
+ const editor = service(ctx, "configEditor");
1155
+ if (editor !== void 0 && typeof editor.configuration === "function") {
1156
+ const match = editor.configuration().find((r) => {
1157
+ const id = r.entry.options.id;
1158
+ const name = r.entry.options.name;
1159
+ return id === ns || name === `@deepseek-ai/dsh-${ns}` || name?.endsWith(`/${ns}`) || ns === "llm-pi-ai" && (id === "llm" || name === "@deepseek-ai/dsh-llm-pi-ai") || ns === "llm-deepseek" && (id === "llm-deepseek-api-key" || name === "@deepseek-ai/dsh-llm-deepseek-api-key");
1160
+ });
1161
+ if (match !== void 0) {
1162
+ if (match.entry.fiber?.config !== void 0) return unwrapVolatiles(match.entry.fiber.config);
1163
+ return unwrapVolatiles(deepMerge(match.inherited, match.override));
1164
+ }
1165
+ }
1111
1166
  } catch {
1112
1167
  return;
1113
1168
  }
@@ -1188,8 +1243,8 @@ var UsageService = class {
1188
1243
  displayName: route.displayName || adapter?.displayName || route.id,
1189
1244
  credential: snapshot?.credential ?? "none",
1190
1245
  supported: adapter !== void 0 && (adapter.balance !== void 0 || adapter.plan !== void 0),
1191
- ...adapter?.balance !== void 0 && balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL) ? { balanceSupported: true } : {},
1192
- ...adapter?.balance !== void 0 && !balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL) ? { balanceSupported: false } : {},
1246
+ ...adapter?.balance !== void 0 && balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id)) ? { balanceSupported: true } : {},
1247
+ ...adapter?.balance !== void 0 && !balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id)) ? { balanceSupported: false } : {},
1193
1248
  ...adapter?.plan !== void 0 ? { planSupported: true } : {},
1194
1249
  ...snapshot?.balance !== void 0 ? { balance: snapshot.balance } : {},
1195
1250
  ...snapshot?.plan !== void 0 ? { plan: snapshot.plan } : {},
@@ -1518,7 +1573,7 @@ var UsageService = class {
1518
1573
  async probeRoute(route, adapter) {
1519
1574
  const credential = await this.resolveCredential(route.id);
1520
1575
  const previous = this.snapshots.get(route.id);
1521
- const balanceApplies = balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL);
1576
+ const balanceApplies = balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id));
1522
1577
  if (credential.key === void 0) {
1523
1578
  this.snapshots.set(route.id, {
1524
1579
  provider: route.id,
@@ -1633,6 +1688,14 @@ var UsageService = class {
1633
1688
  deepseekApiKeyEnv() {
1634
1689
  return readNamespace(this.ctx, "llm-deepseek")?.apiKeyEnv ?? "DEEPSEEK_API_KEY";
1635
1690
  }
1691
+ /** The DeepSeek official adapter's configured baseURL. */
1692
+ deepseekBaseUrl() {
1693
+ return readNamespace(this.ctx, "llm-deepseek")?.baseURL;
1694
+ }
1695
+ /** The effective base URL for a provider route (pi-ai or deepseek). */
1696
+ routeBaseUrl(provider) {
1697
+ return this.piAiProfile(provider)?.baseURL ?? (isDeepSeekProviderRoute(provider) ? this.deepseekBaseUrl() : void 0);
1698
+ }
1636
1699
  };
1637
1700
  //#endregion
1638
1701
  //#region src/host/loopback.ts
@@ -153,4 +153,8 @@ export declare class UsageService {
153
153
  private piAiProfile;
154
154
  /** The DeepSeek official adapter's credential reference name. */
155
155
  private deepseekApiKeyEnv;
156
+ /** The DeepSeek official adapter's configured baseURL. */
157
+ private deepseekBaseUrl;
158
+ /** The effective base URL for a provider route (pi-ai or deepseek). */
159
+ private routeBaseUrl;
156
160
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-usage",
3
3
  "description": "Usage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with a compact price glance card above the sidebar's Settings row",
4
- "version": "0.4.3",
4
+ "version": "0.4.4",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "engines": {
@@ -33,7 +33,7 @@
33
33
  "license": "Apache-2.0",
34
34
  "dsh": {
35
35
  "engines": {
36
- "dsh": ">=0.1.7-rc.2"
36
+ "dsh": ">=0.2.0-rc.1"
37
37
  },
38
38
  "bundle": {
39
39
  "patch": "./cordis.patch.yml"
@@ -53,22 +53,22 @@
53
53
  "@deepseek-ai/schemastery": "^3.18.4"
54
54
  },
55
55
  "peerDependencies": {
56
- "@deepseek-ai/dsh": ">=0.1.7-rc.2",
56
+ "@deepseek-ai/dsh": ">=0.2.0-rc.1",
57
57
  "react": "^18.2.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@deepseek-ai/cordis": "^4.0.4",
61
- "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
62
- "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
63
- "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
64
- "@deepseek-ai/dsh-client-store": "^0.1.7-rc.2",
65
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
66
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
67
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
68
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
69
- "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
70
- "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
71
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-credentials": "^0.2.0-rc.1",
62
+ "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
63
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
64
+ "@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
65
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
66
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
67
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
68
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
69
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
70
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
71
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
72
72
  "@testing-library/dom": "^10.4.1",
73
73
  "@testing-library/react": "^16.3.2",
74
74
  "@types/node": "^22.20.0",
@@ -103,19 +103,129 @@ async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
103
103
  }
104
104
  }
105
105
 
106
+ function unwrapVolatiles(value: unknown): unknown {
107
+ if (value === null || value === undefined) return value
108
+ if (typeof value === 'object') {
109
+ if (typeof (value as { get?: unknown }).get === 'function') {
110
+ return unwrapVolatiles((value as { get(): unknown }).get())
111
+ }
112
+ if (Array.isArray(value)) {
113
+ return value.map(unwrapVolatiles)
114
+ }
115
+ const result: Record<string, unknown> = {}
116
+ for (const [k, v] of Object.entries(value)) {
117
+ result[k] = unwrapVolatiles(v)
118
+ }
119
+ return result
120
+ }
121
+ return value
122
+ }
123
+
124
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
125
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
126
+ }
127
+
128
+ function deepMerge(base: unknown, patch: unknown): unknown {
129
+ if (!isPlainObject(base) || !isPlainObject(patch)) return patch !== undefined ? patch : base
130
+ const result: Record<string, unknown> = { ...base }
131
+ for (const [key, value] of Object.entries(patch)) {
132
+ if (value === undefined) continue
133
+ result[key] = isPlainObject(value) && isPlainObject(result[key]) ? deepMerge(result[key], value) : value
134
+ }
135
+ return result
136
+ }
137
+
106
138
  /**
107
139
  * Read a foreign settings namespace's resolved value (the llm adapter
108
- * profiles, the agent default model). Unregistered namespaces read as
109
- * undefined; nothing here throws into the poll loop.
140
+ * profiles, the agent default model). Resolves across pre-0.1.7 settings.get(),
141
+ * 0.1.7 SettingsForms.describe(), live loader entries, and configEditor.
142
+ * Unregistered namespaces read as undefined; nothing here throws into the poll loop.
110
143
  */
111
144
  function readNamespace(ctx: Context, ns: string): unknown {
112
145
  try {
113
- const settings = service<{ get(ns: unknown): unknown }>(ctx, 'settings')
114
- if (settings === undefined) return undefined
115
- return settings.get(ns)
146
+ // 1. Try legacy settings.get(ns) or 0.1.7 settings.describe()
147
+ const settings = service<{
148
+ get?(ns: unknown): unknown
149
+ describe?(options?: unknown): Array<{ ns: string; value: unknown }>
150
+ }>(ctx, 'settings')
151
+ if (settings !== undefined) {
152
+ if (typeof settings.get === 'function') {
153
+ const value = settings.get(ns)
154
+ if (value !== undefined) return unwrapVolatiles(value)
155
+ }
156
+ if (typeof settings.describe === 'function') {
157
+ const descriptors = settings.describe({ redactSecrets: false })
158
+ if (Array.isArray(descriptors)) {
159
+ const match = descriptors.find((d) =>
160
+ d.ns === ns
161
+ || (ns === 'llm-pi-ai' && (d.ns === 'llm' || isPlainObject((d.value as { providers?: unknown })?.providers)))
162
+ || (ns === 'llm-deepseek' && (d.ns === 'llm-deepseek' || d.ns === 'llm-deepseek-api-key'))
163
+ )
164
+ if (match !== undefined && match.value !== undefined) {
165
+ return unwrapVolatiles(match.value)
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ // 2. Try ctx.root.loader or ctx.loader entries
172
+ const loader = (ctx as unknown as {
173
+ root?: { loader?: { entries(): Iterable<{ options: { id: string; name?: string }; fiber?: { config?: unknown } }> } }
174
+ loader?: { entries(): Iterable<{ options: { id: string; name?: string }; fiber?: { config?: unknown } }> }
175
+ }).root?.loader
176
+ ?? (ctx as unknown as {
177
+ loader?: { entries(): Iterable<{ options: { id: string; name?: string }; fiber?: { config?: unknown } }> }
178
+ }).loader
179
+ if (loader !== undefined && typeof loader.entries === 'function') {
180
+ for (const entry of loader.entries()) {
181
+ const id = entry.options?.id
182
+ const name = entry.options?.name
183
+ if (
184
+ id === ns
185
+ || name === `@deepseek-ai/dsh-${ns}`
186
+ || name?.endsWith(`/${ns}`)
187
+ || (ns === 'llm-pi-ai' && (id === 'llm' || name === '@deepseek-ai/dsh-llm-pi-ai'))
188
+ || (ns === 'llm-deepseek' && (id === 'llm-deepseek-api-key' || name === '@deepseek-ai/dsh-llm-deepseek-api-key'))
189
+ ) {
190
+ if (entry.fiber?.config !== undefined) {
191
+ return unwrapVolatiles(entry.fiber.config)
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // 3. Try configEditor service
198
+ const editor = service<{
199
+ configuration?(): Array<{
200
+ entry: { options: { id: string; name?: string }; fiber?: { config?: unknown } }
201
+ inherited: Record<string, unknown>
202
+ override: Record<string, unknown>
203
+ }>
204
+ }>(ctx, 'configEditor')
205
+ if (editor !== undefined && typeof editor.configuration === 'function') {
206
+ const rows = editor.configuration()
207
+ const match = rows.find((r) => {
208
+ const id = r.entry.options.id
209
+ const name = r.entry.options.name
210
+ return (
211
+ id === ns
212
+ || name === `@deepseek-ai/dsh-${ns}`
213
+ || name?.endsWith(`/${ns}`)
214
+ || (ns === 'llm-pi-ai' && (id === 'llm' || name === '@deepseek-ai/dsh-llm-pi-ai'))
215
+ || (ns === 'llm-deepseek' && (id === 'llm-deepseek-api-key' || name === '@deepseek-ai/dsh-llm-deepseek-api-key'))
216
+ )
217
+ })
218
+ if (match !== undefined) {
219
+ if (match.entry.fiber?.config !== undefined) {
220
+ return unwrapVolatiles(match.entry.fiber.config)
221
+ }
222
+ return unwrapVolatiles(deepMerge(match.inherited, match.override))
223
+ }
224
+ }
116
225
  } catch {
117
226
  return undefined
118
227
  }
228
+ return undefined
119
229
  }
120
230
 
121
231
  export class UsageService {
@@ -206,10 +316,10 @@ export class UsageService {
206
316
  // THIS route's account: it is supported by the adapter but not
207
317
  // applicable here, and the UI renders it as unsupported rather than
208
318
  // showing the other account's number (issue #1688).
209
- ...(adapter?.balance !== undefined && balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL)
319
+ ...(adapter?.balance !== undefined && balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id))
210
320
  ? { balanceSupported: true }
211
321
  : {}),
212
- ...(adapter?.balance !== undefined && !balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL)
322
+ ...(adapter?.balance !== undefined && !balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id))
213
323
  ? { balanceSupported: false }
214
324
  : {}),
215
325
  ...(adapter?.plan !== undefined ? { planSupported: true } : {}),
@@ -587,7 +697,7 @@ export class UsageService {
587
697
  // account, and probing the official endpoint would print this account's
588
698
  // money under that route's name (issue #1688). Such a route reports the
589
699
  // balance as unsupported instead.
590
- const balanceApplies = balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL)
700
+ const balanceApplies = balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id))
591
701
  if (credential.key === undefined) {
592
702
  // Nothing to probe with (oauth grant, no credential): retained facts
593
703
  // would never refresh again, so the row resets to its credential
@@ -705,4 +815,15 @@ export class UsageService {
705
815
  return section?.apiKeyEnv ?? 'DEEPSEEK_API_KEY'
706
816
  }
707
817
 
818
+ /** The DeepSeek official adapter's configured baseURL. */
819
+ private deepseekBaseUrl(): string | undefined {
820
+ const section = readNamespace(this.ctx, 'llm-deepseek') as { baseURL?: string } | undefined
821
+ return section?.baseURL
822
+ }
823
+
824
+ /** The effective base URL for a provider route (pi-ai or deepseek). */
825
+ private routeBaseUrl(provider: string): string | undefined {
826
+ return this.piAiProfile(provider)?.baseURL ?? (isDeepSeekProviderRoute(provider) ? this.deepseekBaseUrl() : undefined)
827
+ }
828
+
708
829
  }
@@ -1,39 +0,0 @@
1
- /**
2
- * Async-boundary guard shared by the plugin family's Host halves: every
3
- * fire-and-forget promise chain and callback the host runtime does not own
4
- * (route handlers, timers, event listeners, spawned work) funnels through
5
- * these helpers. The dsh host installs a process-level fail-loud guard that
6
- * turns ANY unhandled promise rejection into a whole-process exit — one
7
- * plugin's stray rejection would otherwise take every plugin down. These
8
- * helpers exist so that failure mode is structurally impossible in family
9
- * code: the rejection becomes a logged error at the plugin boundary instead.
10
- *
11
- * Complements the aggregate's shell isolation (packages/dsh-web-all): the
12
- * shell contains import/activation failures at boot; runGuarded contains
13
- * run-time failures after activation.
14
- * @module dsh-web-shared/host/run-guarded
15
- */
16
- /**
17
- * Run one async operation, logging (never propagating) a rejection. Use for
18
- * promises whose lifecycle the caller does not await: spawned work, retry
19
- * loops, background flushes. Returns the original promise so callers can
20
- * still chain when they want to.
21
- * @param promise - the work to shield; any thenable.
22
- * @param label - log prefix naming the work site.
23
- * @param log - error sink; defaults to console.error.
24
- * @returns the input promise (rejection already consumed).
25
- */
26
- export declare function runGuarded<T>(promise: PromiseLike<T>, label: string, log?: (error: unknown) => void): PromiseLike<T>;
27
- /**
28
- * Wrap one callback so every invocation is individually guarded: a rejection
29
- * inside one call is logged and swallowed instead of escaping into whatever
30
- * infrastructure invoked the callback (HTTP server, EventEmitter, interval).
31
- * Sync throws are caught identically; a returned promise is replaced by
32
- * `undefined` after guarding (callers that need the original rejection should
33
- * await inside their own try/catch instead).
34
- * @param label - log prefix naming the callback site.
35
- * @param handler - the work to guard.
36
- * @param log - error sink; defaults to console.error.
37
- * @returns a wrapped callback with the same parameter list.
38
- */
39
- export declare function guardedHandler<A extends unknown[]>(label: string, handler: (...args: A) => unknown, log?: (error: unknown) => void): (...args: A) => unknown;
@@ -1,76 +0,0 @@
1
- // Generated by scripts/sync-shared.mjs from shared/host/run-guarded.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
- /**
3
- * Async-boundary guard shared by the plugin family's Host halves: every
4
- * fire-and-forget promise chain and callback the host runtime does not own
5
- * (route handlers, timers, event listeners, spawned work) funnels through
6
- * these helpers. The dsh host installs a process-level fail-loud guard that
7
- * turns ANY unhandled promise rejection into a whole-process exit — one
8
- * plugin's stray rejection would otherwise take every plugin down. These
9
- * helpers exist so that failure mode is structurally impossible in family
10
- * code: the rejection becomes a logged error at the plugin boundary instead.
11
- *
12
- * Complements the aggregate's shell isolation (packages/dsh-web-all): the
13
- * shell contains import/activation failures at boot; runGuarded contains
14
- * run-time failures after activation.
15
- * @module dsh-web-shared/host/run-guarded
16
- */
17
-
18
- /** Format one failure line for logging. */
19
- function formatFailure(label: string, error: unknown): Error {
20
- return new Error(`[${label}] unhandled async failure: ${error instanceof Error ? error.stack ?? error.message : String(error)}`)
21
- }
22
-
23
- /**
24
- * Run one async operation, logging (never propagating) a rejection. Use for
25
- * promises whose lifecycle the caller does not await: spawned work, retry
26
- * loops, background flushes. Returns the original promise so callers can
27
- * still chain when they want to.
28
- * @param promise - the work to shield; any thenable.
29
- * @param label - log prefix naming the work site.
30
- * @param log - error sink; defaults to console.error.
31
- * @returns the input promise (rejection already consumed).
32
- */
33
- export function runGuarded<T>(promise: PromiseLike<T>, label: string, log: (error: unknown) => void = console.error): PromiseLike<T> {
34
- void Promise.resolve(promise).catch(error => {
35
- log(formatFailure(label, error))
36
- })
37
- return promise
38
- }
39
-
40
- /**
41
- * Wrap one callback so every invocation is individually guarded: a rejection
42
- * inside one call is logged and swallowed instead of escaping into whatever
43
- * infrastructure invoked the callback (HTTP server, EventEmitter, interval).
44
- * Sync throws are caught identically; a returned promise is replaced by
45
- * `undefined` after guarding (callers that need the original rejection should
46
- * await inside their own try/catch instead).
47
- * @param label - log prefix naming the callback site.
48
- * @param handler - the work to guard.
49
- * @param log - error sink; defaults to console.error.
50
- * @returns a wrapped callback with the same parameter list.
51
- */
52
- export function guardedHandler<A extends unknown[]>(
53
- label: string,
54
- handler: (...args: A) => unknown,
55
- log: (error: unknown) => void = console.error,
56
- ): (...args: A) => unknown {
57
- return (...args: A) => {
58
- try {
59
- const result = handler(...args)
60
- if (isPromiseLike(result)) {
61
- void Promise.resolve(result).catch(error => {
62
- log(formatFailure(label, error))
63
- })
64
- return undefined
65
- }
66
- return result
67
- } catch (error) {
68
- log(formatFailure(label, error))
69
- return undefined
70
- }
71
- }
72
- }
73
-
74
- function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
75
- return typeof value === 'object' && value !== null && typeof (value as PromiseLike<unknown>).then === 'function'
76
- }