@dshtrading/dsh-home 0.5.0 → 0.6.1
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/lib/file-lock.d.ts +17 -0
- package/lib/file-lock.js +100 -0
- package/lib/index.d.ts +12 -2
- package/lib/index.js +23 -3
- package/lib/store-transaction.d.ts +26 -0
- package/lib/store-transaction.js +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/file-lock.d.ts
|
|
2
|
+
interface HomeFileLockOptions {
|
|
3
|
+
/** 等锁上限(毫秒);超时抛错,不静默降级。 */
|
|
4
|
+
readonly timeoutMs?: number;
|
|
5
|
+
/** 锁文件 mtime 超过该毫秒数视为陈旧(持锁者已崩溃),可被后来者回收。 */
|
|
6
|
+
readonly staleMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/** 等锁超时:本次调用按「明确失败」处理,磁盘上的数据保持持锁者的版本。 */
|
|
9
|
+
declare class HomeFileLockTimeoutError extends Error {
|
|
10
|
+
readonly lockPath: string;
|
|
11
|
+
readonly timeoutMs: number;
|
|
12
|
+
constructor(lockPath: string, timeoutMs: number);
|
|
13
|
+
}
|
|
14
|
+
/** 在跨进程锁内执行 work;无论成功失败都释放锁(进程崩溃时由陈旧阈值兜底)。 */
|
|
15
|
+
declare function withHomeFileLock<T>(filePath: string, work: () => Promise<T>, options?: HomeFileLockOptions): Promise<T>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { HomeFileLockOptions, HomeFileLockTimeoutError, withHomeFileLock };
|
package/lib/file-lock.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { mkdir, open, rename, stat, unlink, utimes } from "node:fs/promises";
|
|
4
|
+
//#region src/file-lock.ts
|
|
5
|
+
/**
|
|
6
|
+
* home 数据文件的跨进程排他锁(2026-10-09 事故「多进程整表回写互相覆盖」的机制基座)。
|
|
7
|
+
*
|
|
8
|
+
* 为什么不是 flock/fcntl:Node 无内建 flock,Windows 与网络盘语义不一致;锁文件
|
|
9
|
+
* (O_EXCL 创建 + pid/host/心跳 mtime)三平台一致,且持锁进程崩溃后可由后来者按
|
|
10
|
+
* 陈旧阈值回收——不依赖持锁者自己清理。
|
|
11
|
+
*
|
|
12
|
+
* 语义:进入临界区前必得锁;拿不到时等待(缺省 5s),仍拿不到抛
|
|
13
|
+
* {@link HomeFileLockTimeoutError}——「明确告知失败」这条出路,绝不静默按陈旧
|
|
14
|
+
* 快照写盘。持锁期间的临界区应只包住「读盘 + 合并 + 原子写」这一小段。
|
|
15
|
+
*/
|
|
16
|
+
/** 等锁超时:本次调用按「明确失败」处理,磁盘上的数据保持持锁者的版本。 */
|
|
17
|
+
var HomeFileLockTimeoutError = class extends Error {
|
|
18
|
+
lockPath;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
constructor(lockPath, timeoutMs) {
|
|
21
|
+
super(`waiting for home file lock ${lockPath} timed out after ${timeoutMs}ms (another process holds it; this process wrote nothing)`);
|
|
22
|
+
this.name = "HomeFileLockTimeoutError";
|
|
23
|
+
this.lockPath = lockPath;
|
|
24
|
+
this.timeoutMs = timeoutMs;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
28
|
+
const DEFAULT_STALE_MS = 3e4;
|
|
29
|
+
const RETRY_MS = 15;
|
|
30
|
+
const delay = (ms) => new Promise((resolve) => {
|
|
31
|
+
setTimeout(resolve, ms);
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* 陈旧锁回收:返回 true 表示可立刻重试拿锁(锁已不在,或已被本次回收)。
|
|
35
|
+
*
|
|
36
|
+
* 抢占用 rename 做原子裁决(同一陈旧 inode 只可能被一个进程搬走);搬走后复核
|
|
37
|
+
* mtime,若发现抢到的是别人刚重建的新锁(极窄窗口),原样搬回,避免误删活锁。
|
|
38
|
+
*/
|
|
39
|
+
async function reclaimIfStale(lockPath, staleMs) {
|
|
40
|
+
let ageMs;
|
|
41
|
+
try {
|
|
42
|
+
ageMs = Date.now() - (await stat(lockPath)).mtimeMs;
|
|
43
|
+
} catch {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (ageMs <= staleMs) return false;
|
|
47
|
+
const claimPath = `${lockPath}.stale-${process.pid}-${Date.now()}`;
|
|
48
|
+
try {
|
|
49
|
+
await rename(lockPath, claimPath);
|
|
50
|
+
} catch {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const claimed = await stat(claimPath).catch(() => void 0);
|
|
54
|
+
if (claimed !== void 0 && Date.now() - claimed.mtimeMs <= staleMs) {
|
|
55
|
+
await rename(claimPath, lockPath).catch(() => unlink(claimPath).catch(() => {}));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
await unlink(claimPath).catch(() => {});
|
|
59
|
+
console.warn(`[dsh-home] reclaimed stale home file lock ${lockPath} (mtime older than ${staleMs}ms; the holding process likely crashed)`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** 在跨进程锁内执行 work;无论成功失败都释放锁(进程崩溃时由陈旧阈值兜底)。 */
|
|
63
|
+
async function withHomeFileLock(filePath, work, options = {}) {
|
|
64
|
+
const lockPath = `${filePath}.lock`;
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
66
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
67
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
let handle;
|
|
70
|
+
while (handle === void 0) {
|
|
71
|
+
try {
|
|
72
|
+
handle = await open(lockPath, "wx");
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code !== "EEXIST") throw error;
|
|
75
|
+
}
|
|
76
|
+
if (handle !== void 0) break;
|
|
77
|
+
if (Date.now() >= deadline) throw new HomeFileLockTimeoutError(lockPath, timeoutMs);
|
|
78
|
+
if (await reclaimIfStale(lockPath, staleMs)) continue;
|
|
79
|
+
await delay(RETRY_MS);
|
|
80
|
+
}
|
|
81
|
+
const heartbeat = setInterval(() => {
|
|
82
|
+
const now = /* @__PURE__ */ new Date();
|
|
83
|
+
utimes(lockPath, now, now).catch(() => {});
|
|
84
|
+
}, Math.max(1e3, Math.floor(staleMs / 3)));
|
|
85
|
+
heartbeat.unref?.();
|
|
86
|
+
try {
|
|
87
|
+
await handle.writeFile(JSON.stringify({
|
|
88
|
+
pid: process.pid,
|
|
89
|
+
host: hostname(),
|
|
90
|
+
at: Date.now()
|
|
91
|
+
}), "utf8");
|
|
92
|
+
return await work();
|
|
93
|
+
} finally {
|
|
94
|
+
clearInterval(heartbeat);
|
|
95
|
+
await handle.close().catch(() => {});
|
|
96
|
+
await unlink(lockPath).catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { HomeFileLockTimeoutError, withHomeFileLock };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { writeJsonAtomic } from "./fs-atomic.js";
|
|
2
|
+
import { HomeFileLockOptions, HomeFileLockTimeoutError, withHomeFileLock } from "./file-lock.js";
|
|
3
|
+
import { SKIP_WRITE, SkipWrite, TransactStoreOptions, transactStore } from "./store-transaction.js";
|
|
2
4
|
//#region src/index.d.ts
|
|
3
5
|
/**
|
|
4
6
|
* 解析 dsh home 根目录。
|
|
5
7
|
* @param env 环境变量映射,缺省读 `process.env`;空白 `$DSH_HOME` 视为未设。
|
|
8
|
+
* @param options.warn 告警出口(缺省写 stderr;测试注入以便断言)。
|
|
9
|
+
*
|
|
10
|
+
* **告警(不改返回值)**:解析结果落在缺省 home(`~/.dsh`)而 `~/.dsh-trading` 存在时,打一行告警。
|
|
11
|
+
* 场景是 2026-10-01 实测过的:agent 会话继承宿主的 `DSH_HOME=~/.dsh`,于是交易数据被解析到宿主 home ——
|
|
12
|
+
* 而这一切**不会报错**,脚本照常"成功",只是作用在另一个实例的数据上。
|
|
13
|
+
* 这一行只把"静默跑错 home"变成"响亮但可忽略":**返回值、优先级、语义全不变**(改默认值属于架构决策,另议)。
|
|
6
14
|
*/
|
|
7
|
-
declare function dshHomeDir(env?: Record<string, string | undefined
|
|
15
|
+
declare function dshHomeDir(env?: Record<string, string | undefined>, options?: {
|
|
16
|
+
readonly warn?: ((message: string) => void) | undefined;
|
|
17
|
+
}): string;
|
|
8
18
|
/** 缺省 home(`~/.dsh`)——迁移判据用:解析结果等于它就没有旧 home 可迁。 */
|
|
9
19
|
declare function defaultDshHome(): string;
|
|
10
20
|
/**
|
|
@@ -51,4 +61,4 @@ interface LegacyHomeMigrationResult {
|
|
|
51
61
|
*/
|
|
52
62
|
declare function migrateLegacyTradingHome(options?: LegacyHomeMigrationOptions): LegacyHomeMigrationResult | null;
|
|
53
63
|
//#endregion
|
|
54
|
-
export { HOME_MIGRATION_MARKER, LegacyHomeMigrationOptions, LegacyHomeMigrationResult, TRADING_HOME_ENTRIES, defaultDshHome, dshHomeDir, migrateLegacyTradingHome, writeJsonAtomic };
|
|
64
|
+
export { HOME_MIGRATION_MARKER, type HomeFileLockOptions, HomeFileLockTimeoutError, LegacyHomeMigrationOptions, LegacyHomeMigrationResult, SKIP_WRITE, type SkipWrite, TRADING_HOME_ENTRIES, type TransactStoreOptions, defaultDshHome, dshHomeDir, migrateLegacyTradingHome, transactStore, withHomeFileLock, writeJsonAtomic };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { writeJsonAtomic } from "./fs-atomic.js";
|
|
2
|
+
import { HomeFileLockTimeoutError, withHomeFileLock } from "./file-lock.js";
|
|
3
|
+
import { SKIP_WRITE, transactStore } from "./store-transaction.js";
|
|
2
4
|
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
5
|
import { homedir } from "node:os";
|
|
4
6
|
import { join, resolve } from "node:path";
|
|
@@ -14,6 +16,16 @@ import { join, resolve } from "node:path";
|
|
|
14
16
|
* (2026-09-08 DSH_HOME 分离,note: implemented/process/2026-09-08-separate-dsh-home.md)。
|
|
15
17
|
*/
|
|
16
18
|
const DEFAULT_HOME_DIR_NAME = ".dsh";
|
|
19
|
+
/** 交易 home 的目录名(与 CLI wrapper / 各脚本的约定一致)。 */
|
|
20
|
+
const TRADING_HOME_DIR_NAME = ".dsh-trading";
|
|
21
|
+
/** stderr 去重标记(**去重只发生在默认出口**:注入的 warn 每次都调,测试才确定)。 */
|
|
22
|
+
let warnedToStderr = false;
|
|
23
|
+
/** 默认告警出口:写 stderr,且进程内只写一次(脏日志比没有日志更糟)。 */
|
|
24
|
+
function warnOnceToStderr(message) {
|
|
25
|
+
if (warnedToStderr) return;
|
|
26
|
+
warnedToStderr = true;
|
|
27
|
+
process.stderr.write(message + String.fromCharCode(10));
|
|
28
|
+
}
|
|
17
29
|
/** 展开 `~` 与 `~/` 前缀(对齐宿主 expandHomePath;Windows 反斜杠前缀同样接受)。 */
|
|
18
30
|
function expandHomePath(path) {
|
|
19
31
|
const home = homedir();
|
|
@@ -24,11 +36,19 @@ function expandHomePath(path) {
|
|
|
24
36
|
/**
|
|
25
37
|
* 解析 dsh home 根目录。
|
|
26
38
|
* @param env 环境变量映射,缺省读 `process.env`;空白 `$DSH_HOME` 视为未设。
|
|
39
|
+
* @param options.warn 告警出口(缺省写 stderr;测试注入以便断言)。
|
|
40
|
+
*
|
|
41
|
+
* **告警(不改返回值)**:解析结果落在缺省 home(`~/.dsh`)而 `~/.dsh-trading` 存在时,打一行告警。
|
|
42
|
+
* 场景是 2026-10-01 实测过的:agent 会话继承宿主的 `DSH_HOME=~/.dsh`,于是交易数据被解析到宿主 home ——
|
|
43
|
+
* 而这一切**不会报错**,脚本照常"成功",只是作用在另一个实例的数据上。
|
|
44
|
+
* 这一行只把"静默跑错 home"变成"响亮但可忽略":**返回值、优先级、语义全不变**(改默认值属于架构决策,另议)。
|
|
27
45
|
*/
|
|
28
|
-
function dshHomeDir(env = process.env) {
|
|
46
|
+
function dshHomeDir(env = process.env, options = {}) {
|
|
29
47
|
const fromEnv = env.DSH_HOME;
|
|
30
48
|
const configured = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : resolve(homedir(), DEFAULT_HOME_DIR_NAME);
|
|
31
|
-
|
|
49
|
+
const resolvedHome = resolve(expandHomePath(configured));
|
|
50
|
+
if (resolvedHome === defaultDshHome() && existsSync(resolve(homedir(), TRADING_HOME_DIR_NAME))) (options.warn ?? warnOnceToStderr)("[dsh-home] 解析到缺省 home " + resolvedHome + ",而本机存在 " + resolve(homedir(), TRADING_HOME_DIR_NAME) + ":交易数据可能正被写到宿主 home。若这不是本意,请显式设置 DSH_HOME=" + resolve(homedir(), TRADING_HOME_DIR_NAME) + "。(本告警不影响返回值)");
|
|
51
|
+
return resolvedHome;
|
|
32
52
|
}
|
|
33
53
|
/** 缺省 home(`~/.dsh`)——迁移判据用:解析结果等于它就没有旧 home 可迁。 */
|
|
34
54
|
function defaultDshHome() {
|
|
@@ -121,4 +141,4 @@ function migrateLegacyTradingHome(options = {}) {
|
|
|
121
141
|
};
|
|
122
142
|
}
|
|
123
143
|
//#endregion
|
|
124
|
-
export { HOME_MIGRATION_MARKER, TRADING_HOME_ENTRIES, defaultDshHome, dshHomeDir, migrateLegacyTradingHome, writeJsonAtomic };
|
|
144
|
+
export { HOME_MIGRATION_MARKER, HomeFileLockTimeoutError, SKIP_WRITE, TRADING_HOME_ENTRIES, defaultDshHome, dshHomeDir, migrateLegacyTradingHome, transactStore, withHomeFileLock, writeJsonAtomic };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/store-transaction.d.ts
|
|
2
|
+
/** mutate 返回它表示「本次无内容变化」:不落盘,但缓存仍取锁内新鲜值。 */
|
|
3
|
+
declare const SKIP_WRITE: unique symbol;
|
|
4
|
+
/** {@link SKIP_WRITE} 的类型形态:写 mutate 签名时用它标注「可能无变化」。 */
|
|
5
|
+
type SkipWrite = typeof SKIP_WRITE;
|
|
6
|
+
interface TransactStoreOptions {
|
|
7
|
+
/** 等锁上限(毫秒);超时抛 {@link HomeFileLockTimeoutError}。 */
|
|
8
|
+
readonly timeoutMs?: number;
|
|
9
|
+
/** 陈旧锁回收阈值(毫秒)。 */
|
|
10
|
+
readonly staleMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 在跨进程锁内读-改-写一份 JSON 表。
|
|
14
|
+
*
|
|
15
|
+
* @param filePath - 目标 JSON 文件
|
|
16
|
+
* @param readFromDisk - **新鲜**读盘(不得回传内存缓存);由各 store 提供 parse
|
|
17
|
+
* 感知的读取,使坏形/坏 JSON 的降级口径与自身 load() 一致
|
|
18
|
+
* @param mutate - 在锁内新鲜值上做本次修改;返回新内存值,或 {@link SKIP_WRITE}
|
|
19
|
+
* 表示无变化不落盘
|
|
20
|
+
* @param serialize - 新内存值 → 落盘形状(数组、按 id 为键的对象、容器包裹皆可)
|
|
21
|
+
* @param logPrefix - 失败日志前缀,与各 store 原 writeJsonAtomic 调用逐字一致
|
|
22
|
+
* @returns 落盘后的当前值(SKIP_WRITE 时即锁内新鲜值)
|
|
23
|
+
*/
|
|
24
|
+
declare function transactStore<T>(filePath: string, readFromDisk: () => Promise<T>, mutate: (onDisk: T) => T | SkipWrite | Promise<T | SkipWrite>, serialize: (value: T) => unknown, logPrefix: string, options?: TransactStoreOptions): Promise<T>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { SKIP_WRITE, SkipWrite, TransactStoreOptions, transactStore };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { writeJsonAtomic } from "./fs-atomic.js";
|
|
2
|
+
import { withHomeFileLock } from "./file-lock.js";
|
|
3
|
+
//#region src/store-transaction.ts
|
|
4
|
+
/**
|
|
5
|
+
* 整表 JSON 的跨进程「锁内读 - 改 - 原子写」(2026-10-09 事故修复的共享入口)。
|
|
6
|
+
*
|
|
7
|
+
* 背景:下列 store 都是「进程内内存缓存 + 整表回写」,两个宿主进程共用同一
|
|
8
|
+
* DSH_HOME 时后写者用自己那份陈旧快照覆盖先写者(last-writer-wins,静默丢数据;
|
|
9
|
+
* 实测抹掉 indicators/chart.json 的 symbolParams 与 hiddenScopes)。
|
|
10
|
+
*
|
|
11
|
+
* {@link transactStore} 是全仓唯一的跨进程临界区实现,各 store 只调它,不各自复制:
|
|
12
|
+
* 1. 取该文件的跨进程排他锁({@link withHomeFileLock});
|
|
13
|
+
* 2. 锁内**新鲜重读磁盘**(readFromDisk,不是进临界区前的缓存);
|
|
14
|
+
* 3. mutate 在这份新鲜值上做本次修改并返回新的内存值;
|
|
15
|
+
* 4. tmp+rename 原子写落盘,返回值即调用方应缓存的当前值。
|
|
16
|
+
*
|
|
17
|
+
* 「读 - 改」整体在锁内,所以两个进程各自的**新增、删除**都不丢:后到者是在先到者的
|
|
18
|
+
* 结果上继续改,而不是用陈旧快照覆盖。这是「先读回并合并双方数据」的具体形态,
|
|
19
|
+
* 而不是「拒绝第二实例写」——本机桌面端与 CLI 长期共用同一 home,拒绝写会让第二
|
|
20
|
+
* 实例完全存不了设置。等锁超时抛 {@link HomeFileLockTimeoutError}:那是明确失败,
|
|
21
|
+
* 绝不静默按陈旧快照覆盖。
|
|
22
|
+
*
|
|
23
|
+
* 合并粒度是条目(行表按 id、嵌套表按各自键):同一条目被两个进程同时改时,后写者
|
|
24
|
+
* 在该条目上的整条值胜出(不做字段级三方合并,见 note 的 Residue)。
|
|
25
|
+
*
|
|
26
|
+
* 幂等 no-op(删除不存在的 id、重复写入同值)由调用方在 mutate 里返回
|
|
27
|
+
* {@link SKIP_WRITE} 表达——此时不落盘(保持既有「无变化不重写」语义),返回值仍是
|
|
28
|
+
* 锁内新鲜读到的值,调用方缓存照旧刷新。
|
|
29
|
+
*/
|
|
30
|
+
/** 合并入口的缺省等锁上限:比 store 内部调用更宽松,避免正常并发被误判超时。 */
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
32
|
+
/** mutate 返回它表示「本次无内容变化」:不落盘,但缓存仍取锁内新鲜值。 */
|
|
33
|
+
const SKIP_WRITE = Symbol("dsh-home:skip-write");
|
|
34
|
+
function lockOptionsOf(options) {
|
|
35
|
+
return {
|
|
36
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
37
|
+
...options.staleMs !== void 0 ? { staleMs: options.staleMs } : {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 在跨进程锁内读-改-写一份 JSON 表。
|
|
42
|
+
*
|
|
43
|
+
* @param filePath - 目标 JSON 文件
|
|
44
|
+
* @param readFromDisk - **新鲜**读盘(不得回传内存缓存);由各 store 提供 parse
|
|
45
|
+
* 感知的读取,使坏形/坏 JSON 的降级口径与自身 load() 一致
|
|
46
|
+
* @param mutate - 在锁内新鲜值上做本次修改;返回新内存值,或 {@link SKIP_WRITE}
|
|
47
|
+
* 表示无变化不落盘
|
|
48
|
+
* @param serialize - 新内存值 → 落盘形状(数组、按 id 为键的对象、容器包裹皆可)
|
|
49
|
+
* @param logPrefix - 失败日志前缀,与各 store 原 writeJsonAtomic 调用逐字一致
|
|
50
|
+
* @returns 落盘后的当前值(SKIP_WRITE 时即锁内新鲜值)
|
|
51
|
+
*/
|
|
52
|
+
async function transactStore(filePath, readFromDisk, mutate, serialize, logPrefix, options = {}) {
|
|
53
|
+
return withHomeFileLock(filePath, async () => {
|
|
54
|
+
const onDisk = await readFromDisk();
|
|
55
|
+
const next = await mutate(onDisk);
|
|
56
|
+
if (next === SKIP_WRITE) return onDisk;
|
|
57
|
+
await writeJsonAtomic(filePath, serialize(next), logPrefix);
|
|
58
|
+
return next;
|
|
59
|
+
}, lockOptionsOf(options));
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { SKIP_WRITE, transactStore };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/dsh-home",
|
|
3
3
|
"description": "dsh home 根目录解析($DSH_HOME 环境变量感知,缺省 ~/.dsh,对齐宿主 dsh-home-paths 语义)——dsh-trading 各包 home 数据文件的唯一路径来源",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|