@linxin666/dsh-usage 0.3.23 → 0.4.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.
Files changed (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +7 -9
  3. package/README.zh.md +7 -9
  4. package/icon.svg +3 -0
  5. package/lib/client.js +621 -65
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +258 -271
  8. package/lib/types/client/UsageFootCard.d.ts +55 -0
  9. package/lib/types/client/UsageSectionCard.d.ts +3 -4
  10. package/lib/types/client/body-mutations.d.ts +45 -0
  11. package/lib/types/client/foot-card-mount.d.ts +34 -0
  12. package/lib/types/client/index.d.ts +21 -8
  13. package/lib/types/client/locales.d.ts +6 -4
  14. package/lib/types/core/provider-routes.d.ts +26 -0
  15. package/lib/types/core/types.d.ts +14 -1
  16. package/lib/types/host/access.d.ts +17 -0
  17. package/lib/types/host/http.d.ts +18 -1
  18. package/lib/types/host/pair-access.d.ts +34 -0
  19. package/lib/types/host/routes.d.ts +16 -7
  20. package/lib/types/host/usage-service.d.ts +32 -76
  21. package/lib/types/index.d.ts +14 -2
  22. package/package.json +20 -17
  23. package/src/client/UsageFootCard.tsx +300 -0
  24. package/src/client/UsageSectionCard.tsx +41 -23
  25. package/src/client/body-mutations.ts +133 -0
  26. package/src/client/foot-card-mount.tsx +112 -0
  27. package/src/client/index.ts +57 -16
  28. package/src/client/locales.ts +12 -8
  29. package/src/client/usage.module.css +188 -0
  30. package/src/core/adapters.ts +40 -6
  31. package/src/core/provider-routes.ts +37 -0
  32. package/src/core/types.ts +14 -1
  33. package/src/host/access.ts +21 -0
  34. package/src/host/http.ts +23 -1
  35. package/src/host/pair-access.ts +49 -0
  36. package/src/host/routes.ts +19 -10
  37. package/src/host/usage-service.ts +74 -218
  38. package/src/index.ts +59 -60
package/src/index.ts CHANGED
@@ -8,14 +8,18 @@ import { makeUsageOverviewRoute, makeUsageRefreshRoute } from './host/routes.ts'
8
8
 
9
9
  export const name = 'dsh-usage'
10
10
  export const inject = ['webServer']
11
+ /**
12
+ * Family settings namespace this row serves. Since 0.1.7 the settings surface
13
+ * addresses one form per profile entry id and the Host generates this row's
14
+ * page from the Config schema below, so the namespace is only the alias the
15
+ * family settings bridge (dsh-web-settings) resolves onto this row's entry id.
16
+ */
11
17
  export const USAGE_SETTINGS_NAMESPACE = 'dsh-usage' as SettingsNamespace
12
18
 
13
19
  export interface Config {
14
20
  enabled?: boolean
15
21
  /** Provider probe cycle in seconds; 30-3600. */
16
22
  pollIntervalSec?: number
17
- /** Pet bubble mode: always (refreshes each poll), change (only on value change), off. */
18
- bubbleMode?: string
19
23
  /** Ledger retention in local days. */
20
24
  retainDays?: number
21
25
  }
@@ -23,7 +27,6 @@ export interface Config {
23
27
  export const Config: z<Config> = z.object({
24
28
  enabled: z.boolean().default(true),
25
29
  pollIntervalSec: z.number().min(30).max(3600).default(60),
26
- bubbleMode: z.string().default('always'),
27
30
  retainDays: z.number().min(7).max(730).default(180),
28
31
  })
29
32
 
@@ -32,83 +35,79 @@ export interface ResolvedConfig extends UsageServiceOptions {
32
35
  }
33
36
 
34
37
  export function resolveConfig(config?: Config): ResolvedConfig {
35
- const bubbleMode = config?.bubbleMode === 'change' || config?.bubbleMode === 'off' ? config.bubbleMode : 'always'
36
38
  return {
37
39
  enabled: config?.enabled ?? true,
38
40
  pollIntervalSec: typeof config?.pollIntervalSec === 'number' ? config.pollIntervalSec : 60,
39
- bubbleMode,
40
41
  retainDays: typeof config?.retainDays === 'number' ? config.retainDays : 180,
41
42
  }
42
43
  }
43
44
 
45
+ /**
46
+ * Tail of the process-wide flush chain; undefined while no predecessor's final
47
+ * ledger flush is still in flight. The Host reloads this profile row when its
48
+ * configuration changes, so a successor activation is a dispose + apply pair in
49
+ * the same process: its first ledger load must serialize behind its
50
+ * predecessor's final write (and behind the reloads before that one).
51
+ */
52
+ let pendingStop: Promise<unknown> | undefined
53
+
54
+ /**
55
+ * Host body. The plugin's own Config schema above is the settings document the
56
+ * 0.1.7 Host serves for this row; the effective config arrives here as the
57
+ * `config` argument the Loader resolved and the Host reloads the row when the
58
+ * user saves a change, so nothing in this body observes or writes settings.
59
+ * @param ctx - host root context.
60
+ * @param config - the row's resolved configuration.
61
+ */
44
62
  export const apply = mountOnce('@linxin666/dsh-usage', (ctx: Context, config?: Config): void => {
45
- let source: () => Config = () => config ?? {}
63
+ const value = resolveConfig(config)
64
+ // A disabled row mounts nothing: no probe cycle, no ledger, no routes. The
65
+ // settings page owns the enable switch, and re-enabling reloads this row.
66
+ if (!value.enabled) return
67
+
68
+ let live = true
46
69
  let service: UsageService | undefined
47
- let pendingStop: Promise<void> | undefined
48
70
  let disposeRoutes: (() => void) | undefined
49
71
 
50
- const rearm = (): void => {
51
- const value = resolveConfig(source())
52
- if (!value.enabled) {
53
- pendingStop = service?.stop()
54
- service = undefined
55
- disposeRoutes?.()
56
- disposeRoutes = undefined
57
- return
58
- }
59
- if (service === undefined) {
60
- const next = new UsageService(ctx, value)
61
- service = next
62
- const begin = (): void => {
63
- // A disable may have landed while the predecessor's final flush was
64
- // still pending; only start if this instance is still the live one.
65
- if (service !== next) return
66
- next.start()
67
- const disposers = [makeUsageOverviewRoute(next), makeUsageRefreshRoute(next)]
68
- .map((route) => ctx.webServer.register(route))
69
- disposeRoutes = () => {
70
- for (const dispose of disposers) {
71
- try {
72
- dispose()
73
- } catch {
74
- // Route fiber already gone during shutdown.
75
- }
76
- }
72
+ const begin = (): void => {
73
+ // The fiber may have gone down while the predecessor's final flush was
74
+ // still pending; only start while this activation is the live one.
75
+ if (!live) return
76
+ const next = new UsageService(ctx, value)
77
+ service = next
78
+ next.start()
79
+ const disposers = [makeUsageOverviewRoute(ctx, next), makeUsageRefreshRoute(ctx, next)]
80
+ .map((route) => ctx.webServer.register(route))
81
+ disposeRoutes = () => {
82
+ for (const dispose of disposers) {
83
+ try {
84
+ dispose()
85
+ } catch {
86
+ // Route fiber already gone during shutdown.
77
87
  }
78
88
  }
79
- // Serialize the start behind a just-stopped predecessor: its final
80
- // ledger flush must land on disk before this instance loads the file.
81
- if (pendingStop !== undefined) pendingStop.then(begin, begin)
82
- else begin()
83
- } else {
84
- service.applyOptions(value)
85
89
  }
86
90
  }
87
91
 
88
- ctx.inject(['settings'], (settingsCtx) => {
89
- try {
90
- if (typeof settingsCtx.settings?.installSection === 'function') {
91
- settingsCtx.settings.installSection(ctx, USAGE_SETTINGS_NAMESPACE, Config, config ?? {}, {
92
- setSource: (next) => { source = next; rearm() },
93
- onChange: rearm,
94
- })
95
- } else if (typeof settingsCtx.settings?.register === 'function') {
96
- const scope = settingsCtx.settings.register(USAGE_SETTINGS_NAMESPACE, Config, { base: config ?? {} })
97
- source = () => scope?.get?.() ?? (config ?? {})
98
- scope?.watch?.(() => { rearm() })
99
- rearm()
100
- }
101
- } catch {
102
- // Defensive fallback against settings registration differences
103
- }
104
- })
105
-
106
92
  ctx.effect(() => {
107
- rearm()
93
+ if (pendingStop === undefined) {
94
+ // No predecessor's flush is in flight: start now.
95
+ begin()
96
+ } else {
97
+ // Serialize the start behind every predecessor: their final writes must
98
+ // land on disk before this instance loads the file.
99
+ pendingStop = pendingStop.then(begin, begin).then(() => undefined)
100
+ }
108
101
  return () => {
102
+ live = false
109
103
  disposeRoutes?.()
110
- void service?.stop()
104
+ disposeRoutes = undefined
105
+ const stopping = service?.stop()
111
106
  service = undefined
107
+ // A successor waits for this instance's final flush, which itself waited
108
+ // for the predecessor's start: a row disposed before it ever started
109
+ // keeps the earlier link instead of dropping it.
110
+ if (stopping !== undefined) pendingStop = (pendingStop ?? Promise.resolve()).then(() => stopping)
112
111
  }
113
112
  }, 'dsh-usage: runtime')
114
113
  })