refactor(session-projection): compact checkpoint row fields to ver/seq/val
The persisted row (sessionId, key, stateVersion, observedSeq, state) becomes (sessionId, key, ver, seq, val) — the cache medium repeats these three names for every unit of every session, so the long forms dominated the JSON payload. ProjectionCheckpointRow and the checkpointRow zod spec rename together; the domain spec bumps to v3 (cache semantics: the old medium is discarded, not migrated). The unit-facing declaration keeps stateVersion — only the persisted/checkpoint row shape changes.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session-projection/session-projection/README.md
|
||||
README.md: 2e026aab55933c96ba961481f9597bc18cbbe910
|
||||
README.zh.md: a3e0b0f46466d19321b0950dc41d06473a54a1ce
|
||||
README.md: f4898b8e567fa5998c18111c5f4e27a8a350a42e
|
||||
README.zh.md: 385862868df495a5c857d91c32f6503c3ef72025
|
||||
|
||||
@@ -23,7 +23,7 @@ Session-projection seam. It owns `ctx.sessionProjections`, the registry that DRI
|
||||
- **Same-reference means no work.** `apply` MUST return the same state reference for events that do not concern the unit; the drive gates the change feed on `Object.is`, so non-matching events cost one call and nothing downstream.
|
||||
- **Whole-value event rule (load-bearing).** A state-carrying log event MUST carry the complete post-change state, never a bare delta — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers).
|
||||
- **Synchronous unit discipline.** `init`/`apply`/`view` MUST be synchronous; carriers read `snapshot()` in the same tick as their page slice, which is what makes `asOfSeq` one consistent cut. An accidentally-async `view` returns a Promise, which fails the boundary `schema.parse` loudly.
|
||||
- **State is plain JSON, `stateVersion` is its invalidation anchor.** The persisted projection cache (a later phase) stores `(sessionId, key, stateVersion, observedSeq, stateJson)` rows; bump `stateVersion` whenever the state shape or the fold semantics change so stale rows are discarded instead of forward-applied into garbage.
|
||||
- **State is plain JSON, `stateVersion` is its invalidation anchor.** The persisted projection cache stores `(sessionId, key, ver, seq, val)` rows; bump `stateVersion` whenever the state shape or the fold semantics change so stale rows are discarded instead of forward-applied into garbage.
|
||||
- **No wire vocabulary here.** The registry exposes only the change feed and the snapshot read face; carriers (api-proxy) mint their own frames (`session/projection`) and blocks from them.
|
||||
- **Optional seam.** Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected; carriers use `ctx.get('sessionProjections')` and omit their block/frames entirely when the registry is absent.
|
||||
|
||||
@@ -43,5 +43,5 @@ None; projections never assemble or send provider requests.
|
||||
|
||||
- **Every tail page carries every registered key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states (a todo list, a goal snapshot), revisit if a domain's value grows large.
|
||||
- **Eager drive touches every unit per event** — cheap by construction (whole-value rule, same-reference gate), but a hot path would justify per-unit event-type prefilters, addable without contract change.
|
||||
- **The persisted projection cache is a later phase** — cells live in memory only; a restart rebuilds by folding the in-memory log on first touch. The `stateVersion` field is the forward-declared invalidation anchor for that phase.
|
||||
- **Registry cells live in memory only** — a restart rebuilds by folding the log on first touch; compositions that mount `dsh-session-projection-cache` seed that fold from persisted rows instead.
|
||||
- **Synchronous unit discipline is only partially mechanical** — the boundary `schema.parse` rejects a Promise-returning `view`, but an `apply` that blocks or reads torn non-session state is a review concern; the invariant companion documents why no runtime check exists.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- **同引用即无工作。** 对与单元无关的事件,`apply` 必须返回同一个状态引用;驱动以 `Object.is` 把守变更流,因此不匹配的事件只花一次调用,不产生任何下游工作。
|
||||
- **全量值事件规则(承重)。** 携带状态的日志事件必须携带变更后的完整状态,绝不携带裸增量——这让每次状态转移始终足够廉价,也让每个被供给的值自描述(对消费方即 last-wins)。
|
||||
- **单元的同步纪律。** `init`/`apply`/`view` 必须是同步的;载体在切出页面切片的同一 tick 内读取 `snapshot()`,`asOfSeq` 之所以是一个一致切面正系于此。误写成异步的 `view` 会返回 Promise,让边界的 `schema.parse` 当场大声失败。
|
||||
- **状态是纯 JSON,`stateVersion` 是其失效锚点。** 持久投影缓存(persisted projection cache,后续阶段)存储 `(sessionId, key, stateVersion, observedSeq, stateJson)` 行;状态形状或折叠语义一旦变化就递增 `stateVersion`,使陈旧行被丢弃,而不是被正向 apply 成垃圾。
|
||||
- **状态是纯 JSON,`stateVersion` 是其失效锚点。** 持久投影缓存(persisted projection cache)存储 `(sessionId, key, ver, seq, val)` 行;状态形状或折叠语义一旦变化就递增 `stateVersion`,使陈旧行被丢弃,而不是被正向 apply 成垃圾。
|
||||
- **本层没有协议词汇。** 注册表只暴露变更流与快照读取面;载体(api-proxy)据此自铸各自的帧(`session/projection`)与块。
|
||||
- **可选 seam。** 领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响;载体使用 `ctx.get('sessionProjections')`,注册表缺席时完全省略自己的块与帧。
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
|
||||
- **每个尾页携带每个已注册的 key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态(一张 todo 清单、一份 goal 快照)时可以接受,若某领域的值变大再重议。
|
||||
- **正向驱动(eager drive)逐事件触达每个单元**——按构造开销很低(全量值规则、同引用闸门),但若出现热点路径,可加按单元的事件类型预过滤,契约不变。
|
||||
- **持久投影缓存属于后续阶段**——cell 目前只活在内存里;重启后首次触达时靠折叠内存日志重建。`stateVersion` 字段是为该阶段预先声明的失效锚点。
|
||||
- **注册表 cell 只活在内存里**——重启后首次触达时靠折叠日志重建;挂载了 `dsh-session-projection-cache` 的组合改由持久行播种该折叠。
|
||||
- **单元同步纪律只有部分可机械把关**——边界 `schema.parse` 能拒绝返回 Promise 的 `view`,但阻塞的 `apply`、或读取撕裂的非会话状态的 `apply`,只能靠评审把关;invariant 配套记载了为何不存在运行时检查。
|
||||
|
||||
@@ -66,9 +66,9 @@ export interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
|
||||
view(state: S): SessionProjectionMap[K]
|
||||
/**
|
||||
* Persisted-cache invalidation anchor: bump whenever the state shape or the
|
||||
* fold semantics change, so persisted `(sessionId, key, stateVersion,
|
||||
* observedSeq, state)` rows from an older unit are discarded instead of
|
||||
* being forward-applied into garbage. Non-negative integer.
|
||||
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
|
||||
* rows from an older unit are discarded instead of being forward-applied
|
||||
* into garbage. Non-negative integer.
|
||||
*/
|
||||
stateVersion: number
|
||||
}
|
||||
@@ -99,20 +99,19 @@ export interface ProjectionSnapshot {
|
||||
|
||||
/**
|
||||
* One unit's checkpoint: its internal state (plain JSON by the unit
|
||||
* contract), the seq of the last event folded into it, and the
|
||||
* contract), the seq of the last event folded into it, and the unit
|
||||
* `stateVersion` that produced it — the persisted projection-cache row
|
||||
* `(sessionId, key, stateVersion, observedSeq, state)` minus the two outer
|
||||
* keys. A row is never authoritative, only a fold shortcut: `restore`
|
||||
* discards it on a `stateVersion` mismatch or when it claims events past the
|
||||
* stored log end.
|
||||
* `(sessionId, key, ver, seq, val)` minus the two outer keys. A row is
|
||||
* never authoritative, only a fold shortcut: `restore` discards it on a
|
||||
* version mismatch or when it claims events past the stored log end.
|
||||
*/
|
||||
export interface ProjectionCheckpointRow {
|
||||
/** The registering unit's `stateVersion` at fold time. */
|
||||
stateVersion: number
|
||||
/** Seq of the last event folded into `state`; -1 for the empty log. */
|
||||
observedSeq: number
|
||||
ver: number
|
||||
/** Seq of the last event folded into `val`; -1 for the empty log. */
|
||||
seq: number
|
||||
/** The unit's internal state — plain JSON per the unit contract. */
|
||||
state: unknown
|
||||
val: unknown
|
||||
}
|
||||
|
||||
/** Checkpoint rows keyed by projection key (one session's persisted cache value). */
|
||||
@@ -231,9 +230,9 @@ export class SessionProjectionRegistry extends Service {
|
||||
* State-level checkpoint of every registered unit for one session, read
|
||||
* from the watermark cache (missing cells fold lazily over the in-memory
|
||||
* log). This is the write side of the persisted projection cache: the
|
||||
* returned rows are the `(key → {stateVersion, observedSeq, state})` part
|
||||
* of the durable `(sessionId, key, stateVersion, observedSeq, state)`
|
||||
* rows. Every `state` is a DETACHED structured clone — never the live
|
||||
* returned rows are the `(key → {ver, seq, val})` part of the durable
|
||||
* `(sessionId, key, ver, seq, val)`
|
||||
* rows. Every `val` is a DETACHED structured clone — never the live
|
||||
* cell reference: the watermark cache is this registry's authoritative
|
||||
* mutable state, and a caller reaching the live reference could corrupt
|
||||
* every subsequent snapshot and frame through it (plain JSON by the unit
|
||||
@@ -246,9 +245,9 @@ export class SessionProjectionRegistry extends Service {
|
||||
for (const registration of this.registrations.values()) {
|
||||
const cell = this.cellFor(registration, session)
|
||||
rows[registration.def.key] = {
|
||||
stateVersion: registration.def.stateVersion,
|
||||
observedSeq: cell.observedSeq,
|
||||
state: structuredClone(cell.state),
|
||||
ver: registration.def.stateVersion,
|
||||
seq: cell.observedSeq,
|
||||
val: structuredClone(cell.state),
|
||||
}
|
||||
}
|
||||
return rows
|
||||
@@ -257,7 +256,7 @@ export class SessionProjectionRegistry extends Service {
|
||||
/**
|
||||
* The stored seq a {@link restore} tail read over `checkpoint` must start
|
||||
* at: one event BELOW the lowest usable watermark (a row is usable when
|
||||
* its `stateVersion` matches the live unit; an absent or mismatched row
|
||||
* its `ver` matches the live unit's `stateVersion`; an absent or mismatched row
|
||||
* pulls the floor to `0` — that key must refold the full log). The
|
||||
* one-below anchor is load-bearing: the tail then proves how far the
|
||||
* stored log still extends, so {@link restore} can detect a log that
|
||||
@@ -274,8 +273,8 @@ export class SessionProjectionRegistry extends Service {
|
||||
let floor: number | undefined
|
||||
for (const registration of this.registrations.values()) {
|
||||
const row = checkpoint[registration.def.key]
|
||||
const need = row !== undefined && row.stateVersion === registration.def.stateVersion
|
||||
? Math.max(row.observedSeq + 1, 0)
|
||||
const need = row !== undefined && row.ver === registration.def.stateVersion
|
||||
? Math.max(row.seq + 1, 0)
|
||||
: 0
|
||||
floor = floor === undefined ? need : Math.min(floor, need)
|
||||
}
|
||||
@@ -284,7 +283,7 @@ export class SessionProjectionRegistry extends Service {
|
||||
|
||||
/**
|
||||
* View a checkpoint's rows without any log read: for every registered
|
||||
* unit whose row's `stateVersion` matches, serve the schema-validated
|
||||
* unit whose row's `ver` matches, serve the schema-validated
|
||||
* `view` of the stored state; mismatched or absent rows leave their key
|
||||
* absent (a cold or listing consumer treats it as not-yet-available and a
|
||||
* fuller read path refolds it). The zero-I/O rung of the read ladder —
|
||||
@@ -297,8 +296,8 @@ export class SessionProjectionRegistry extends Service {
|
||||
for (const registration of this.registrations.values()) {
|
||||
const def = registration.def
|
||||
const row = checkpoint[def.key]
|
||||
if (row === undefined || row.stateVersion !== def.stateVersion) continue
|
||||
values[def.key] = def.schema.parse(def.view(row.state))
|
||||
if (row === undefined || row.ver !== def.stateVersion) continue
|
||||
values[def.key] = def.schema.parse(def.view(row.val))
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -311,9 +310,9 @@ export class SessionProjectionRegistry extends Service {
|
||||
* `readFrom(id, restoreFloor(checkpoint))` and that same floor as
|
||||
* `baseSeq`; the floor's one-below anchor makes the supplied end honest,
|
||||
* so a shrunk log is detected here. A row is usable iff its
|
||||
* `stateVersion` matches the live unit, it does not predate `baseSeq`
|
||||
* (`observedSeq >= baseSeq - 1`), and it does not claim events past the
|
||||
* supplied end (`observedSeq <= endSeq`); an unusable row is discarded
|
||||
* `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq`
|
||||
* (`seq >= baseSeq - 1`), and it does not claim events past the
|
||||
* supplied end (`seq <= endSeq`); an unusable row is discarded
|
||||
* and its key refolds from `init` — which is only sound over the full
|
||||
* log, so a discarded row with `baseSeq > 0` throws (the caller re-reads
|
||||
* from seq 0, e.g. after a crash-repair truncation shrank the log below
|
||||
@@ -334,22 +333,22 @@ export class SessionProjectionRegistry extends Service {
|
||||
const def = registration.def
|
||||
const row = checkpoint[def.key]
|
||||
const usable = row !== undefined
|
||||
&& row.stateVersion === def.stateVersion
|
||||
&& row.observedSeq >= baseSeq - 1
|
||||
&& row.observedSeq <= endSeq
|
||||
&& row.ver === def.stateVersion
|
||||
&& row.seq >= baseSeq - 1
|
||||
&& row.seq <= endSeq
|
||||
if (!usable && baseSeq > 0) {
|
||||
throw new Error(
|
||||
`session projection ${JSON.stringify(def.key)} cannot restore from seq ${baseSeq}: `
|
||||
+ 'its checkpoint row is missing, version-mismatched, or beyond the supplied log end; re-read from seq 0',
|
||||
)
|
||||
}
|
||||
let state = usable ? row.state : def.init()
|
||||
const from = usable ? row.observedSeq : baseSeq - 1
|
||||
let state = usable ? row.val : def.init()
|
||||
const from = usable ? row.seq : baseSeq - 1
|
||||
for (const event of events) {
|
||||
if (event.seq > from) state = def.apply(state, event)
|
||||
}
|
||||
values[def.key] = def.schema.parse(def.view(state))
|
||||
refreshed[def.key] = { stateVersion: def.stateVersion, observedSeq: endSeq, state }
|
||||
refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state }
|
||||
}
|
||||
return {
|
||||
snapshot: { asOfSeq: endSeq, values: values },
|
||||
|
||||
@@ -175,11 +175,11 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
ctx.sessionProjections.register({ ...countUnit(), stateVersion: 7 })
|
||||
const markEvent = mark(session, ['a'])
|
||||
const rows = ctx.sessionProjections.checkpoint(session)
|
||||
expect(rows['test/marks']).toEqual({ stateVersion: 1, observedSeq: markEvent.seq, state: { marks: ['a'] } })
|
||||
expect(rows['test/count']).toEqual({ stateVersion: 7, observedSeq: markEvent.seq, state: 1 })
|
||||
expect(rows['test/marks']).toEqual({ ver: 1, seq: markEvent.seq, val: { marks: ['a'] } })
|
||||
expect(rows['test/count']).toEqual({ ver: 7, seq: markEvent.seq, val: 1 })
|
||||
// Empty log: init-derived state at watermark -1.
|
||||
const fresh = ctx.sessions.create()
|
||||
expect(ctx.sessionProjections.checkpoint(fresh)['test/marks']).toEqual({ stateVersion: 1, observedSeq: -1, state: null })
|
||||
expect(ctx.sessionProjections.checkpoint(fresh)['test/marks']).toEqual({ ver: 1, seq: -1, val: null })
|
||||
})
|
||||
|
||||
it('checkpoint states are detached clones — mutating them cannot corrupt the watermark cache', async () => {
|
||||
@@ -188,11 +188,11 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
mark(session, ['a'])
|
||||
const rows = ctx.sessionProjections.checkpoint(session)
|
||||
// Hostile (or merely careless) consumer mutates the handed-out state.
|
||||
;(rows['test/marks']?.state as { marks: string[] }).marks.push('INJECTED')
|
||||
;(rows['test/marks']?.val as { marks: string[] }).marks.push('INJECTED')
|
||||
// The registry's authoritative cell is untouched: snapshot and a fresh
|
||||
// checkpoint both still serve the committed value.
|
||||
expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toEqual({ marks: ['a'] })
|
||||
expect(ctx.sessionProjections.checkpoint(session)['test/marks']?.state).toEqual({ marks: ['a'] })
|
||||
expect(ctx.sessionProjections.checkpoint(session)['test/marks']?.val).toEqual({ marks: ['a'] })
|
||||
})
|
||||
|
||||
it('restoreFloor anchors one below the lowest usable watermark and at 0 for missing or mismatched rows', async () => {
|
||||
@@ -204,18 +204,18 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
// Lowest usable watermark is count's 5 → the anchored tail starts AT 5
|
||||
// (one below the first needed seq 6), so the read proves seq 5 still exists.
|
||||
expect(ctx.sessionProjections.restoreFloor({
|
||||
'test/marks': { stateVersion: 1, observedSeq: 10, state: { marks: [] } },
|
||||
'test/count': { stateVersion: 1, observedSeq: 5, state: 6 },
|
||||
'test/marks': { ver: 1, seq: 10, val: { marks: [] } },
|
||||
'test/count': { ver: 1, seq: 5, val: 6 },
|
||||
})).toBe(5)
|
||||
// A version-mismatched row forces that key back to a full refold.
|
||||
expect(ctx.sessionProjections.restoreFloor({
|
||||
'test/marks': { stateVersion: 2, observedSeq: 10, state: { marks: [] } },
|
||||
'test/count': { stateVersion: 1, observedSeq: 5, state: 6 },
|
||||
'test/marks': { ver: 2, seq: 10, val: { marks: [] } },
|
||||
'test/count': { ver: 1, seq: 5, val: 6 },
|
||||
})).toBe(0)
|
||||
// A fresh (-1) row still needs the whole tail from 0.
|
||||
expect(ctx.sessionProjections.restoreFloor({
|
||||
'test/marks': { stateVersion: 1, observedSeq: -1, state: null },
|
||||
'test/count': { stateVersion: 1, observedSeq: -1, state: 0 },
|
||||
'test/marks': { ver: 1, seq: -1, val: null },
|
||||
'test/count': { ver: 1, seq: -1, val: 0 },
|
||||
})).toBe(0)
|
||||
})
|
||||
|
||||
@@ -230,8 +230,8 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
// marks row usable (watermark 2, tail starts at 3); count row mismatched — but
|
||||
// a mismatch with baseSeq > 0 cannot silently refold: it throws for a re-read.
|
||||
expect(() => ctx.sessionProjections.restore({
|
||||
'test/marks': { stateVersion: 1, observedSeq: 2, state: { marks: ['old'] } },
|
||||
'test/count': { stateVersion: 99, observedSeq: 2, state: 3 },
|
||||
'test/marks': { ver: 1, seq: 2, val: { marks: ['old'] } },
|
||||
'test/count': { ver: 99, seq: 2, val: 3 },
|
||||
}, tail, 3)).toThrow(/re-read from seq 0/)
|
||||
// The full-log re-read (baseSeq 0) refolds the mismatched key from init.
|
||||
const full: SessionEvent[] = [
|
||||
@@ -241,15 +241,15 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
...tail,
|
||||
]
|
||||
const { snapshot, checkpoint } = ctx.sessionProjections.restore({
|
||||
'test/marks': { stateVersion: 1, observedSeq: 2, state: { marks: ['old', '2'] } },
|
||||
'test/count': { stateVersion: 99, observedSeq: 2, state: 3 },
|
||||
'test/marks': { ver: 1, seq: 2, val: { marks: ['old', '2'] } },
|
||||
'test/count': { ver: 99, seq: 2, val: 3 },
|
||||
}, full, 0)
|
||||
expect(snapshot.asOfSeq).toBe(4)
|
||||
expect(snapshot.values['test/marks']).toEqual({ marks: ['new'] })
|
||||
expect(snapshot.values['test/count']).toBe(5) // refolded from init over all 5 events
|
||||
// The refreshed rows sit at the served cut, ready for a durable write-back.
|
||||
expect(checkpoint['test/marks']).toEqual({ stateVersion: 1, observedSeq: 4, state: { marks: ['new'] } })
|
||||
expect(checkpoint['test/count']).toEqual({ stateVersion: 1, observedSeq: 4, state: 5 })
|
||||
expect(checkpoint['test/marks']).toEqual({ ver: 1, seq: 4, val: { marks: ['new'] } })
|
||||
expect(checkpoint['test/count']).toEqual({ ver: 1, seq: 4, val: 5 })
|
||||
})
|
||||
|
||||
it('restore over a suffix folds only past each row watermark and serves an exact empty-tail cut', async () => {
|
||||
@@ -257,8 +257,8 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
ctx.sessionProjections.register(marksUnit())
|
||||
ctx.sessionProjections.register(countUnit())
|
||||
const rows = {
|
||||
'test/marks': { stateVersion: 1, observedSeq: 4, state: { marks: ['done'] } },
|
||||
'test/count': { stateVersion: 1, observedSeq: 2, state: 3 },
|
||||
'test/marks': { ver: 1, seq: 4, val: { marks: ['done'] } },
|
||||
'test/count': { ver: 1, seq: 2, val: 3 },
|
||||
}
|
||||
const tail: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 3, time: 3, data: { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
||||
@@ -273,8 +273,8 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
|
||||
// Empty tail (checkpoint is current): the cut sits at baseSeq - 1.
|
||||
const { snapshot: current } = ctx.sessionProjections.restore({
|
||||
'test/marks': { stateVersion: 1, observedSeq: 4, state: { marks: ['done'] } },
|
||||
'test/count': { stateVersion: 1, observedSeq: 4, state: 5 },
|
||||
'test/marks': { ver: 1, seq: 4, val: { marks: ['done'] } },
|
||||
'test/count': { ver: 1, seq: 4, val: 5 },
|
||||
}, [], 5)
|
||||
expect(current.asOfSeq).toBe(4)
|
||||
expect(current.values['test/count']).toBe(5)
|
||||
@@ -285,8 +285,8 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
ctx.sessionProjections.register(marksUnit())
|
||||
ctx.sessionProjections.register(countUnit())
|
||||
const values = ctx.sessionProjections.viewCheckpoint({
|
||||
'test/marks': { stateVersion: 1, observedSeq: 4, state: { marks: ['stored'] } },
|
||||
'test/count': { stateVersion: 99, observedSeq: 4, state: 5 }, // mismatched: absent
|
||||
'test/marks': { ver: 1, seq: 4, val: { marks: ['stored'] } },
|
||||
'test/count': { ver: 99, seq: 4, val: 5 }, // mismatched: absent
|
||||
})
|
||||
expect(values['test/marks']).toEqual({ marks: ['stored'] })
|
||||
expect('test/count' in values).toBe(false)
|
||||
@@ -296,7 +296,7 @@ describe('SessionProjectionRegistry drive', () => {
|
||||
it('restore rejects a row claiming events past the supplied log end (shrunk log ⇒ re-read)', async () => {
|
||||
const { ctx } = await harness()
|
||||
ctx.sessionProjections.register(countUnit())
|
||||
const rows = { 'test/count': { stateVersion: 1, observedSeq: 9, state: 10 } }
|
||||
const rows = { 'test/count': { ver: 1, seq: 9, val: 10 } }
|
||||
// The anchored floor sits ON the watermark, so the tail read must return
|
||||
// at least seq 9 from an intact log…
|
||||
const floor = ctx.sessionProjections.restoreFloor(rows)
|
||||
|
||||
Reference in New Issue
Block a user