@deepseek-ai/dsh-session-projection-cache 0.1.1-rc.2 → 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/README.i18n.yaml +2 -2
- package/README.md +102 -27
- package/README.zh.md +105 -30
- package/lib/index.js +91 -81
- package/lib/invariant.js +2 -2
- package/lib/types/index.d.ts +59 -41
- package/lib/types/spec.d.ts +17 -13
- package/package.json +16 -16
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
|
# pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: c5e438a2ededef00a1faa456e9913492a603b51d
|
|
6
|
+
README.zh.md: eab0b81af618558e6f1e5e6bc0c40da3fcc38884
|
package/README.md
CHANGED
|
@@ -1,42 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The persisted session-projection cache for deployments and maintainers choosing, configuring, or debugging durable checkpoints, zero-I/O list reads, and accelerated cold projection folds."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-session-projection-cache
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A stored row `(key → {ver, seq, val})` is a fold shortcut, never an authority: possibly stale (`seq` says exactly how stale) but never wrong. Consequences the implementation commits to:
|
|
10
|
+
## Summary
|
|
8
11
|
|
|
9
|
-
-
|
|
10
|
-
- **A `ver` mismatch against the live unit's `stateVersion` discards, never migrates.** A unit bump invalidates its rows at read time; the key refolds from the log.
|
|
11
|
-
- **A row must pass the live unit's `stateSchema`.** A malformed row is omitted from the zero-I/O view and rejected by restore so the cold-read ladder refolds it from the log.
|
|
12
|
-
- **Whole-record writes.** Each write replaces the session's full checkpoint (the registry cut is always complete), snapshotted through the lossless-JSON boundary — a unit state violating the plain-JSON contract fails loud.
|
|
13
|
-
- **Records are bound to a log lifecycle, not just an id.** Each record stores the header identity (`createdAt`, `cwd`) it was folded from; every read validates it (the live or stored header is the witness) before accepting a row, so a deleted-then-recreated id or a persistence store swapped under a surviving cache discards the unrelated record instead of seeding phantom values.
|
|
14
|
-
- **The log leads, the cache follows.** A live checkpoint flushes the session's buffered events durably BEFORE the cache row lands, so a crash can leave the cache behind the log (a longer tail replay) but never ahead of it.
|
|
12
|
+
`dsh-session-projection-cache` persists the state checkpoints of every registered projection unit (`ctx.sessionProjectionCache`) as one versioned document per session in the `session_projcache` storage domain's `per-record` layout. The shipped JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json`, and the cache never reads the session-persistence layer. A stored row is a fold shortcut, never an authority: it may be stale — its `seq` says exactly how stale — but never wrong. Three mandatory checkpoints (session creation, `turn/end`, and session disposal) plus configurable count and interval throttles keep the cache fresh. Choose it when list views need synchronous cached values or cold projection folds should skip an already-checkpointed prefix.
|
|
15
13
|
|
|
16
|
-
##
|
|
14
|
+
## Table of Contents
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|---|---|
|
|
22
|
-
| `turn/end` | Mandatory — the turn-final value is what cold reads want. |
|
|
23
|
-
| Session disposal (detach) | Mandatory — the live-to-cold moment; after it the cold ladder serves this session. |
|
|
24
|
-
| `writeEveryEvents` committed events | Config throttle (count). |
|
|
25
|
-
| `writeIntervalMs` since the first dirty event | Config throttle (interval). |
|
|
23
|
+
-----
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
Mount this package beside the projection registry and the storage stack when clients should list projection values for cold sessions without loading their logs. Without it, consumers must obtain the log before they can reconstruct cold projection values.
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
### When to choose it
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
Choose it when a deployment restarts sessions and needs durable projection values for history lists, statistics, or goal snapshots. Skip it when projections serve only live sessions, or when the extra storage writes cost more than the saved projection work.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
### Minimal configuration
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
Both throttle fields are required — flush cadence is a deployment choice with no universally correct value:
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
The cache opens its domain through the storage stack, so base mounts `storage`, `storage-json` (root `dshHomePath('storages')`), and `storage-domain` (`backend: json`) before it:
|
|
40
39
|
|
|
41
40
|
```yaml
|
|
42
41
|
- id: session-projection-cache
|
|
@@ -46,11 +45,72 @@ The read ladder, zero full-log load on the happy path: cached rows → `sessionP
|
|
|
46
45
|
writeIntervalMs: 5000
|
|
47
46
|
```
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
| Field | Default | Meaning |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `writeEveryEvents` | required | Committed events per session that force a durable checkpoint write between mandatory points |
|
|
51
|
+
| `writeIntervalMs` | required | Longest time a dirty checkpoint may stay unwritten between mandatory points |
|
|
52
|
+
|
|
53
|
+
The plugin injects `storageDomain`, `sessionProjections`, and `sessions`. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-projection-cache) is the exhaustive source for every accepted field and its JSDoc.
|
|
54
|
+
|
|
55
|
+
### How checkpoints are written
|
|
56
|
+
|
|
57
|
+
Three mandatory points always write: session creation persists the seed-derived cut, `turn/end` persists the value that listing reads want, and session disposal persists the final live cut. Between them, the configured count and interval throttles write as events accumulate. Every write atomically replaces the session's complete record through the domain write chain; a failure logs a warning and keeps the cache stale, and the next write self-heals.
|
|
58
|
+
|
|
59
|
+
### Reading cached values
|
|
60
|
+
|
|
61
|
+
`cachedSnapshot(meta)` synchronously serves client values from the storage domain's in-memory tables with zero I/O. It accepts only an identity-matching record and version- and schema-matching keys, then returns a `{ asOfSeq, values }` cut at the lowest served-row watermark. It returns `undefined` for an unknown id, unrelated lifecycle, absent or foreign record document, or no usable rows. `coldSnapshot(meta, events)` accepts a complete ordered log, skips the checkpointed prefix while folding, and refreshes the record without reading the persistence layer itself.
|
|
62
|
+
|
|
63
|
+
### What the cache guarantees
|
|
64
|
+
|
|
65
|
+
The log leads and the cache follows: a live checkpoint flushes the session's buffered events durably before the cache row lands, so a crash can leave the cache behind the log but never ahead of it. Reads and writes share the storage domain's coherent in-memory state; the per-unit write chain mutates memory only after durability. Each version-stamped record must match the live unit schema and session header identity (`createdAt`, `cwd`), so malformed, stale, or unrelated records read as absent. The JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json` in an owner-only directory tree.
|
|
66
|
+
|
|
67
|
+
-----
|
|
68
|
+
|
|
69
|
+
<a id="understand-the-implementation"></a>
|
|
70
|
+
## Understand the implementation
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
<summary>Implementation internals — click to expand</summary>
|
|
74
|
+
|
|
75
|
+
This section explains the cache's durability and storage ownership; the observable behavior is covered in [Use this package](#use-this-package).
|
|
76
|
+
|
|
77
|
+
### Design concept
|
|
78
|
+
|
|
79
|
+
The cache is a fold shortcut over the projection registry's checkpoint face, stored in a `per-record` domain data table. It commits to six consequences: reads never bypass the domain write chain; every background write is fail-soft; a `ver` mismatch discards rather than migrates a row; a record must pass the live unit's `stateSchema`; writes replace one complete session record through the lossless-JSON boundary; and the log leads, the cache follows.
|
|
80
|
+
|
|
81
|
+
### Read and write ownership
|
|
50
82
|
|
|
83
|
+
The cache stores one version-stamped document per session in the `session_projcache` domain. It does not depend on a session-persistence backend, call `locate`, or inspect per-session directories. A malformed or stale record reads as absent, and consumers that require a cold value own any log refold.
|
|
84
|
+
|
|
85
|
+
### Source map
|
|
86
|
+
|
|
87
|
+
| File | Role |
|
|
88
|
+
|---|---|
|
|
89
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: `SessionProjectionCache` service, write-behind listeners, cache reads |
|
|
90
|
+
| [`src/spec.ts`](src/spec.ts) | The `session_projcache` domain spec and record identity types |
|
|
91
|
+
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; correctness is enforced at the write and read paths) |
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
<a id="further-exploration"></a>
|
|
98
|
+
## Further Exploration
|
|
99
|
+
|
|
100
|
+
Read these pages when the package-level contract is not enough. They move from the cache to the registry it checkpoints and the storage domain that holds its records.
|
|
101
|
+
|
|
102
|
+
- [Session projections subsystem](../../../docs/subsystems/session-projection.md) — the projection unit contract and drive semantics this cache checkpoints.
|
|
103
|
+
- [Session projection registry](../session-projection/README.md) — the `ctx.sessionProjections` service whose checkpoints this cache persists.
|
|
104
|
+
- [Storage subsystem](../../../docs/subsystems/storage.md) — the domain routing and backend behavior that store cache records.
|
|
105
|
+
- [Session package map](../README.md) — adjacent persistence, title, and telemetry packages.
|
|
106
|
+
- [Session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) — the persisted projection cache design rationale.
|
|
107
|
+
|
|
108
|
+
-----
|
|
109
|
+
|
|
110
|
+
<a id="model-experience"></a>
|
|
51
111
|
## Model Experience
|
|
52
112
|
|
|
53
|
-
None, as the cache
|
|
113
|
+
None, as the persisted cache accelerates host-side reads of projection state and registers nothing model-facing.
|
|
54
114
|
|
|
55
115
|
#### KV Cache effect
|
|
56
116
|
|
|
@@ -58,6 +118,21 @@ None; the cache never assembles or sends provider requests.
|
|
|
58
118
|
|
|
59
119
|
## Known Limitations and Deferred Work
|
|
60
120
|
|
|
121
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
These limits define where the cache needs operational care. They are current package constraints, not a task backlog.
|
|
125
|
+
|
|
61
126
|
- **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself.
|
|
62
127
|
- **Interval throttle is per-session coarse** — the timer arms at the first dirty event after a clean write; a steady sub-threshold trickle writes once per interval, not a sliding window.
|
|
63
|
-
-
|
|
128
|
+
- **No cache-side cold refold** — the cache serves and refreshes its rows but never reads the session log (it does not depend on the persistence layer); a consumer that needs a guaranteed cold snapshot refolds from the log itself.
|
|
129
|
+
|
|
130
|
+
<a id="dev-note"></a>
|
|
131
|
+
### Dev Note
|
|
132
|
+
|
|
133
|
+
<details>
|
|
134
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
135
|
+
|
|
136
|
+
None.
|
|
137
|
+
|
|
138
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,42 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向部署方与维护者的持久会话投影缓存说明,用于选择、配置或排查持久检查点、零 I/O 列表读取与加速的冷投影折叠。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-session-projection-cache
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
一条存储行 `(key → {ver, seq, val})` 是折叠捷径,绝不是权威:可能陈旧(`seq` 精确说明陈旧到哪),但绝不会错。实现据此承诺:
|
|
10
|
+
## 概述
|
|
8
11
|
|
|
9
|
-
-
|
|
10
|
-
- **`ver` 与当前运行单元的 `stateVersion` 不匹配即丢弃,绝不迁移。** 单元递增版本会在读取时使其行失效;该 key 从日志重新折叠。
|
|
11
|
-
- **存储行必须通过当前单元的 `stateSchema`。** 畸形行从零 I/O view 中省略,并被 restore 拒绝,使冷读阶梯从日志重新折叠。
|
|
12
|
-
- **整记录写入。** 每次写入替换该会话的完整检查点(注册表切面始终是完整的),并经无损 JSON 边界快照——违反纯 JSON 约定的单元状态会显式失败并报错。
|
|
13
|
-
- **记录绑定到日志生命周期,而不只是 id。** 每条记录存储其折叠来源的 header 身份(`createdAt`、`cwd`);每次读取先以活 header 或存储 header 为证验证它,再接受任何行——被删后重建的 id、或缓存幸存而持久化存储被换掉时,无关记录被整体丢弃,绝不播种幻影值。
|
|
14
|
-
- **日志领先,缓存跟随。** 活会话检查点先把缓冲事件持久 flush,缓存行才落地,因此崩溃只会让缓存落后于日志(更长的尾部回放),绝不领先于它。
|
|
12
|
+
`dsh-session-projection-cache` 将每个已注册投影单元的状态检查点(`ctx.sessionProjectionCache`)存为 `session_projcache` 存储域 `per-record` 布局下的逐会话版本化文档。随附 JSON 后端将每条记录存于 `<root>/session_projcache/sessions/<id>.json`,缓存绝不读取会话持久化层。存储行是折叠捷径,绝不是权威:它可能陈旧——`seq` 精确说明陈旧到哪——但绝不会错。三个必写点(会话创建、`turn/end` 与会话释放)加上可配置的条数与间隔节流让缓存保持新鲜。当列表视图需要同步缓存值,或冷投影折叠应跳过已检查点化的前缀时,选择本包。
|
|
15
13
|
|
|
16
|
-
##
|
|
14
|
+
## 目录
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|---|---|
|
|
22
|
-
| `turn/end` | 必写——冷读要的正是轮次终值。 |
|
|
23
|
-
| 会话释放(detach) | 必写——live 转 cold 的时刻;此后冷读阶梯接管该会话。 |
|
|
24
|
-
| 累计 `writeEveryEvents` 个已提交事件 | 配置节流(条数)。 |
|
|
25
|
-
| 距首个脏事件 `writeIntervalMs` 毫秒 | 配置节流(间隔)。 |
|
|
23
|
+
-----
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
当客户端应在不加载日志的情况下列出冷会话投影值时,把本包与投影注册表及存储栈一起挂载。没有它时,消费方必须先取得日志,才能重建冷投影值。
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
### 何时选择
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
当部署会重启会话,并需要为历史列表、统计信息或 goal 快照提供持久投影值时,选择本包。当投影只服务实时会话,或额外存储写入的成本高于所节省的投影工作时,跳过本包。
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
### 最小配置
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
两个节流字段均必填——写入节奏是部署选择,没有普适正确值:
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
缓存通过存储栈打开自己的域,因此 base 先挂 `storage`、`storage-json`(根 `dshHomePath('storages')`)与 `storage-domain`(`backend: json`):
|
|
40
39
|
|
|
41
40
|
```yaml
|
|
42
41
|
- id: session-projection-cache
|
|
@@ -46,18 +45,94 @@
|
|
|
46
45
|
writeIntervalMs: 5000
|
|
47
46
|
```
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
| 字段 | 默认值 | 含义 |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `writeEveryEvents` | 必填 | 在各必写点之间强制一次持久检查点写入的每会话已提交事件数 |
|
|
51
|
+
| `writeIntervalMs` | 必填 | 各必写点之间脏检查点最长可保持未写入的时间 |
|
|
52
|
+
|
|
53
|
+
本插件注入 `storageDomain`、`sessionProjections` 与 `sessions`。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-projection-cache)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
|
54
|
+
|
|
55
|
+
### 检查点如何写入
|
|
56
|
+
|
|
57
|
+
三个必写点总是写入:会话创建保存由种子派生的切面,`turn/end` 保存列表读取所需的轮次终值,会话释放保存最终实时切面。其间,配置的条数与间隔节流随事件累积写入。每次写入通过领域写入链以原子方式替换该会话的完整记录;失败会记录警告并让缓存保持陈旧,后续写入会自行修复。
|
|
58
|
+
|
|
59
|
+
### 读取缓存值
|
|
60
|
+
|
|
61
|
+
`cachedSnapshot(meta)` 以零 I/O 从存储域的内存表同步提供客户端值。它只接受身份匹配的记录以及版本和 schema 均匹配的 key,再按所服务行的最低水位返回 `{ asOfSeq, values }` 切面。对于未知 id、无关生命周期、缺失或外来的记录文档,或没有可用行的情况,它返回 `undefined`。`coldSnapshot(meta, events)` 接受完整有序日志,在折叠时跳过已检查点化的前缀,并在自身不读取持久化层的情况下刷新记录。
|
|
62
|
+
|
|
63
|
+
### 缓存保证什么
|
|
64
|
+
|
|
65
|
+
日志领先,缓存跟随:实时检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配实时单元 schema 与会话 header 身份(`createdAt`、`cwd`),因此畸形、陈旧或无关的记录都会读作不存在。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。
|
|
66
|
+
|
|
67
|
+
-----
|
|
68
|
+
|
|
69
|
+
<a id="understand-the-implementation"></a>
|
|
70
|
+
## 理解实现
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
<summary>实现细节——点击展开</summary>
|
|
74
|
+
|
|
75
|
+
本节说明缓存的持久性与存储所有权;可观察行为已在[使用本包](#use-this-package)中说明。
|
|
76
|
+
|
|
77
|
+
### 设计理念
|
|
78
|
+
|
|
79
|
+
缓存是投影注册表检查点接口上的折叠捷径,存于 `per-record` 领域数据表中。它带来六项后果:读取绝不绕过领域写入链;每次后台写入都 fail-soft;`ver` 不匹配时丢弃而不迁移记录;记录必须通过实时单元的 `stateSchema`;写入通过无损 JSON 边界替换一份完整会话记录;日志领先,缓存跟随。
|
|
80
|
+
|
|
81
|
+
### 读写所有权
|
|
50
82
|
|
|
83
|
+
缓存在 `session_projcache` 领域中为每个会话保存一份带版本戳的文档。它不依赖会话持久化后端,不调用 `locate`,也不检查逐会话目录。畸形或陈旧的记录读作不存在;需要冷值的消费方负责提供日志以重新折叠。
|
|
84
|
+
|
|
85
|
+
### 源码地图
|
|
86
|
+
|
|
87
|
+
| 文件 | 职责 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:`SessionProjectionCache` 服务、写后监听器、缓存读取 |
|
|
90
|
+
| [`src/spec.ts`](src/spec.ts) | `session_projcache` 域 spec 与记录身份类型 |
|
|
91
|
+
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;正确性在写入与读取路径强制) |
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
<a id="further-exploration"></a>
|
|
98
|
+
## 进一步探索
|
|
99
|
+
|
|
100
|
+
当包级约定不够用时阅读以下页面。它们从缓存逐步进入它检查点化的注册表与保存其记录的存储域。
|
|
101
|
+
|
|
102
|
+
- [会话投影子系统](../../../docs/subsystems/session-projection.zh.md)——本缓存检查点化的投影单元约定与驱动语义。
|
|
103
|
+
- [会话投影注册表](../session-projection/README.zh.md)——本缓存持久化其检查点的 `ctx.sessionProjections` 服务。
|
|
104
|
+
- [存储子系统](../../../docs/subsystems/storage.zh.md)——保存缓存记录的领域路由与后端行为。
|
|
105
|
+
- [会话包映射](../README.zh.md)——相邻的持久化、标题与遥测包。
|
|
106
|
+
- [会话投影 RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)——持久投影缓存的设计理由。
|
|
107
|
+
|
|
108
|
+
-----
|
|
109
|
+
|
|
110
|
+
<a id="model-experience"></a>
|
|
51
111
|
## 模型体验
|
|
52
112
|
|
|
53
|
-
|
|
113
|
+
无,因为持久缓存只加速主机侧的投影状态读取,不注册任何模型可见内容。
|
|
54
114
|
|
|
55
115
|
#### KV Cache 影响
|
|
56
116
|
|
|
57
117
|
无;缓存从不组装或发送提供方请求。
|
|
58
118
|
|
|
59
|
-
##
|
|
119
|
+
## 已知限制与延期工作
|
|
120
|
+
|
|
121
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
这些限制说明缓存何时需要运维注意。它们是当前包约束,不是任务积压。
|
|
125
|
+
|
|
126
|
+
- **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。
|
|
127
|
+
- **间隔节流采用按会话的粗粒度控制**——一次无脏数据的写入完成后,计时器在首个脏事件到达时启动;持续但低于条数阈值的事件流每间隔写入一次,而非滑动窗口。
|
|
128
|
+
- **缓存侧不做冷重折叠**——缓存只服务并刷新自己的记录,从不读取会话日志,因为它不依赖持久化层;需要保证冷快照的消费方自行从日志重新折叠。
|
|
129
|
+
|
|
130
|
+
<a id="dev-note"></a>
|
|
131
|
+
### 开发备注
|
|
132
|
+
|
|
133
|
+
<details>
|
|
134
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
135
|
+
|
|
136
|
+
无。
|
|
60
137
|
|
|
61
|
-
|
|
62
|
-
- **间隔节流采用按会话的粗粒度控制**:一次无脏数据的写入完成后,计时器会在首个脏事件到达时启动;对于持续但未达到条数阈值的事件流,系统每个间隔写入一次,而不采用滑动窗口。
|
|
63
|
-
- **`coldSnapshot` 读取不去重**——同一会话的两个并发冷读各跑一遍阶梯;写回最后者胜(行等价),对列表级调用频率可接受。
|
|
138
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { snapshotJsonValue } from "@deepseek-ai/dsh-
|
|
3
|
+
import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
|
|
4
4
|
import { z as z$1 } from "zod";
|
|
5
5
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
6
6
|
//#region lib/types/spec.js
|
|
7
7
|
/**
|
|
8
|
-
* The
|
|
8
|
+
* The projection-cache domain declaration: one `sessions` table keyed by
|
|
9
9
|
* {@link SessionId}, each record the full projection checkpoint for one
|
|
10
|
-
* session (`key → {ver, seq, val}` rows). The spec object
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* session (`key → {ver, seq, val}` rows). The spec object is the single
|
|
11
|
+
* source of the domain's identity, version, layout, and record schema; the
|
|
12
|
+
* storage-domain routing decides the medium (the shipped composition's json
|
|
13
|
+
* backend stores the domain `per-record`: one document per session under
|
|
14
|
+
* `<root>/session_projcache/sessions/`, so a checkpoint write rewrites one
|
|
15
|
+
* session's document instead of the whole unit).
|
|
15
16
|
* @module @deepseek-ai/dsh-session-projection-cache/src/spec
|
|
16
17
|
*/
|
|
17
18
|
/**
|
|
@@ -32,9 +33,9 @@ const checkpointRow = z$1.object({
|
|
|
32
33
|
* that distinguish one session lifecycle from another under the same id. A
|
|
33
34
|
* session id names a slot, not a lifecycle — a deleted-then-recreated id, or
|
|
34
35
|
* a persistence root swapped under a surviving cache, would otherwise let an
|
|
35
|
-
* old
|
|
36
|
-
* log. Reads validate this against the live header (listing) or
|
|
37
|
-
* header (cold read) before accepting any
|
|
36
|
+
* old record pass every watermark check and seed state folded from an
|
|
37
|
+
* unrelated log. Reads validate this against the live header (listing) or
|
|
38
|
+
* the stored header (cold read) before accepting any record.
|
|
38
39
|
*/
|
|
39
40
|
const checkpointIdentity = z$1.object({
|
|
40
41
|
createdAt: z$1.number().int().nonnegative(),
|
|
@@ -51,27 +52,34 @@ const checkpointRecord = z$1.object({
|
|
|
51
52
|
rows: z$1.record(z$1.string(), checkpointRow)
|
|
52
53
|
});
|
|
53
54
|
/**
|
|
54
|
-
* The session-projcache domain spec.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* The session-projcache domain spec. The `per-record` layout scopes version
|
|
56
|
+
* bumps per session: after a bump, a stale session document is discarded on
|
|
57
|
+
* open (cache semantics — a stale or unreadable cache costs a longer tail
|
|
58
|
+
* replay, never a wrong value) while the rest of the domain stays usable,
|
|
59
|
+
* instead of rejecting the whole medium.
|
|
57
60
|
*/
|
|
58
61
|
const projectionCacheDomainSpec = defineDomain({
|
|
59
62
|
name: "session_projcache",
|
|
60
|
-
version:
|
|
63
|
+
version: 4,
|
|
64
|
+
layout: "per-record",
|
|
61
65
|
tables: { sessions: domainTable(checkpointRecord) }
|
|
62
66
|
});
|
|
63
67
|
//#endregion
|
|
64
68
|
//#region lib/types/index.js
|
|
65
69
|
/**
|
|
66
70
|
* Persisted projection cache (`ctx.sessionProjectionCache`): durable
|
|
67
|
-
* checkpoints of every
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* authority:
|
|
71
|
+
* checkpoints of every projection unit's state, one record per session on
|
|
72
|
+
* the `session_projcache` domain (`per-record` layout — the shipped json
|
|
73
|
+
* backend stores one document per session under its root). Reads and writes
|
|
74
|
+
* share ONE coherent state: the domain's in-memory tables serve every read
|
|
75
|
+
* synchronously, and each write lands on the domain's write chain (durability
|
|
76
|
+
* first, then memory), so a read can never observe a disk write the memory
|
|
77
|
+
* has not applied, or a memory value the disk does not hold. The cache is a
|
|
78
|
+
* fold shortcut, never an authority: a row
|
|
79
|
+
* is possibly stale (its `seq` says how stale) but never wrong, so every
|
|
80
|
+
* write path is fail-soft (a lost write costs a longer tail replay on the
|
|
81
|
+
* next cold read) and a `ver` mismatch discards the row instead of migrating
|
|
82
|
+
* it. Design authority: the session-projection RFC
|
|
75
83
|
* (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
|
|
76
84
|
* @module @deepseek-ai/dsh-session-projection-cache
|
|
77
85
|
*/
|
|
@@ -82,18 +90,17 @@ const Config = z.object({
|
|
|
82
90
|
/**
|
|
83
91
|
* The persisted projection cache service. Opens the `session_projcache`
|
|
84
92
|
* domain at init, checkpoints live sessions on a throttled write-behind
|
|
85
|
-
* (count/interval triggers from {@link Config}) plus
|
|
86
|
-
* `turn/end
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* log a warning and the cache self-heals on the next write
|
|
93
|
+
* (count/interval triggers from {@link Config}) plus three mandatory points —
|
|
94
|
+
* session creation, `turn/end`, and session disposal (the live-to-cold
|
|
95
|
+
* moment) — and serves the
|
|
96
|
+
* cached rows for a session header. Every durable write is fail-soft:
|
|
97
|
+
* failures log a warning and the cache self-heals on the next write.
|
|
90
98
|
*/
|
|
91
99
|
var SessionProjectionCache = class extends Service {
|
|
92
100
|
config;
|
|
93
101
|
static inject = [
|
|
94
102
|
"storageDomain",
|
|
95
103
|
"sessionProjections",
|
|
96
|
-
"sessionPersistence",
|
|
97
104
|
"sessions"
|
|
98
105
|
];
|
|
99
106
|
static Config = Config;
|
|
@@ -115,7 +122,9 @@ var SessionProjectionCache = class extends Service {
|
|
|
115
122
|
* identity matches `expected`. A session id names a slot, not a lifecycle:
|
|
116
123
|
* a recreated id or a persistence store swapped under a surviving cache
|
|
117
124
|
* must not let an old record seed state folded from an unrelated log.
|
|
118
|
-
* Synchronous from the domain's in-memory state
|
|
125
|
+
* Synchronous from the domain's in-memory state — the same state every
|
|
126
|
+
* write mutated, so a read can never go around the write chain to the
|
|
127
|
+
* medium.
|
|
119
128
|
* @param id - the session whose record is read.
|
|
120
129
|
* @param expected - the log identity the caller holds (live or stored header).
|
|
121
130
|
* @returns the identity-matching record, or `undefined` (absent or unrelated).
|
|
@@ -127,32 +136,53 @@ var SessionProjectionCache = class extends Service {
|
|
|
127
136
|
}
|
|
128
137
|
/**
|
|
129
138
|
* The zero-I/O listing read: whole values viewed straight from the stored
|
|
130
|
-
* rows (version-matching keys only), each cut carried with its watermark
|
|
131
|
-
*
|
|
132
|
-
*
|
|
139
|
+
* rows (version-matching keys only), each cut carried with its watermark so
|
|
140
|
+
* a client value store can seed under its higher-seq-wins rule — as stale
|
|
141
|
+
* as the last durable checkpoint but never wrong, and never from an
|
|
133
142
|
* unrelated log (the caller's header is the identity witness). Fresher
|
|
134
|
-
* paths (the history tail baseline
|
|
135
|
-
*
|
|
143
|
+
* paths (the history tail baseline) supersede these values whenever a
|
|
144
|
+
* session is actually opened.
|
|
136
145
|
* @param meta - the listed session's header (identity witness; no log read).
|
|
146
|
+
* @param keys - optional projection keys required by the caller's audience.
|
|
137
147
|
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
|
|
138
148
|
* `undefined` when no usable row exists for this lifecycle.
|
|
139
149
|
*/
|
|
140
|
-
cachedSnapshot(meta) {
|
|
150
|
+
cachedSnapshot(meta, keys) {
|
|
141
151
|
const record = this.recordFor(meta.id, identityOf(meta));
|
|
142
152
|
if (record === void 0) return void 0;
|
|
143
|
-
const values = this.ctx.sessionProjections.viewCheckpoint(record.rows);
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
153
|
+
const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
|
|
154
|
+
const servedKeys = Object.keys(values);
|
|
155
|
+
if (servedKeys.length === 0) return void 0;
|
|
146
156
|
return {
|
|
147
|
-
asOfSeq: Math.min(...
|
|
157
|
+
asOfSeq: Math.min(...servedKeys.map((key) => record.rows[key].seq)),
|
|
148
158
|
values
|
|
149
159
|
};
|
|
150
160
|
}
|
|
151
161
|
/**
|
|
152
|
-
*
|
|
162
|
+
* Hydrate projection cells for an already-prepared Session without another
|
|
163
|
+
* persistence read. The cache seeds matching rows; the supplied exact log
|
|
164
|
+
* advances every unit to the observation cut. No checkpoint is written
|
|
165
|
+
* because the logical observation may contain recovery events not yet durable.
|
|
166
|
+
* @param session - exact unpublished Session retained by persistence.
|
|
167
|
+
* @param meta - observed lifecycle header.
|
|
168
|
+
* @param events - exact logical event prefix represented by the observation.
|
|
169
|
+
* @returns all projection values at the event cut.
|
|
170
|
+
*/
|
|
171
|
+
hydratePrepared(session, meta, events) {
|
|
172
|
+
const record = this.recordFor(meta.id, identityOf(meta));
|
|
173
|
+
if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, 0);
|
|
174
|
+
try {
|
|
175
|
+
return this.ctx.sessionProjections.hydrate(session, record.rows, events, 0);
|
|
176
|
+
} catch {
|
|
177
|
+
return this.ctx.sessionProjections.hydrate(session, {}, events, 0);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Durably checkpoint one live session NOW (all mandatory points call
|
|
153
182
|
* this; tests and carriers may too). The registry cut is snapshotted at
|
|
154
|
-
* this boundary (states are live references), then the
|
|
155
|
-
* replaced. NOT fail-soft — callers on the
|
|
183
|
+
* this boundary (states are live references), then the session's record is
|
|
184
|
+
* replaced on the domain's write chain. NOT fail-soft — callers on the
|
|
185
|
+
* fail-soft paths contain it.
|
|
156
186
|
* @param session - the live session to checkpoint.
|
|
157
187
|
* @returns resolution after durability and event emission.
|
|
158
188
|
*/
|
|
@@ -163,37 +193,22 @@ var SessionProjectionCache = class extends Service {
|
|
|
163
193
|
await this.put(session.id, identityOf(session.header), rows);
|
|
164
194
|
}
|
|
165
195
|
/**
|
|
166
|
-
* Cold-read one
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* @param
|
|
174
|
-
* @param
|
|
175
|
-
* @returns the
|
|
196
|
+
* Cold-read one session's projections from its complete log. Each unit is
|
|
197
|
+
* seeded from the identity-checked cached rows — the registry skips `apply`
|
|
198
|
+
* for the already-folded prefix (events at or below the row's `seq`) — and
|
|
199
|
+
* the refreshed checkpoint is written back (fail-soft, fire-and-forget), so
|
|
200
|
+
* the first cold read creates the cache row and later ones seed from it.
|
|
201
|
+
* The caller supplies the complete log in seq order: this service never
|
|
202
|
+
* consults the persistence layer.
|
|
203
|
+
* @param meta - the stored session header (identity witness).
|
|
204
|
+
* @param events - the session's complete log, in seq order.
|
|
205
|
+
* @returns the projection cut at the log end.
|
|
176
206
|
*/
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (floor === void 0) return {
|
|
183
|
-
asOfSeq: (await persistence.readFrom(id, 0, signal)).events.at(-1)?.seq ?? -1,
|
|
184
|
-
values: {}
|
|
185
|
-
};
|
|
186
|
-
let restored;
|
|
187
|
-
const tail = await persistence.readFrom(id, floor, signal);
|
|
188
|
-
const related = record === void 0 || identityMatches(record.identity, identityOf(tail.meta));
|
|
189
|
-
try {
|
|
190
|
-
if (!related) throw new Error("unrelated log identity");
|
|
191
|
-
restored = this.ctx.sessionProjections.restore(cached, tail.events, floor);
|
|
192
|
-
} catch {
|
|
193
|
-
const whole = await persistence.readFrom(id, 0, signal);
|
|
194
|
-
restored = this.ctx.sessionProjections.restore({}, whole.events, 0);
|
|
195
|
-
}
|
|
196
|
-
await this.putSoft(id, identityOf(tail.meta), restored.checkpoint, "cold-read write-back");
|
|
207
|
+
coldSnapshot(meta, events) {
|
|
208
|
+
const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identityOf(meta))?.rows ?? {}, events, 0, meta);
|
|
209
|
+
this.put(meta.id, identityOf(meta), restored.checkpoint).catch((error) => {
|
|
210
|
+
this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
|
|
211
|
+
});
|
|
197
212
|
return restored.snapshot;
|
|
198
213
|
}
|
|
199
214
|
installWritePath() {
|
|
@@ -216,6 +231,9 @@ var SessionProjectionCache = class extends Service {
|
|
|
216
231
|
this.flushSoft(session, "interval");
|
|
217
232
|
}, this.config.writeIntervalMs);
|
|
218
233
|
});
|
|
234
|
+
this.ctx.on("session/created", (session) => {
|
|
235
|
+
this.flushSoft(session, "create");
|
|
236
|
+
});
|
|
219
237
|
this.ctx.on("session/disposed", (session) => {
|
|
220
238
|
this.flushSoft(session, "detach");
|
|
221
239
|
this.markClean(session);
|
|
@@ -229,7 +247,7 @@ var SessionProjectionCache = class extends Service {
|
|
|
229
247
|
/**
|
|
230
248
|
* One fail-soft durable checkpoint. Every caller has work by construction:
|
|
231
249
|
* the throttle triggers only fire dirty (markClean clears the timer with
|
|
232
|
-
* the counter) and the
|
|
250
|
+
* the counter) and the mandatory points write unconditionally.
|
|
233
251
|
*/
|
|
234
252
|
async flushSoft(session, trigger) {
|
|
235
253
|
try {
|
|
@@ -257,14 +275,6 @@ var SessionProjectionCache = class extends Service {
|
|
|
257
275
|
rows: detached
|
|
258
276
|
});
|
|
259
277
|
}
|
|
260
|
-
/** Fail-soft {@link put}: cache writes must never fail their caller's read or event path. */
|
|
261
|
-
async putSoft(id, identity, rows, what) {
|
|
262
|
-
try {
|
|
263
|
-
await this.put(id, identity, rows);
|
|
264
|
-
} catch (error) {
|
|
265
|
-
this.ctx.logger.warn(`session projection cache: ${what} for "${id}" failed (cache stays stale): ${String(error)}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
278
|
requireTable() {
|
|
269
279
|
/* v8 ignore next -- Service.init assigns the table before the service becomes injectable */
|
|
270
280
|
if (this.table === void 0) throw new Error("session projection cache is not initialized");
|
package/lib/invariant.js
CHANGED
|
@@ -13,8 +13,8 @@ const inject = ["invariants"];
|
|
|
13
13
|
* the registry fold at its `seq` watermark) is only checkable by re-running the
|
|
14
14
|
* fold over the persisted log — duplicating the implementation rather than
|
|
15
15
|
* detecting drift — and its staleness is by design (fail-soft writes). The
|
|
16
|
-
* durable boundary is
|
|
17
|
-
*
|
|
16
|
+
* durable boundary is schema-validated by the cache's own zod parse on every
|
|
17
|
+
* read, and the read ladder's version/watermark guards are proven
|
|
18
18
|
* by the package spec.
|
|
19
19
|
*/
|
|
20
20
|
const install = () => {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persisted projection cache (`ctx.sessionProjectionCache`): durable
|
|
3
|
-
* checkpoints of every
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* authority:
|
|
3
|
+
* checkpoints of every projection unit's state, one record per session on
|
|
4
|
+
* the `session_projcache` domain (`per-record` layout — the shipped json
|
|
5
|
+
* backend stores one document per session under its root). Reads and writes
|
|
6
|
+
* share ONE coherent state: the domain's in-memory tables serve every read
|
|
7
|
+
* synchronously, and each write lands on the domain's write chain (durability
|
|
8
|
+
* first, then memory), so a read can never observe a disk write the memory
|
|
9
|
+
* has not applied, or a memory value the disk does not hold. The cache is a
|
|
10
|
+
* fold shortcut, never an authority: a row
|
|
11
|
+
* is possibly stale (its `seq` says how stale) but never wrong, so every
|
|
12
|
+
* write path is fail-soft (a lost write costs a longer tail replay on the
|
|
13
|
+
* next cold read) and a `ver` mismatch discards the row instead of migrating
|
|
14
|
+
* it. Design authority: the session-projection RFC
|
|
11
15
|
* (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
|
|
12
16
|
* @module @deepseek-ai/dsh-session-projection-cache
|
|
13
17
|
*/
|
|
14
18
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
15
19
|
import z from '@deepseek-ai/schemastery';
|
|
16
|
-
import type { Session,
|
|
17
|
-
import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection';
|
|
20
|
+
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session';
|
|
21
|
+
import type { ProjectionSnapshot, SessionProjectionMap } from '@deepseek-ai/dsh-session-projection';
|
|
18
22
|
export { checkpointIdentity, checkpointRecord, checkpointRow, projectionCacheDomainSpec } from './spec.ts';
|
|
19
23
|
export type { CheckpointIdentity, CheckpointRecord } from './spec.ts';
|
|
20
24
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -25,8 +29,9 @@ declare module '@deepseek-ai/cordis' {
|
|
|
25
29
|
/**
|
|
26
30
|
* Plugin config. Both throttle triggers are deployment choices with no
|
|
27
31
|
* universally correct value, so the composition states them explicitly
|
|
28
|
-
* (cordis.yml); the
|
|
29
|
-
* disposal) are policy, not tunables, and always
|
|
32
|
+
* (cordis.yml); the three mandatory write points (session creation,
|
|
33
|
+
* `turn/end`, and session disposal) are policy, not tunables, and always
|
|
34
|
+
* fire.
|
|
30
35
|
*/
|
|
31
36
|
export interface Config {
|
|
32
37
|
/** Committed events per session that force a durable checkpoint write between mandatory points. */
|
|
@@ -38,11 +43,11 @@ export declare const Config: z<Config>;
|
|
|
38
43
|
/**
|
|
39
44
|
* The persisted projection cache service. Opens the `session_projcache`
|
|
40
45
|
* domain at init, checkpoints live sessions on a throttled write-behind
|
|
41
|
-
* (count/interval triggers from {@link Config}) plus
|
|
42
|
-
* `turn/end
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* log a warning and the cache self-heals on the next write
|
|
46
|
+
* (count/interval triggers from {@link Config}) plus three mandatory points —
|
|
47
|
+
* session creation, `turn/end`, and session disposal (the live-to-cold
|
|
48
|
+
* moment) — and serves the
|
|
49
|
+
* cached rows for a session header. Every durable write is fail-soft:
|
|
50
|
+
* failures log a warning and the cache self-heals on the next write.
|
|
46
51
|
*/
|
|
47
52
|
export declare class SessionProjectionCache extends Service {
|
|
48
53
|
config: Config;
|
|
@@ -58,7 +63,9 @@ export declare class SessionProjectionCache extends Service {
|
|
|
58
63
|
* identity matches `expected`. A session id names a slot, not a lifecycle:
|
|
59
64
|
* a recreated id or a persistence store swapped under a surviving cache
|
|
60
65
|
* must not let an old record seed state folded from an unrelated log.
|
|
61
|
-
* Synchronous from the domain's in-memory state
|
|
66
|
+
* Synchronous from the domain's in-memory state — the same state every
|
|
67
|
+
* write mutated, so a read can never go around the write chain to the
|
|
68
|
+
* medium.
|
|
62
69
|
* @param id - the session whose record is read.
|
|
63
70
|
* @param expected - the log identity the caller holds (live or stored header).
|
|
64
71
|
* @returns the identity-matching record, or `undefined` (absent or unrelated).
|
|
@@ -66,52 +73,63 @@ export declare class SessionProjectionCache extends Service {
|
|
|
66
73
|
private recordFor;
|
|
67
74
|
/**
|
|
68
75
|
* The zero-I/O listing read: whole values viewed straight from the stored
|
|
69
|
-
* rows (version-matching keys only), each cut carried with its watermark
|
|
70
|
-
*
|
|
71
|
-
*
|
|
76
|
+
* rows (version-matching keys only), each cut carried with its watermark so
|
|
77
|
+
* a client value store can seed under its higher-seq-wins rule — as stale
|
|
78
|
+
* as the last durable checkpoint but never wrong, and never from an
|
|
72
79
|
* unrelated log (the caller's header is the identity witness). Fresher
|
|
73
|
-
* paths (the history tail baseline
|
|
74
|
-
*
|
|
80
|
+
* paths (the history tail baseline) supersede these values whenever a
|
|
81
|
+
* session is actually opened.
|
|
75
82
|
* @param meta - the listed session's header (identity witness; no log read).
|
|
83
|
+
* @param keys - optional projection keys required by the caller's audience.
|
|
76
84
|
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
|
|
77
85
|
* `undefined` when no usable row exists for this lifecycle.
|
|
78
86
|
*/
|
|
79
|
-
cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined;
|
|
87
|
+
cachedSnapshot(meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
|
|
80
88
|
/**
|
|
81
|
-
*
|
|
89
|
+
* Hydrate projection cells for an already-prepared Session without another
|
|
90
|
+
* persistence read. The cache seeds matching rows; the supplied exact log
|
|
91
|
+
* advances every unit to the observation cut. No checkpoint is written
|
|
92
|
+
* because the logical observation may contain recovery events not yet durable.
|
|
93
|
+
* @param session - exact unpublished Session retained by persistence.
|
|
94
|
+
* @param meta - observed lifecycle header.
|
|
95
|
+
* @param events - exact logical event prefix represented by the observation.
|
|
96
|
+
* @returns all projection values at the event cut.
|
|
97
|
+
*/
|
|
98
|
+
hydratePrepared(session: Session, meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot;
|
|
99
|
+
/**
|
|
100
|
+
* Durably checkpoint one live session NOW (all mandatory points call
|
|
82
101
|
* this; tests and carriers may too). The registry cut is snapshotted at
|
|
83
|
-
* this boundary (states are live references), then the
|
|
84
|
-
* replaced. NOT fail-soft — callers on the
|
|
102
|
+
* this boundary (states are live references), then the session's record is
|
|
103
|
+
* replaced on the domain's write chain. NOT fail-soft — callers on the
|
|
104
|
+
* fail-soft paths contain it.
|
|
85
105
|
* @param session - the live session to checkpoint.
|
|
86
106
|
* @returns resolution after durability and event emission.
|
|
87
107
|
*/
|
|
88
108
|
write(session: Session): Promise<void>;
|
|
89
109
|
/**
|
|
90
|
-
* Cold-read one
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* @param
|
|
98
|
-
* @param
|
|
99
|
-
* @returns the
|
|
110
|
+
* Cold-read one session's projections from its complete log. Each unit is
|
|
111
|
+
* seeded from the identity-checked cached rows — the registry skips `apply`
|
|
112
|
+
* for the already-folded prefix (events at or below the row's `seq`) — and
|
|
113
|
+
* the refreshed checkpoint is written back (fail-soft, fire-and-forget), so
|
|
114
|
+
* the first cold read creates the cache row and later ones seed from it.
|
|
115
|
+
* The caller supplies the complete log in seq order: this service never
|
|
116
|
+
* consults the persistence layer.
|
|
117
|
+
* @param meta - the stored session header (identity witness).
|
|
118
|
+
* @param events - the session's complete log, in seq order.
|
|
119
|
+
* @returns the projection cut at the log end.
|
|
100
120
|
*/
|
|
101
|
-
coldSnapshot(
|
|
121
|
+
coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot;
|
|
102
122
|
private installWritePath;
|
|
103
123
|
/**
|
|
104
124
|
* One fail-soft durable checkpoint. Every caller has work by construction:
|
|
105
125
|
* the throttle triggers only fire dirty (markClean clears the timer with
|
|
106
|
-
* the counter) and the
|
|
126
|
+
* the counter) and the mandatory points write unconditionally.
|
|
107
127
|
*/
|
|
108
128
|
private flushSoft;
|
|
109
129
|
/** Reset one session's dirty bookkeeping (its checkpoint is being written). */
|
|
110
130
|
private markClean;
|
|
111
131
|
/** Replace one session's stored record with its log identity and a detached snapshot of `rows`. */
|
|
112
132
|
private put;
|
|
113
|
-
/** Fail-soft {@link put}: cache writes must never fail their caller's read or event path. */
|
|
114
|
-
private putSoft;
|
|
115
133
|
private requireTable;
|
|
116
134
|
}
|
|
117
135
|
export default SessionProjectionCache;
|
package/lib/types/spec.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The projection-cache domain declaration: one `sessions` table keyed by
|
|
3
3
|
* {@link SessionId}, each record the full projection checkpoint for one
|
|
4
|
-
* session (`key → {ver, seq, val}` rows). The spec object
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* session (`key → {ver, seq, val}` rows). The spec object is the single
|
|
5
|
+
* source of the domain's identity, version, layout, and record schema; the
|
|
6
|
+
* storage-domain routing decides the medium (the shipped composition's json
|
|
7
|
+
* backend stores the domain `per-record`: one document per session under
|
|
8
|
+
* `<root>/session_projcache/sessions/`, so a checkpoint write rewrites one
|
|
9
|
+
* session's document instead of the whole unit).
|
|
9
10
|
* @module @deepseek-ai/dsh-session-projection-cache/src/spec
|
|
10
11
|
*/
|
|
11
12
|
import { z } from 'zod';
|
|
12
|
-
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
13
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
13
14
|
/**
|
|
14
15
|
* One persisted checkpoint row (the RFC's `(sessionId, key, ver, seq, val)`
|
|
15
16
|
* minus the two record keys). `val` is the unit's internal state — plain
|
|
@@ -28,9 +29,9 @@ export declare const checkpointRow: z.ZodObject<{
|
|
|
28
29
|
* that distinguish one session lifecycle from another under the same id. A
|
|
29
30
|
* session id names a slot, not a lifecycle — a deleted-then-recreated id, or
|
|
30
31
|
* a persistence root swapped under a surviving cache, would otherwise let an
|
|
31
|
-
* old
|
|
32
|
-
* log. Reads validate this against the live header (listing) or
|
|
33
|
-
* header (cold read) before accepting any
|
|
32
|
+
* old record pass every watermark check and seed state folded from an
|
|
33
|
+
* unrelated log. Reads validate this against the live header (listing) or
|
|
34
|
+
* the stored header (cold read) before accepting any record.
|
|
34
35
|
*/
|
|
35
36
|
export declare const checkpointIdentity: z.ZodObject<{
|
|
36
37
|
createdAt: z.ZodNumber;
|
|
@@ -58,13 +59,16 @@ export declare const checkpointRecord: z.ZodObject<{
|
|
|
58
59
|
/** One stored per-session checkpoint record, inferred from {@link checkpointRecord}. */
|
|
59
60
|
export type CheckpointRecord = z.infer<typeof checkpointRecord>;
|
|
60
61
|
/**
|
|
61
|
-
* The session-projcache domain spec.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* The session-projcache domain spec. The `per-record` layout scopes version
|
|
63
|
+
* bumps per session: after a bump, a stale session document is discarded on
|
|
64
|
+
* open (cache semantics — a stale or unreadable cache costs a longer tail
|
|
65
|
+
* replay, never a wrong value) while the rest of the domain stays usable,
|
|
66
|
+
* instead of rejecting the whole medium.
|
|
64
67
|
*/
|
|
65
68
|
export declare const projectionCacheDomainSpec: {
|
|
66
69
|
name: string;
|
|
67
70
|
version: number;
|
|
71
|
+
layout: "per-record";
|
|
68
72
|
tables: {
|
|
69
73
|
sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<SessionId, {
|
|
70
74
|
identity: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session-projection-cache",
|
|
3
|
-
"description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session
|
|
4
|
-
"version": "0.1.
|
|
3
|
+
"description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read",
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"zod": "^4.4.3",
|
|
36
|
-
"@deepseek-ai/
|
|
36
|
+
"@deepseek-ai/dsh-util-values": "^0.1.2-alpha.2",
|
|
37
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
40
|
-
"@deepseek-ai/dsh-session
|
|
41
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/
|
|
44
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
48
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
49
|
-
"@deepseek-ai/dsh-session
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/
|
|
52
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.
|
|
53
|
-
"@deepseek-ai/
|
|
47
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.2",
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
52
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.2"
|
|
54
54
|
}
|
|
55
55
|
}
|