@deepseek-ai/dsh-client-store 0.1.2-alpha.2
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +45 -0
- package/README.zh.md +45 -0
- package/lib/index.js +180 -0
- package/lib/invariant.js +25 -0
- package/lib/types/contract.d.ts +122 -0
- package/lib/types/index.d.ts +92 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
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.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/store/README.md
|
|
5
|
+
README.md: 49990551a47e81cdeeeeee0daa56b6fadb956fe6
|
|
6
|
+
README.zh.md: 97c29b9f776c06c5b8eddd382d1d3dc58e29a8d5
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Observable browser state stores with explicit snapshots, subscriptions, and lifecycle ownership."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
# @deepseek-ai/dsh-client-store
|
|
6
|
+
|
|
7
|
+
English | [中文](README.zh.md)
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
React-free observable and snapshot-store primitives shared by Client controllers and renderer adapters. The package owns synchronous and animation-frame publication, Immer-backed updates, shallow equality, and optional browser persistence; React hook construction remains in `@deepseek-ai/dsh-client-ui-renderer`. Use it when Client state must publish stable snapshots without depending on React.
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Model Experience](#model-experience)
|
|
16
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
17
|
+
- [Dev Note](#dev-note)
|
|
18
|
+
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
<a id="model-experience"></a>
|
|
22
|
+
## Model Experience
|
|
23
|
+
|
|
24
|
+
None, as this package provides browser-side state primitives and registers nothing model-facing.
|
|
25
|
+
|
|
26
|
+
#### KV Cache effect
|
|
27
|
+
|
|
28
|
+
None; the stores neither assemble nor send model requests.
|
|
29
|
+
|
|
30
|
+
## Known Limitations and Deferred Work
|
|
31
|
+
|
|
32
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
33
|
+
|
|
34
|
+
- **Persistence is browser-local** — persisted stores use JSON in `localStorage`; non-browser runtimes disable persistence, and the package provides no cross-device synchronization.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
<a id="dev-note"></a>
|
|
38
|
+
### Dev Note
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
42
|
+
|
|
43
|
+
None.
|
|
44
|
+
|
|
45
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "具有显式快照、订阅与生命周期所有权的浏览器可观察状态 store。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
# @deepseek-ai/dsh-client-store
|
|
6
|
+
|
|
7
|
+
[English](README.md) | 中文
|
|
8
|
+
|
|
9
|
+
## 概述
|
|
10
|
+
|
|
11
|
+
供 Client controller 与 renderer adapter 共用的不依赖 React 的 observable 和 snapshot-store 基础设施。本包负责同步与 animation-frame 发布、基于 Immer 的更新、浅比较和可选的浏览器持久化;React hook 的构造仍属于 `@deepseek-ai/dsh-client-ui-renderer`。当 Client 状态必须在不依赖 React 的情况下发布稳定 snapshot 时,请使用它。
|
|
12
|
+
|
|
13
|
+
## 目录
|
|
14
|
+
|
|
15
|
+
- [模型体验](#model-experience)
|
|
16
|
+
- [已知限制与暂缓事项](#known-limitations-and-deferred-work)
|
|
17
|
+
- [开发备注](#dev-note)
|
|
18
|
+
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
<a id="model-experience"></a>
|
|
22
|
+
## 模型体验
|
|
23
|
+
|
|
24
|
+
无,因为本包提供浏览器侧状态基础设施,不注册任何面向模型的内容。
|
|
25
|
+
|
|
26
|
+
#### KV Cache 影响
|
|
27
|
+
|
|
28
|
+
无;这些 store 既不组装也不发送模型请求。
|
|
29
|
+
|
|
30
|
+
## 已知限制与暂缓事项
|
|
31
|
+
|
|
32
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
33
|
+
|
|
34
|
+
- **持久化仅限浏览器本地**——持久化 store 使用 `localStorage` 中的 JSON;非浏览器运行时会禁用持久化,本包也不提供跨设备同步。
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
<a id="dev-note"></a>
|
|
38
|
+
### 开发备注
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
42
|
+
|
|
43
|
+
无。
|
|
44
|
+
|
|
45
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createStore } from "zustand/vanilla";
|
|
2
|
+
import { subscribeWithSelector } from "zustand/middleware";
|
|
3
|
+
import { shallow } from "zustand/shallow";
|
|
4
|
+
import { freeze, produce } from "immer";
|
|
5
|
+
//#region lib/types/index.js
|
|
6
|
+
/**
|
|
7
|
+
* React-free snapshot store engine (zustand vanilla + immer + subscribeWithSelector +
|
|
8
|
+
* rafFlush middleware + opt-in persist + dev freeze) plus the declarative
|
|
9
|
+
* shell over it: {@link defineStore} bakes an init/persist/actions literal
|
|
10
|
+
* into a {@link StoreHandle}, the registration-side store seat of slot
|
|
11
|
+
* terminals. Engine products are bare observables — subscribe/getSnapshot/
|
|
12
|
+
* update/set, NO selector hook. Hook synthesis is ui-renderer's (the one
|
|
13
|
+
* uSES bridge, cached per source at the binding site).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Notify an observer set without allowing one callback to starve the rest.
|
|
17
|
+
* @param listeners - current observer callbacks; copied before dispatch.
|
|
18
|
+
* @param label - diagnostic owner prefix.
|
|
19
|
+
* @param args - callback arguments.
|
|
20
|
+
*/
|
|
21
|
+
function notifySubscribers(listeners, label, ...args) {
|
|
22
|
+
for (const listener of [...listeners]) try {
|
|
23
|
+
listener(...args);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`${label} subscriber failed:`, error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Shallow equality for selector slices (zustand/shallow semantics; travels
|
|
30
|
+
* with the engine so hook consumers need no zustand dependency).
|
|
31
|
+
* @param a - left value.
|
|
32
|
+
* @param b - right value.
|
|
33
|
+
* @returns whether the values are shallowly equal.
|
|
34
|
+
*/
|
|
35
|
+
function shallowEqual(a, b) {
|
|
36
|
+
return shallow(a, b);
|
|
37
|
+
}
|
|
38
|
+
/** Batches subscriber notification into one flush per animation frame. */
|
|
39
|
+
function rafBatch(notify) {
|
|
40
|
+
const schedule = typeof requestAnimationFrame === "function" ? (fn) => {
|
|
41
|
+
requestAnimationFrame(() => {
|
|
42
|
+
fn();
|
|
43
|
+
});
|
|
44
|
+
} : (fn) => {
|
|
45
|
+
queueMicrotask(fn);
|
|
46
|
+
};
|
|
47
|
+
let scheduled = false;
|
|
48
|
+
return () => {
|
|
49
|
+
if (scheduled) return;
|
|
50
|
+
scheduled = true;
|
|
51
|
+
schedule(() => {
|
|
52
|
+
scheduled = false;
|
|
53
|
+
notify();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create a snapshot store.
|
|
59
|
+
*
|
|
60
|
+
* Flush default is 'sync' (controlled inputs need same-tick echo); frame-driven
|
|
61
|
+
* stores opt into 'raf', where a frame's worth of updates coalesces into one
|
|
62
|
+
* notification. Known raf-mode tradeoff: a component mounting mid-frame reads
|
|
63
|
+
* fresh state while existing subscribers hear it next flush — transient
|
|
64
|
+
* frame-level skew, same nature as the object layer's microtask batching.
|
|
65
|
+
*
|
|
66
|
+
* @param init - initial state.
|
|
67
|
+
* @param opts - flush mode and opt-in persistence (localStorage, keyed by name).
|
|
68
|
+
* @returns the store.
|
|
69
|
+
*/
|
|
70
|
+
function createSnapshotStore(init, opts) {
|
|
71
|
+
const withSelector = subscribeWithSelector(() => init);
|
|
72
|
+
const api = createStore()(withSelector);
|
|
73
|
+
if (opts?.persist) attachPersistence(api, opts.persist.name);
|
|
74
|
+
let subscribe = (fn) => api.subscribe(() => {
|
|
75
|
+
notifySubscribers([fn], "[client-store]");
|
|
76
|
+
});
|
|
77
|
+
if (opts?.flush === "raf") {
|
|
78
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
79
|
+
const flush = rafBatch(() => {
|
|
80
|
+
notifySubscribers(listeners, "[client-store]");
|
|
81
|
+
});
|
|
82
|
+
api.subscribe(flush);
|
|
83
|
+
subscribe = (fn) => {
|
|
84
|
+
listeners.add(fn);
|
|
85
|
+
return () => {
|
|
86
|
+
listeners.delete(fn);
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
getSnapshot: () => api.getState(),
|
|
92
|
+
subscribe: (fn) => subscribe(fn),
|
|
93
|
+
update: (mutator) => {
|
|
94
|
+
api.setState(produce(api.getState(), (draft) => {
|
|
95
|
+
mutator(draft);
|
|
96
|
+
}), true);
|
|
97
|
+
},
|
|
98
|
+
set: (next) => {
|
|
99
|
+
api.setState(devFreeze(next), true);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whole-value JSON persistence to localStorage. Hand-rolled instead of the
|
|
105
|
+
* zustand persist middleware: its write path spreads state into an object
|
|
106
|
+
* (`partialize({ ...get() })`), exploding primitive state (a persisted string
|
|
107
|
+
* draft becomes {0:'h',1:'e',...}) — not fixable via merge/deserialize options
|
|
108
|
+
* because the corruption happens before serialization. Storage failures
|
|
109
|
+
* (quota, private mode) only disable persistence, never break the store.
|
|
110
|
+
*/
|
|
111
|
+
function attachPersistence(api, name) {
|
|
112
|
+
if (typeof localStorage === "undefined") return;
|
|
113
|
+
try {
|
|
114
|
+
const raw = localStorage.getItem(name);
|
|
115
|
+
if (raw !== null) api.setState(devFreeze(JSON.parse(raw)), true);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`snapshot store '${name}' rehydration failed:`, error);
|
|
118
|
+
}
|
|
119
|
+
api.subscribe((state) => {
|
|
120
|
+
try {
|
|
121
|
+
localStorage.setItem(name, JSON.stringify(state));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`snapshot store '${name}' persistence failed:`, error);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Deep-freeze draftable wholesale-set state outside production: set() bypasses immer's freeze. */
|
|
128
|
+
function devFreeze(value) {
|
|
129
|
+
return freeze(value, true);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Declare a store: initial state, optional persistence, and the full write
|
|
133
|
+
* set as pure draft mutators. The returned handle is the registration
|
|
134
|
+
* currency of the store seat — its identity keys instance sharing. Satisfies
|
|
135
|
+
* ui-slots' DefineStore contract (the handle/instance are the engine-extended
|
|
136
|
+
* subtypes).
|
|
137
|
+
*
|
|
138
|
+
* The `A & ActionsDecl<T>` actions position is load-bearing: T resolves from
|
|
139
|
+
* `init` in the first inference round, and the intersection then contextually
|
|
140
|
+
* types each mutator's draft parameter (context-sensitive functions defer),
|
|
141
|
+
* so call sites write `(d, x: X) => { ... }` with no draft annotation. If a
|
|
142
|
+
* future TS version breaks this single-literal inference, the design's
|
|
143
|
+
* documented fallback is currying (`defineStore(init).actions({...})`).
|
|
144
|
+
* @param decl - init lambda (fresh state per instance), optional persist key, actions table.
|
|
145
|
+
* @returns the store handle.
|
|
146
|
+
*/
|
|
147
|
+
function defineStore(decl) {
|
|
148
|
+
return {
|
|
149
|
+
spec: decl,
|
|
150
|
+
create(scopeKey) {
|
|
151
|
+
const persistKey = decl.persist === void 0 ? void 0 : scopeKey === void 0 ? decl.persist : `${decl.persist}.${scopeKey}`;
|
|
152
|
+
const store = createSnapshotStore(decl.init(), persistKey !== void 0 ? { persist: { name: persistKey } } : void 0);
|
|
153
|
+
const actions = {};
|
|
154
|
+
for (const key of Object.keys(decl.actions)) {
|
|
155
|
+
const mutate = decl.actions[key];
|
|
156
|
+
actions[key] = (...params) => {
|
|
157
|
+
store.update((draft) => {
|
|
158
|
+
mutate(draft, ...params);
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
actions,
|
|
164
|
+
getSnapshot: () => store.getSnapshot(),
|
|
165
|
+
subscribe: (fn) => store.subscribe(fn),
|
|
166
|
+
store,
|
|
167
|
+
clearPersisted: () => {
|
|
168
|
+
if (persistKey === void 0 || typeof localStorage === "undefined") return;
|
|
169
|
+
try {
|
|
170
|
+
localStorage.removeItem(persistKey);
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
export { createSnapshotStore, defineStore, notifySubscribers, shallowEqual };
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=index.js.map
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-store`.
|
|
4
|
+
* @module @deepseek-ai/dsh-client-store/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-client-store";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "client-store-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: the package exports a library engine and creates no
|
|
13
|
+
* process-global state; each store instance is covered by its owning tests.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Framework-neutral snapshot and store contracts. */
|
|
2
|
+
/** Minimal observable snapshot source shared by controllers, stores, and render adapters. */
|
|
3
|
+
export interface ObservableSnapshot<T> {
|
|
4
|
+
/** Read the cached snapshot reference. */
|
|
5
|
+
getSnapshot(): T;
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe to snapshot invalidation.
|
|
8
|
+
* @param fn - invalidation callback.
|
|
9
|
+
* @returns unsubscribe function.
|
|
10
|
+
*/
|
|
11
|
+
subscribe(fn: () => void): () => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Typed selector hook over a snapshot source. Canonical shape for the whole
|
|
15
|
+
* slot system (ui-renderer's engine hook is structurally identical; the
|
|
16
|
+
* framework is the only party that ever constructs one).
|
|
17
|
+
*/
|
|
18
|
+
export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S;
|
|
19
|
+
/**
|
|
20
|
+
* Selector hook over a source that follows the current session. The hook is
|
|
21
|
+
* always present, while its selected value is absent whenever no session is
|
|
22
|
+
* current. This keeps hook call sites stable across no-session/session
|
|
23
|
+
* transitions without pretending that a session snapshot exists.
|
|
24
|
+
*/
|
|
25
|
+
export type MaybeSnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Action declaration table: pure immer-draft transforms over the store state,
|
|
28
|
+
* declared as the store's complete write set (the audit face — components can
|
|
29
|
+
* only write through these).
|
|
30
|
+
*/
|
|
31
|
+
export type ActionsDecl<T> = Record<string, (draft: T, ...params: any[]) => void>;
|
|
32
|
+
/**
|
|
33
|
+
* Draft-stripped callback form of an actions table: what components
|
|
34
|
+
* (`props.actions`) and inject factories receive — the framework bakes the
|
|
35
|
+
* draft parameter away by binding each action to the resolved instance.
|
|
36
|
+
*/
|
|
37
|
+
export type BakedActions<T, A extends ActionsDecl<T>> = {
|
|
38
|
+
[K in keyof A]: A[K] extends (draft: T, ...params: infer P) => void ? (...params: P) => void : never;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Store declaration spec: initial-state factory (a lambda so every instance
|
|
42
|
+
* gets a fresh state), optional persistence key (mechanical, framework-run),
|
|
43
|
+
* and the actions write set.
|
|
44
|
+
*/
|
|
45
|
+
export interface StoreSpec<T, A extends ActionsDecl<T>> {
|
|
46
|
+
init: () => T;
|
|
47
|
+
persist?: string;
|
|
48
|
+
actions: A;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Live engine instance: the create() product consumed by the render machinery
|
|
52
|
+
* and by tests. A bare snapshot source plus the baked write set — no React
|
|
53
|
+
* hook rides the engine product (the engine lives in this React-free package);
|
|
54
|
+
* the render machinery binds the `useStore` hook from this source on its own
|
|
55
|
+
* side, cached per instance. Production components and render paths never
|
|
56
|
+
* call create() themselves — instance lifecycle is the framework's.
|
|
57
|
+
*/
|
|
58
|
+
export interface StoreInstance<T, A extends ActionsDecl<T>> {
|
|
59
|
+
readonly actions: BakedActions<T, A>;
|
|
60
|
+
getSnapshot(): T;
|
|
61
|
+
/**
|
|
62
|
+
* Subscribe to state changes (uSES subscribe side).
|
|
63
|
+
* @param fn - change callback.
|
|
64
|
+
* @returns unsubscribe.
|
|
65
|
+
*/
|
|
66
|
+
subscribe(fn: () => void): () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Drop this instance's persisted value (no-op for non-persist specs). The
|
|
69
|
+
* framework calls it when the owning scope dies for good — a pruned session
|
|
70
|
+
* must not leave orphaned storage keys behind.
|
|
71
|
+
*/
|
|
72
|
+
clearPersisted(): void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Store handle: spec + state/actions types + shared identity + instance
|
|
76
|
+
* factory in one value. Handles are constructed in apply world (shared across
|
|
77
|
+
* registrations of one plugin) or by the framework from a registrant's
|
|
78
|
+
* factory (exclusive). Never export a handle at module level — module-cache
|
|
79
|
+
* identity is a disguised singleton across plugin reloads.
|
|
80
|
+
*/
|
|
81
|
+
export interface StoreHandle<T, A extends ActionsDecl<T>> {
|
|
82
|
+
readonly spec: StoreSpec<T, A>;
|
|
83
|
+
/**
|
|
84
|
+
* Create a live engine instance (framework machinery and tests only).
|
|
85
|
+
* @param scopeKey - session id for session-scope instances; suffixes the
|
|
86
|
+
* persist key so per-session instances persist independently (root-scope
|
|
87
|
+
* instances omit it).
|
|
88
|
+
* @returns a fresh instance seeded from `spec.init()`.
|
|
89
|
+
*/
|
|
90
|
+
create(scopeKey?: string): StoreInstance<T, A>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Exclusive-store registration form: the registrant passes the factory itself
|
|
94
|
+
* and the framework calls it per entry x scope (no shared identity exists).
|
|
95
|
+
*/
|
|
96
|
+
export type StoreFactory = () => StoreHandle<any, any>;
|
|
97
|
+
/** The register `store` option position: a shared handle or an exclusive factory. */
|
|
98
|
+
export type StoreDecl = StoreHandle<any, any> | StoreFactory;
|
|
99
|
+
/** Normalize a store declaration to its handle type (factories yield their return). */
|
|
100
|
+
export type HandleOf<H> = H extends () => infer R ? R : H;
|
|
101
|
+
/**
|
|
102
|
+
* Handle-keyed baked actions: the `actions` parameter of an inject factory
|
|
103
|
+
* whose registration declared a store — the same baked callback set the
|
|
104
|
+
* component receives via {@link PropsStore}.
|
|
105
|
+
*/
|
|
106
|
+
export type BoundActions<H> = H extends StoreHandle<infer T, infer A> ? BakedActions<T, A> : never;
|
|
107
|
+
/**
|
|
108
|
+
* The store props share, derived from the declared handle: a typed selector
|
|
109
|
+
* hook plus the baked write set. Components never see the instance itself
|
|
110
|
+
* (no update/set — reads via useStore, writes via the declared actions only).
|
|
111
|
+
*/
|
|
112
|
+
export type PropsStore<H> = H extends StoreHandle<infer T, infer A> ? {
|
|
113
|
+
useStore: SnapshotSelectorHook<T>;
|
|
114
|
+
actions: BakedActions<T, A>;
|
|
115
|
+
} : object;
|
|
116
|
+
/**
|
|
117
|
+
* The defineStore contract (implementation lives beside this declaration,
|
|
118
|
+
* bound to the snapshot-store engine): spec in, handle out, with T inferred
|
|
119
|
+
* from `init` and the actions table constrained by T.
|
|
120
|
+
*/
|
|
121
|
+
export type DefineStore = <T, A extends ActionsDecl<T>>(spec: StoreSpec<T, A>) => StoreHandle<T, A>;
|
|
122
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ActionsDecl, ObservableSnapshot, StoreHandle, StoreInstance, StoreSpec } from './contract.ts';
|
|
2
|
+
export type { ActionsDecl, BakedActions, BoundActions, DefineStore, HandleOf, MaybeSnapshotSelectorHook, ObservableSnapshot, PropsStore, SnapshotSelectorHook, StoreDecl, StoreFactory, StoreHandle, StoreInstance, StoreSpec, } from './contract.ts';
|
|
3
|
+
/** Writable snapshot store (bare data face; React selector hooks are synthesized in ui-renderer). */
|
|
4
|
+
export interface SnapshotStore<T> extends ObservableSnapshot<T> {
|
|
5
|
+
/**
|
|
6
|
+
* Mutate the state through an immer draft.
|
|
7
|
+
* @param mutator - draft mutator.
|
|
8
|
+
*/
|
|
9
|
+
update(mutator: (draft: T) => void): void;
|
|
10
|
+
/**
|
|
11
|
+
* Replace the state wholesale.
|
|
12
|
+
* @param next - next state.
|
|
13
|
+
*/
|
|
14
|
+
set(next: T): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Notify an observer set without allowing one callback to starve the rest.
|
|
18
|
+
* @param listeners - current observer callbacks; copied before dispatch.
|
|
19
|
+
* @param label - diagnostic owner prefix.
|
|
20
|
+
* @param args - callback arguments.
|
|
21
|
+
*/
|
|
22
|
+
export declare function notifySubscribers<Args extends readonly unknown[]>(listeners: Iterable<(...args: Args) => void>, label: string, ...args: Args): void;
|
|
23
|
+
/**
|
|
24
|
+
* Shallow equality for selector slices (zustand/shallow semantics; travels
|
|
25
|
+
* with the engine so hook consumers need no zustand dependency).
|
|
26
|
+
* @param a - left value.
|
|
27
|
+
* @param b - right value.
|
|
28
|
+
* @returns whether the values are shallowly equal.
|
|
29
|
+
*/
|
|
30
|
+
export declare function shallowEqual(a: unknown, b: unknown): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Create a snapshot store.
|
|
33
|
+
*
|
|
34
|
+
* Flush default is 'sync' (controlled inputs need same-tick echo); frame-driven
|
|
35
|
+
* stores opt into 'raf', where a frame's worth of updates coalesces into one
|
|
36
|
+
* notification. Known raf-mode tradeoff: a component mounting mid-frame reads
|
|
37
|
+
* fresh state while existing subscribers hear it next flush — transient
|
|
38
|
+
* frame-level skew, same nature as the object layer's microtask batching.
|
|
39
|
+
*
|
|
40
|
+
* @param init - initial state.
|
|
41
|
+
* @param opts - flush mode and opt-in persistence (localStorage, keyed by name).
|
|
42
|
+
* @returns the store.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSnapshotStore<T>(init: T, opts?: {
|
|
45
|
+
flush?: 'raf' | 'sync';
|
|
46
|
+
persist?: {
|
|
47
|
+
name: string;
|
|
48
|
+
};
|
|
49
|
+
}): SnapshotStore<T>;
|
|
50
|
+
/** A live engine instance: the contract instance plus the raw engine store. */
|
|
51
|
+
export interface EngineStoreInstance<T, A extends ActionsDecl<T>> extends StoreInstance<T, A> {
|
|
52
|
+
/** The underlying engine store (framework/test API; components never see it). */
|
|
53
|
+
readonly store: SnapshotStore<T>;
|
|
54
|
+
}
|
|
55
|
+
/** The engine-backed handle: create() narrowed to the engine instance. */
|
|
56
|
+
export interface EngineStoreHandle<T, A extends ActionsDecl<T>> extends StoreHandle<T, A> {
|
|
57
|
+
/**
|
|
58
|
+
* Construct a live engine instance (see the contract JSDoc on
|
|
59
|
+
* {@link StoreHandle.create} for scopeKey/persist semantics).
|
|
60
|
+
*
|
|
61
|
+
* Known boundary: the persist key is the storage identity, so multiple live
|
|
62
|
+
* instances created under the same resolved key share (and cross-pollute)
|
|
63
|
+
* one localStorage entry. Instance uniqueness per key is the caller's
|
|
64
|
+
* responsibility — production is safe because the framework caches one
|
|
65
|
+
* instance per handle x scope key; tests wanting isolation use distinct
|
|
66
|
+
* scope keys or persist-free declarations (multi-create freedom is a
|
|
67
|
+
* feature there, so create() deliberately does not dedupe or throw).
|
|
68
|
+
* @param scopeKey - session id for session-scope instances; omitted for root scope.
|
|
69
|
+
* @returns the engine instance.
|
|
70
|
+
*/
|
|
71
|
+
create(scopeKey?: string): EngineStoreInstance<T, A>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Declare a store: initial state, optional persistence, and the full write
|
|
75
|
+
* set as pure draft mutators. The returned handle is the registration
|
|
76
|
+
* currency of the store seat — its identity keys instance sharing. Satisfies
|
|
77
|
+
* ui-slots' DefineStore contract (the handle/instance are the engine-extended
|
|
78
|
+
* subtypes).
|
|
79
|
+
*
|
|
80
|
+
* The `A & ActionsDecl<T>` actions position is load-bearing: T resolves from
|
|
81
|
+
* `init` in the first inference round, and the intersection then contextually
|
|
82
|
+
* types each mutator's draft parameter (context-sensitive functions defer),
|
|
83
|
+
* so call sites write `(d, x: X) => { ... }` with no draft annotation. If a
|
|
84
|
+
* future TS version breaks this single-literal inference, the design's
|
|
85
|
+
* documented fallback is currying (`defineStore(init).actions({...})`).
|
|
86
|
+
* @param decl - init lambda (fresh state per instance), optional persist key, actions table.
|
|
87
|
+
* @returns the store handle.
|
|
88
|
+
*/
|
|
89
|
+
export declare function defineStore<T, A extends ActionsDecl<T>>(decl: StoreSpec<T, A> & {
|
|
90
|
+
actions: A & ActionsDecl<T>;
|
|
91
|
+
}): EngineStoreHandle<T, A>;
|
|
92
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-store`.
|
|
3
|
+
* @module @deepseek-ai/dsh-client-store/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "client-store-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-client-store",
|
|
3
|
+
"description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine",
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/client/store"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"immer": "^10.1.1",
|
|
31
|
+
"zustand": "~4.4.7"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"lib/index.js",
|
|
42
|
+
"lib/invariant.js",
|
|
43
|
+
"lib/types/**/*.d.ts"
|
|
44
|
+
]
|
|
45
|
+
}
|