@slux/telemetry 0.0.0-stage → 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @slux/telemetry
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 首次公开发布 slux 的 25 个包,统一版本为 0.1.0。
8
+
9
+ - 提供框架无关的响应式核心与共享工具,以及 Store、Atom、Proxy、Flux、状态机和 RxJS 六种状态组织方式。
10
+ - 提供持久化、历史记录、跨标签页同步、遥测和开发工具五类 Store 扩展。
11
+ - 提供 React、Vue、Solid、Svelte、Angular、Preact、Ripple、Inferno、Octane、Vitarx、ActView 和 Viewfly 绑定。
12
+ - 提供各包文档与对应示例,并接入发布检查和文档部署工作流。
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies
17
+ - @slux/shared@0.1.0
18
+ - @slux/store@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 itmanyong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # @slux/telemetry
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 为 `@slux/store` 记录初始化、状态变化、订阅三类事件的遥测中间件 `telemetry`。它把元数据交给调用方提供的回调,支持按字段过滤与运行时暂停;不发送网络请求、不克隆状态快照。它作为 `@slux/store` 中间件接入,复用 Store 的公共契约,本文只写本包差异。
4
+
5
+ > 中间件的组合方式、实例归属、销毁清理链与顺序等通用约定见仓库根 [README](../../README.md) 的「Store 中间件 · 通用契约」与「本地开发」。
6
+
7
+ ## 特性与适用场景
8
+
9
+ | 场景 | 是否适合 | 说明 |
10
+ | ------------------------------ | -------- | ------------------------------------------------------------ |
11
+ | 统计每次更新耗时、累计更新次数 | 适合 | `StateChangeMetadata` 含 `durationMs` 与 `changeCount` |
12
+ | 把指标接入监控/日志系统 | 适合 | 通过 `onStateChange` / `onInit` / `onSubscribe` 回调自行上报 |
13
+ | 按字段过滤或临时暂停上报 | 适合 | `filter` 与 `setEnabled(false)` |
14
+ | 需要隔离回调错误 | 适合 | 遥测回调异常交给 `onError`,不阻断状态更新 |
15
+ | 需要精确的活跃订阅数 | 不适合 | `totalSubscriptions` 是累计订阅次数,不是当前活跃数 |
16
+ | 需要自动网络上报或落盘 | 不适合 | 本包不发请求、不持久化 |
17
+ | 需要状态快照的独立副本 | 需谨慎 | `prevState` / `nextState` 是引用,不做深拷贝 |
18
+
19
+ ## 安装
20
+
21
+ ```sh
22
+ pnpm add @slux/store @slux/telemetry
23
+ ```
24
+
25
+ - **运行时依赖**(`dependencies`):`@slux/store`、`@slux/shared`。
26
+ - **无 `peerDependencies`**:示例里直接 import 的 `@slux/store` 建议在应用侧显式声明。
27
+ - **宿主能力**:耗时优先取 `performance.now()`,缺失时回退 `Date.now()`;`timestamp` 用 `new Date().toISOString()`。无浏览器专属 API 依赖,可用于 SSR。
28
+
29
+ ## 快速开始
30
+
31
+ ```ts
32
+ import { createStore } from "@slux/store";
33
+ import { telemetry } from "@slux/telemetry";
34
+
35
+ type State = { count: number };
36
+
37
+ const metrics = telemetry<State>({
38
+ onStateChange: (meta) => console.log(meta.changeCount, meta.durationMs),
39
+ onError: (context) => console.error(context.source, context.error),
40
+ });
41
+
42
+ const store = createStore<State>(() => ({ count: 0 }), { middleware: [metrics] });
43
+
44
+ store.setState({ count: 1 });
45
+ metrics.setEnabled(false); // 暂停上报,状态更新照常
46
+ ```
47
+
48
+ ## 核心 API
49
+
50
+ ### `telemetry<T>(options: TelemetryOptions<T> = {}): TelemetryMiddleware<T>`
51
+
52
+ | 选项 | 类型 | 说明 |
53
+ | --------------- | -------------------------------------------------------------- | -------------------------- |
54
+ | `onInit` | `(initialState: T, metadata: InitMetadata) => void` | `init` 时的初始状态事件 |
55
+ | `onStateChange` | `(metadata: StateChangeMetadata<T>) => void` | 每次状态更新事件 |
56
+ | `onSubscribe` | `(metadata: SubscriptionMetadata) => void` | 每次注册订阅事件 |
57
+ | `onError` | `(context: ErrorContext<T>) => void` | 接收遥测回调的错误与来源 |
58
+ | `filter` | `(prevState: T, nextState: T, partial: Partial<T>) => boolean` | 返回 `true` 才上报状态变化 |
59
+
60
+ 三类事件:
61
+
62
+ | 事件 | 触发时机 | 回调参数 |
63
+ | -------- | --------------------------------- | -------------------------------------------------------------------------------- |
64
+ | 初始化 | `init` | `onInit(api.getState(), { name, timestamp })`,`state` 即 creator 返回的初始状态 |
65
+ | 状态更新 | `onSet` 中 `next(partial)` 成功后 | `onStateChange(StateChangeMetadata<T>)` |
66
+ | 订阅 | 每次 `subscribe` 注册 | `onSubscribe({ name, totalSubscriptions, timestamp })` |
67
+
68
+ 元数据字段(均为只读):
69
+
70
+ | 类型 | 字段 |
71
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
72
+ | `StateChangeMetadata<T>` | `name`、`prevState`、`nextState`、`partial`、`timestamp`(ISO 字符串)、`durationMs`、`changeCount` |
73
+ | `InitMetadata` | `name`、`timestamp` |
74
+ | `SubscriptionMetadata` | `name`、`totalSubscriptions`、`timestamp` |
75
+ | `ErrorContext<T>` | `error`、`source`(`"onStateChange" \| "onInit" \| "onSubscribe" \| "filter"`)、`state`(`onSubscribe` 错误为 `undefined`) |
76
+
77
+ 控制器方法:`setEnabled(enabled: boolean): void` 与 `isEnabled(): boolean`。实例默认启用;`name` 固定为 `"telemetry"`。
78
+
79
+ ## 专属语义与边界
80
+
81
+ **生成时机**:`onSet` 先记录 `prevState`、调用 `next(partial)` 让下游中间件执行,再读取 `nextState`,因此 `nextState` 能看到下游改写后的状态。`durationMs` 覆盖下游更新与 `filter` 执行,不包含上报回调自身。
82
+
83
+ **计时**:优先使用 `performance.now()`,缺失时回退 `Date.now()`;元数据里的 `timestamp` 是 `toISOString()` 字符串,不是毫秒时间戳。
84
+
85
+ **过滤与暂停**:`filter` 返回 `false` 则不上报;`setEnabled(false)` 时跳过 `onInit` / `onSubscribe` / `onStateChange` / `filter` 全部回调。两者都**不阻断状态更新**,且 `changeCount` 在暂停与被过滤期间仍累计(它统计的是包含这些情形在内的更新次数)。未配置 `onStateChange` 时不会调用 `filter`。
86
+
87
+ **订阅计数**:`totalSubscriptions` 是累计订阅次数,包含已取消的订阅(Store 没有取消订阅钩子),暂停期间注册也照常累计。`onSubscribe` 返回原 `listener`,不包装监听逻辑。
88
+
89
+ **错误隔离**:`onInit` / `onStateChange` / `onSubscribe` / `filter` 抛错时交给 `onError`,并用 `source` 标识来源;`onError` 自身抛错会被吞掉。业务中间件或订阅者抛出的异常仍向调用者传播,不会被包装成遥测错误。
90
+
91
+ **销毁**:`onDestroy` 把 `changeCount` 与 `totalSubscriptions` 归零,因此同一个中间件实例接入新 Store 时计数从 1 重新开始。销毁前的元数据不会被自动上报。
92
+
93
+ **快照与异步**:本包不克隆 `prevState` / `nextState`,二者是引用;只拦截每次同步 `setState` 写入,异步 action 在 `await` 后写入同样形成一次更新事件。不发网络请求、不落盘。
94
+
95
+ ## 自动导入
96
+
97
+ `createAutoImportApis(prefix?)` 通过 `@slux/shared` 的 `generateBatchAutoImports` 生成 `unplugin-auto-import` 的 `ImportsMap`,来源名为 `@slux/telemetry`。登记清单以 `src/auto-import-api.ts` 为准,只有 1 项:`telemetry`。
98
+
99
+ ```ts
100
+ import AutoImport from "unplugin-auto-import/vite";
101
+ import { createAutoImportApis } from "@slux/telemetry";
102
+
103
+ export default {
104
+ plugins: [AutoImport({ imports: [createAutoImportApis()] })],
105
+ };
106
+ ```
107
+
108
+ 传入字符串前缀会为登记项生成带前缀别名(如 `createAutoImportApis("u")` 把 `telemetry` 映射为 `uTelemetry`);前缀以小写字母结尾时 API 名首字母大写,否则直接拼接。`createAutoImportApis` 本身不登记。
109
+
110
+ ## 类型导出
111
+
112
+ `src/index.ts` 实际导出的类型:`ErrorContext<T>`、`InitMetadata`、`StateChangeMetadata<T>`、`SubscriptionMetadata`、`TelemetryMiddleware<T>`、`TelemetryOptions<T>`。
113
+
114
+ `TelemetryMiddleware<T>` 含 `name`、`init`、`onSet`、`onSubscribe`、`onDestroy` 以及 `setEnabled` / `isEnabled`;其结构满足 `@slux/store` 的 `Middleware<T>`,后者由 `@slux/store` 定义,本包不重复导出。
115
+
116
+ ## 不适用的场景
117
+
118
+ - 需要精确的当前活跃订阅数:`totalSubscriptions` 是累计次数。
119
+ - 需要自动把指标发到远端或持久化:本包只调用回调,需自行上报。
120
+ - 需要状态快照的独立副本或序列化:本包不克隆,`prevState` / `nextState` 为引用。
121
+ - 需要精确的性能剖析或跨异步任务计时:`durationMs` 只覆盖同步 `onSet` 链与 `filter`。
122
+ - 需要撤销重做、持久化或跨标签同步:分别使用 `@slux/history`、`@slux/storage`、`@slux/tab-sync`。
123
+
124
+ ## 版本、许可与反馈
125
+
126
+ | 项 | 值 | 来源 |
127
+ | -------- | ------------------------------------------ | ----------------------------------------- |
128
+ | 版本 | `0.1.0` | `package.json` |
129
+ | 许可 | MIT | `package.json` / [LICENSE](../../LICENSE) |
130
+ | 问题反馈 | `https://github.com/itmanyong/slux/issues` | `package.json` `bugs` |
131
+
132
+ > `pnpm add` 基于 `package.json` 的发布配置;是否已实际发布到 npm 及线上版本号无法从源码确认。
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";const __rslib_import_meta_url__="u"<typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,t,r)=>{var _=(t,r)=>{for(var _ in t)__webpack_require__.o(t,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,[r]:t[_]})};_(t,"get"),_(r,"value")},__webpack_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),__webpack_require__.r=e=>{"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};function createTimer(){let e=globalThis.performance;return void 0!==e&&"function"==typeof e.now?e.now.bind(e):()=>Date.now()}function telemetry(e={}){let t=createTimer(),{onStateChange:r,onInit:_,onSubscribe:o,onError:a,filter:i}=e,c=!0,p=0,n=0;function s(e){try{a?.(e)}catch{}}return{name:"telemetry",init(e){if(c&&void 0!==_)try{_(e.getState(),{name:"telemetry",timestamp:new Date().toISOString()})}catch(t){s({error:t,source:"onInit",state:e.getState()})}},onSet(e,_,o){let a=t(),n=e.getState();if(_(o),p+=1,!c||void 0===r)return;let u=e.getState();if(void 0!==i)try{if(!i(n,u,o))return}catch(e){s({error:e,source:"filter",state:u});return}try{r({name:"telemetry",prevState:n,nextState:u,partial:o,timestamp:new Date().toISOString(),durationMs:t()-a,changeCount:p})}catch(e){s({error:e,source:"onStateChange",state:u})}},onSubscribe(e,t){if(n+=1,c&&void 0!==o)try{o({name:"telemetry",totalSubscriptions:n,timestamp:new Date().toISOString()})}catch(e){s({error:e,source:"onSubscribe",state:void 0})}return t},onDestroy(){p=0,n=0},setEnabled(e){c=e},isEnabled:()=>c}}__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{createAutoImportApis:()=>createAutoImportApis,telemetry:()=>telemetry});const shared_namespaceObject=require("@slux/shared"),allImportApis=["telemetry"],createAutoImportApis=(e="")=>(0,shared_namespaceObject.generateBatchAutoImports)([{prefix:e,fromName:"@slux/telemetry",imports:allImportApis}]);for(var __rspack_i in exports.createAutoImportApis=__webpack_exports__.createAutoImportApis,exports.telemetry=__webpack_exports__.telemetry,__webpack_exports__)-1===["createAutoImportApis","telemetry"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["webpack://webpack/runtime/define_property_getters","webpack://webpack/runtime/has_own_property","webpack://webpack/runtime/make_namespace_object","../src/telemetry.ts","../src/auto-import-api.ts"],"sourcesContent":["__webpack_require__.d = (exports, getters, values) => {\n\tvar define = (defs, kind) => {\n\t\tfor(var key in defs) {\n\t\t\tif(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] });\n\t\t\t}\n\t\t}\n\t};\n\tdefine(getters, \"get\");\n\tdefine(values, \"value\");\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import type { ErrorContext, TelemetryMiddleware, TelemetryOptions } from \"./types.js\";\n\nfunction createTimer(): () => number {\n const timer = (globalThis as { performance?: { now?: () => number } }).performance;\n if (timer !== undefined && typeof timer.now === \"function\") return timer.now.bind(timer);\n return () => Date.now();\n}\n\nexport function telemetry<T>(options: TelemetryOptions<T> = {}): TelemetryMiddleware<T> {\n const now = createTimer();\n const { onStateChange, onInit, onSubscribe, onError, filter } = options;\n let enabled = true;\n let changeCount = 0;\n let totalSubscriptions = 0;\n\n function reportError(context: ErrorContext<T>): void {\n try {\n onError?.(context);\n } catch {\n // 遥测自身的错误处理失败不能影响业务状态。\n }\n }\n\n return {\n name: \"telemetry\",\n init(api) {\n if (!enabled || onInit === undefined) return;\n try {\n onInit(api.getState(), { name: \"telemetry\", timestamp: new Date().toISOString() });\n } catch (error) {\n reportError({ error, source: \"onInit\", state: api.getState() });\n }\n },\n onSet(api, next, partial) {\n const start = now();\n const prevState = api.getState();\n // next 的业务异常必须继续向调用者传播,不能作为遥测错误吞掉。\n next(partial);\n changeCount += 1;\n if (!enabled || onStateChange === undefined) return;\n const nextState = api.getState();\n if (filter !== undefined) {\n try {\n if (!filter(prevState, nextState, partial)) return;\n } catch (error) {\n reportError({ error, source: \"filter\", state: nextState });\n return;\n }\n }\n try {\n onStateChange({\n name: \"telemetry\",\n prevState,\n nextState,\n partial,\n timestamp: new Date().toISOString(),\n durationMs: now() - start,\n changeCount,\n });\n } catch (error) {\n reportError({ error, source: \"onStateChange\", state: nextState });\n }\n },\n onSubscribe(_api, listener) {\n totalSubscriptions += 1;\n if (enabled && onSubscribe !== undefined) {\n try {\n onSubscribe({ name: \"telemetry\", totalSubscriptions, timestamp: new Date().toISOString() });\n } catch (error) {\n reportError({ error, source: \"onSubscribe\", state: undefined });\n }\n }\n return listener;\n },\n onDestroy() {\n changeCount = 0;\n totalSubscriptions = 0;\n },\n setEnabled(value) {\n enabled = value;\n },\n isEnabled() {\n return enabled;\n },\n };\n}\n","import { generateBatchAutoImports } from \"@slux/shared\";\n\nconst allImportApis: string[] = [\"telemetry\"];\n\nexport const createAutoImportApis = (prefix: string = \"\") => {\n return generateBatchAutoImports([\n {\n prefix,\n fromName: \"@slux/telemetry\",\n imports: allImportApis,\n },\n ]);\n};\n"],"names":["__webpack_require__","e","Object","Symbol","createTimer","timer","globalThis","undefined","Date","telemetry","options","now","onStateChange","onInit","onSubscribe","onError","filter","enabled","changeCount","totalSubscriptions","reportError","context","api","error","next","partial","start","prevState","nextState","_api","listener","value","allImportApis","createAutoImportApis","prefix","generateBatchAutoImports"],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { Listener } from '@slux/store';
2
+ import type { MiddlewareAPI } from '@slux/store';
3
+ import type { SetFn } from '@slux/store';
4
+
5
+ export declare const createAutoImportApis: (prefix?: string) => {
6
+ [x: string]: (string | [string, string])[];
7
+ };
8
+
9
+ export declare type ErrorContext<T> = {
10
+ readonly error: unknown;
11
+ readonly source: "onStateChange" | "onInit" | "onSubscribe" | "filter";
12
+ /** onSubscribe 错误没有状态快照。 */
13
+ readonly state: T | undefined;
14
+ };
15
+
16
+ export declare type InitMetadata = {
17
+ readonly name: string;
18
+ readonly timestamp: string;
19
+ };
20
+
21
+ export declare type StateChangeMetadata<T> = {
22
+ readonly name: string;
23
+ readonly prevState: T;
24
+ readonly nextState: T;
25
+ readonly partial: Partial<T>;
26
+ readonly timestamp: string;
27
+ /** 下游更新与 filter 执行耗时,不包含上报回调自身。 */
28
+ readonly durationMs: number;
29
+ /** 包含被过滤和暂停期间的更新;销毁后归零。 */
30
+ readonly changeCount: number;
31
+ };
32
+
33
+ export declare type SubscriptionMetadata = {
34
+ readonly name: string;
35
+ /** 累计订阅次数,不是当前活跃订阅数。 */
36
+ readonly totalSubscriptions: number;
37
+ readonly timestamp: string;
38
+ };
39
+
40
+ export declare function telemetry<T>(options?: TelemetryOptions<T>): TelemetryMiddleware<T>;
41
+
42
+ export declare type TelemetryMiddleware<T> = {
43
+ readonly name: string;
44
+ readonly init: (api: MiddlewareAPI<T>) => void;
45
+ readonly onSet: (api: MiddlewareAPI<T>, next: SetFn<T>, partial: Partial<T>) => void;
46
+ readonly onSubscribe: (api: MiddlewareAPI<T>, listener: Listener<T>) => Listener<T>;
47
+ readonly onDestroy: (api: MiddlewareAPI<T>) => void;
48
+ setEnabled(enabled: boolean): void;
49
+ isEnabled(): boolean;
50
+ };
51
+
52
+ export declare type TelemetryOptions<T> = {
53
+ readonly onStateChange?: (metadata: StateChangeMetadata<T>) => void;
54
+ readonly onInit?: (initialState: T, metadata: InitMetadata) => void;
55
+ readonly onSubscribe?: (metadata: SubscriptionMetadata) => void;
56
+ readonly onError?: (context: ErrorContext<T>) => void;
57
+ readonly filter?: (prevState: T, nextState: T, partial: Partial<T>) => boolean;
58
+ };
59
+
60
+ export { }
@@ -0,0 +1,60 @@
1
+ import type { Listener } from '@slux/store';
2
+ import type { MiddlewareAPI } from '@slux/store';
3
+ import type { SetFn } from '@slux/store';
4
+
5
+ export declare const createAutoImportApis: (prefix?: string) => {
6
+ [x: string]: (string | [string, string])[];
7
+ };
8
+
9
+ export declare type ErrorContext<T> = {
10
+ readonly error: unknown;
11
+ readonly source: "onStateChange" | "onInit" | "onSubscribe" | "filter";
12
+ /** onSubscribe 错误没有状态快照。 */
13
+ readonly state: T | undefined;
14
+ };
15
+
16
+ export declare type InitMetadata = {
17
+ readonly name: string;
18
+ readonly timestamp: string;
19
+ };
20
+
21
+ export declare type StateChangeMetadata<T> = {
22
+ readonly name: string;
23
+ readonly prevState: T;
24
+ readonly nextState: T;
25
+ readonly partial: Partial<T>;
26
+ readonly timestamp: string;
27
+ /** 下游更新与 filter 执行耗时,不包含上报回调自身。 */
28
+ readonly durationMs: number;
29
+ /** 包含被过滤和暂停期间的更新;销毁后归零。 */
30
+ readonly changeCount: number;
31
+ };
32
+
33
+ export declare type SubscriptionMetadata = {
34
+ readonly name: string;
35
+ /** 累计订阅次数,不是当前活跃订阅数。 */
36
+ readonly totalSubscriptions: number;
37
+ readonly timestamp: string;
38
+ };
39
+
40
+ export declare function telemetry<T>(options?: TelemetryOptions<T>): TelemetryMiddleware<T>;
41
+
42
+ export declare type TelemetryMiddleware<T> = {
43
+ readonly name: string;
44
+ readonly init: (api: MiddlewareAPI<T>) => void;
45
+ readonly onSet: (api: MiddlewareAPI<T>, next: SetFn<T>, partial: Partial<T>) => void;
46
+ readonly onSubscribe: (api: MiddlewareAPI<T>, listener: Listener<T>) => Listener<T>;
47
+ readonly onDestroy: (api: MiddlewareAPI<T>) => void;
48
+ setEnabled(enabled: boolean): void;
49
+ isEnabled(): boolean;
50
+ };
51
+
52
+ export declare type TelemetryOptions<T> = {
53
+ readonly onStateChange?: (metadata: StateChangeMetadata<T>) => void;
54
+ readonly onInit?: (initialState: T, metadata: InitMetadata) => void;
55
+ readonly onSubscribe?: (metadata: SubscriptionMetadata) => void;
56
+ readonly onError?: (context: ErrorContext<T>) => void;
57
+ readonly filter?: (prevState: T, nextState: T, partial: Partial<T>) => boolean;
58
+ };
59
+
60
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{generateBatchAutoImports as t}from"@slux/shared";function e(t={}){let r,o=void 0!==(r=globalThis.performance)&&"function"==typeof r.now?r.now.bind(r):()=>Date.now(),{onStateChange:n,onInit:a,onSubscribe:i,onError:m,filter:s}=t,c=!0,l=0,u=0;function y(t){try{m?.(t)}catch{}}return{name:"telemetry",init(t){if(c&&void 0!==a)try{a(t.getState(),{name:"telemetry",timestamp:new Date().toISOString()})}catch(e){y({error:e,source:"onInit",state:t.getState()})}},onSet(t,e,r){let a=o(),i=t.getState();if(e(r),l+=1,!c||void 0===n)return;let m=t.getState();if(void 0!==s)try{if(!s(i,m,r))return}catch(t){y({error:t,source:"filter",state:m});return}try{n({name:"telemetry",prevState:i,nextState:m,partial:r,timestamp:new Date().toISOString(),durationMs:o()-a,changeCount:l})}catch(t){y({error:t,source:"onStateChange",state:m})}},onSubscribe(t,e){if(u+=1,c&&void 0!==i)try{i({name:"telemetry",totalSubscriptions:u,timestamp:new Date().toISOString()})}catch(t){y({error:t,source:"onSubscribe",state:void 0})}return e},onDestroy(){l=0,u=0},setEnabled(t){c=t},isEnabled:()=>c}}let r=["telemetry"],o=(e="")=>t([{prefix:e,fromName:"@slux/telemetry",imports:r}]);export{o as createAutoImportApis,e as telemetry};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/telemetry.ts","../src/auto-import-api.ts"],"sourcesContent":["import type { ErrorContext, TelemetryMiddleware, TelemetryOptions } from \"./types.js\";\n\nfunction createTimer(): () => number {\n const timer = (globalThis as { performance?: { now?: () => number } }).performance;\n if (timer !== undefined && typeof timer.now === \"function\") return timer.now.bind(timer);\n return () => Date.now();\n}\n\nexport function telemetry<T>(options: TelemetryOptions<T> = {}): TelemetryMiddleware<T> {\n const now = createTimer();\n const { onStateChange, onInit, onSubscribe, onError, filter } = options;\n let enabled = true;\n let changeCount = 0;\n let totalSubscriptions = 0;\n\n function reportError(context: ErrorContext<T>): void {\n try {\n onError?.(context);\n } catch {\n // 遥测自身的错误处理失败不能影响业务状态。\n }\n }\n\n return {\n name: \"telemetry\",\n init(api) {\n if (!enabled || onInit === undefined) return;\n try {\n onInit(api.getState(), { name: \"telemetry\", timestamp: new Date().toISOString() });\n } catch (error) {\n reportError({ error, source: \"onInit\", state: api.getState() });\n }\n },\n onSet(api, next, partial) {\n const start = now();\n const prevState = api.getState();\n // next 的业务异常必须继续向调用者传播,不能作为遥测错误吞掉。\n next(partial);\n changeCount += 1;\n if (!enabled || onStateChange === undefined) return;\n const nextState = api.getState();\n if (filter !== undefined) {\n try {\n if (!filter(prevState, nextState, partial)) return;\n } catch (error) {\n reportError({ error, source: \"filter\", state: nextState });\n return;\n }\n }\n try {\n onStateChange({\n name: \"telemetry\",\n prevState,\n nextState,\n partial,\n timestamp: new Date().toISOString(),\n durationMs: now() - start,\n changeCount,\n });\n } catch (error) {\n reportError({ error, source: \"onStateChange\", state: nextState });\n }\n },\n onSubscribe(_api, listener) {\n totalSubscriptions += 1;\n if (enabled && onSubscribe !== undefined) {\n try {\n onSubscribe({ name: \"telemetry\", totalSubscriptions, timestamp: new Date().toISOString() });\n } catch (error) {\n reportError({ error, source: \"onSubscribe\", state: undefined });\n }\n }\n return listener;\n },\n onDestroy() {\n changeCount = 0;\n totalSubscriptions = 0;\n },\n setEnabled(value) {\n enabled = value;\n },\n isEnabled() {\n return enabled;\n },\n };\n}\n","import { generateBatchAutoImports } from \"@slux/shared\";\n\nconst allImportApis: string[] = [\"telemetry\"];\n\nexport const createAutoImportApis = (prefix: string = \"\") => {\n return generateBatchAutoImports([\n {\n prefix,\n fromName: \"@slux/telemetry\",\n imports: allImportApis,\n },\n ]);\n};\n"],"names":["telemetry","options","timer","now","undefined","globalThis","Date","onStateChange","onInit","onSubscribe","onError","filter","enabled","changeCount","totalSubscriptions","reportError","context","api","error","next","partial","start","prevState","nextState","_api","listener","value","allImportApis","createAutoImportApis","prefix","generateBatchAutoImports"],"mappings":"wDAQO,SAASA,EAAaC,EAA+B,CAAC,CAAC,EAC5D,IANMC,EAMAC,EALFD,AAAUE,UADRF,EAASG,WAAwD,WAAW,GACvD,AAAqB,YAArB,OAAOH,EAAM,GAAG,CAAwBA,EAAM,GAAG,CAAC,IAAI,CAACA,GAC3E,IAAMI,KAAK,GAAG,GAKf,CAAEC,cAAAA,CAAa,CAAEC,OAAAA,CAAM,CAAEC,YAAAA,CAAW,CAAEC,QAAAA,CAAO,CAAEC,OAAAA,CAAM,CAAE,CAAGV,EAC5DW,EAAU,GACVC,EAAc,EACdC,EAAqB,EAEzB,SAASC,EAAYC,CAAwB,EAC3C,GAAI,CACFN,IAAUM,EACZ,CAAE,KAAM,CAER,CACF,CAEA,MAAO,CACL,KAAM,YACN,KAAKC,CAAG,EACN,GAAI,AAACL,GAAWJ,AAAWJ,SAAXI,EAChB,GAAI,CACFA,EAAOS,EAAI,QAAQ,GAAI,CAAE,KAAM,YAAa,UAAW,IAAIX,OAAO,WAAW,EAAG,EAClF,CAAE,MAAOY,EAAO,CACdH,EAAY,CAAEG,MAAAA,EAAO,OAAQ,SAAU,MAAOD,EAAI,QAAQ,EAAG,EAC/D,CACF,EACA,MAAMA,CAAG,CAAEE,CAAI,CAAEC,CAAO,EACtB,IAAMC,EAAQlB,IACRmB,EAAYL,EAAI,QAAQ,GAI9B,GAFAE,EAAKC,GACLP,GAAe,EACX,CAACD,GAAWL,AAAkBH,SAAlBG,EAA6B,OAC7C,IAAMgB,EAAYN,EAAI,QAAQ,GAC9B,GAAIN,AAAWP,SAAXO,EACF,GAAI,CACF,GAAI,CAACA,EAAOW,EAAWC,EAAWH,GAAU,MAC9C,CAAE,MAAOF,EAAO,CACdH,EAAY,CAAEG,MAAAA,EAAO,OAAQ,SAAU,MAAOK,CAAU,GACxD,MACF,CAEF,GAAI,CACFhB,EAAc,CACZ,KAAM,YACNe,UAAAA,EACAC,UAAAA,EACAH,QAAAA,EACA,UAAW,IAAId,OAAO,WAAW,GACjC,WAAYH,IAAQkB,EACpBR,YAAAA,CACF,EACF,CAAE,MAAOK,EAAO,CACdH,EAAY,CAAEG,MAAAA,EAAO,OAAQ,gBAAiB,MAAOK,CAAU,EACjE,CACF,EACA,YAAYC,CAAI,CAAEC,CAAQ,EAExB,GADAX,GAAsB,EAClBF,GAAWH,AAAgBL,SAAhBK,EACb,GAAI,CACFA,EAAY,CAAE,KAAM,YAAaK,mBAAAA,EAAoB,UAAW,IAAIR,OAAO,WAAW,EAAG,EAC3F,CAAE,MAAOY,EAAO,CACdH,EAAY,CAAEG,MAAAA,EAAO,OAAQ,cAAe,MAAOd,MAAU,EAC/D,CAEF,OAAOqB,CACT,EACA,YACEZ,EAAc,EACdC,EAAqB,CACvB,EACA,WAAWY,CAAK,EACdd,EAAUc,CACZ,EACA,cACSd,CAEX,CACF,CCnFA,IAAMe,EAA0B,CAAC,YAAY,CAEhCC,EAAuB,CAACC,EAAiB,EAAE,GAC/CC,EAAyB,CAC9B,CACED,OAAAA,EACA,SAAU,kBACV,QAASF,CACX,EACD,S"}
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@slux/telemetry",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Store 状态变化与订阅遥测中间件",
5
+ "sideEffects": false,
6
+ "keywords": [
7
+ "slux",
8
+ "telemetry",
9
+ "middleware",
10
+ "metrics"
11
+ ],
12
+ "author": "itmanyong <itmanyong@gmail.com>",
13
+ "contributors": [
14
+ "itmanyong <itmanyong@gmail.com>"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "license": "MIT",
20
+ "homepage": "https://www.npmjs.com/package/@slux/telemetry",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/itmanyong/slux.git",
24
+ "directory": "packages/telemetry"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/itmanyong/slux/issues"
28
+ },
29
+ "type": "module",
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "CHANGELOG.md",
34
+ "package.json"
35
+ ],
36
+ "main": "./dist/index.cjs",
37
+ "module": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "import": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/index.d.cts",
47
+ "default": "./dist/index.cjs"
48
+ }
49
+ },
50
+ "./package.json": "./package.json"
51
+ },
52
+ "dependencies": {
53
+ "@slux/store": "0.1.0",
54
+ "@slux/shared": "0.1.0"
55
+ },
56
+ "devDependencies": {
57
+ "rstack": "0.9.0",
58
+ "typescript": "7.1.0-beta"
59
+ },
60
+ "scripts": {
61
+ "dev": "rs lib -w",
62
+ "build": "rs lib",
63
+ "test": "rs test",
64
+ "lint": "rs lint",
65
+ "fmt": "rs fmt",
66
+ "typecheck": "tsc --noEmit",
67
+ "clean": "rm -rf dist node_modules .turbo"
68
+ }
6
69
  }