@mzzsfy/dsh-bug-perf 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,421 @@
1
+ // 列表瘦身的契约:只裁白名单、命中缓存不重建、增删事件失效、任何异常回落到原行为。
2
+ // 安全面:包装必须保留调用方 receiver(宿主在 list 内按 invocation 逐项鉴权),
3
+ // 缓存只服务 owner/本地调用,非 owner 主体不得命中共享缓存。
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { installListSlim, slimItem } from '../src/list-slim.mjs'
7
+
8
+ /** 一条带完整投影的列表项。 */
9
+ const item = (id, values) => ({ sessionId: id, updatedAt: 1, projections: { kind: 'cached', asOfSeq: 1, values } })
10
+
11
+ /** 造一个只实现本模块用到表面的上下文(缺服务时不回调,模拟 cordis inject 门控)。 */
12
+ function makeContext(list, { withController = true, controller: given, effectError } = {}) {
13
+ const handlers = new Map()
14
+ const disposers = []
15
+ const logs = { info: [], warn: [] }
16
+ const controller = withController ? (given ?? { ctx: {}, list }) : undefined
17
+ const ctx = {
18
+ get: (name) => (name === 'sessionController' ? controller : undefined),
19
+ on: (event, handler) => handlers.set(event, handler),
20
+ logger: {
21
+ info: (message) => logs.info.push(message),
22
+ warn: (message) => logs.warn.push(message),
23
+ },
24
+ inject: (deps, callback) => {
25
+ if (controller === undefined) return
26
+ callback({
27
+ sessionController: controller,
28
+ on: (event, handler) => handlers.set(event, handler),
29
+ effect: (factory) => {
30
+ if (effectError !== undefined) throw effectError
31
+ disposers.push(factory())
32
+ },
33
+ })
34
+ },
35
+ }
36
+ return { ctx, controller, handlers, disposers, logs }
37
+ }
38
+
39
+ const sample = () => [
40
+ item('a', { title: 'A', sessionListMetadata: { blank: false }, contextHeaders: [{ pad: 'x'.repeat(64) }] }),
41
+ item('b', { title: 'B', inbox: { 'next-turn': [], 'next-step': [] } }),
42
+ ]
43
+
44
+ /** 模拟宿主 service proxy 的 receiver:invocation 决定 remoteAccess 的判定。 */
45
+ const receiverOf = (principal) => ({ ctx: { invocation: principal === undefined ? undefined : { peer: { principal } } } })
46
+
47
+ test('裁剪只保留白名单里的投影', () => {
48
+ const slim = slimItem(sample()[0], ['title'])
49
+ assert.deepEqual(Object.keys(slim.projections.values), ['title'])
50
+ assert.equal(slim.projections.kind, 'cached')
51
+ assert.equal(slim.sessionId, 'a')
52
+ })
53
+
54
+ test('形状不符的条目原样返回', () => {
55
+ const bare = { sessionId: 'c' }
56
+ assert.equal(slimItem(bare, ['title']), bare)
57
+ assert.equal(slimItem({ projections: { values: null } }, ['title']).projections.values, null)
58
+ const array = { projections: { values: ['x'] } }
59
+ assert.equal(slimItem(array, ['title']), array)
60
+ })
61
+
62
+ test('开关关闭时包装保留,但每次调用透传原方法', async () => {
63
+ const original = async () => ({ items: sample() })
64
+ const { ctx, controller } = makeContext(original)
65
+ installListSlim(ctx, { isEnabled: () => false, warmupDelayMs: -1 })
66
+ assert.notEqual(controller.list, original)
67
+ const value = await controller.list({}, undefined)
68
+ assert.equal(value.items[0].projections.values.contextHeaders[0].pad.length, 64)
69
+ })
70
+
71
+ // Given 宿主没有 sessionController, When 装载, Then inject 不回调、不接管、不抛错
72
+ test('宿主没有 sessionController 时静默等待', () => {
73
+ const { ctx, logs } = makeContext(undefined, { withController: false })
74
+ assert.equal(installListSlim(ctx, { warmupDelayMs: -1 }), true)
75
+ assert.deepEqual(logs.warn, [])
76
+ })
77
+
78
+ // Given 服务在场但没有 list 方法, When 装载, Then 记录原因并放弃
79
+ test('controller 缺 list 方法时记录原因', () => {
80
+ const { ctx, logs } = makeContext(undefined, { controller: { ctx: {} } })
81
+ installListSlim(ctx, { warmupDelayMs: -1 })
82
+ assert.ok(logs.warn.some((message) => message.includes('list 不可用')))
83
+ })
84
+
85
+ test('第二次调用命中缓存,不重建', async () => {
86
+ let built = 0
87
+ const { ctx, controller } = makeContext(async () => {
88
+ built += 1
89
+ return { items: sample() }
90
+ })
91
+ installListSlim(ctx, { warmupDelayMs: -1 })
92
+ await controller.list({}, undefined)
93
+ const value = await controller.list({}, undefined)
94
+ assert.equal(built, 1)
95
+ assert.equal(value.items[0].projections.values.contextHeaders, undefined)
96
+ })
97
+
98
+ test('过期后重建', async () => {
99
+ let built = 0
100
+ const { ctx, controller } = makeContext(async () => {
101
+ built += 1
102
+ return { items: sample() }
103
+ })
104
+ installListSlim(ctx, { ttlMs: 0, warmupDelayMs: -1 })
105
+ await controller.list({}, undefined)
106
+ await controller.list({}, undefined)
107
+ assert.equal(built, 2)
108
+ })
109
+
110
+ test('会话增删让缓存失效', async () => {
111
+ let built = 0
112
+ const { ctx, controller, handlers } = makeContext(async () => {
113
+ built += 1
114
+ return { items: sample() }
115
+ })
116
+ installListSlim(ctx, { warmupDelayMs: -1 })
117
+ await controller.list({}, undefined)
118
+ handlers.get('api-session/added')()
119
+ await controller.list({}, undefined)
120
+ assert.equal(built, 2)
121
+ handlers.get('api-session/removed')()
122
+ await controller.list({}, undefined)
123
+ assert.equal(built, 3)
124
+ })
125
+
126
+ test('并发请求只触发一次重建', async () => {
127
+ let built = 0
128
+ const { ctx, controller } = makeContext(async () => {
129
+ built += 1
130
+ return { items: sample() }
131
+ })
132
+ installListSlim(ctx, { warmupDelayMs: -1 })
133
+ await Promise.all([controller.list({}, undefined), controller.list({}, undefined), controller.list({}, undefined)])
134
+ assert.equal(built, 1)
135
+ })
136
+
137
+ test('不同 signal 的并发调用共享一次构建,取消互不影响', async () => {
138
+ let built = 0
139
+ let release
140
+ const { ctx, controller } = makeContext(() => {
141
+ built += 1
142
+ return new Promise((resolve) => {
143
+ release = () => resolve({ items: sample() })
144
+ })
145
+ })
146
+ installListSlim(ctx, { warmupDelayMs: -1 })
147
+ const firstController = new AbortController()
148
+ const secondController = new AbortController()
149
+ const first = controller.list({}, firstController.signal)
150
+ const second = controller.list({}, secondController.signal)
151
+ firstController.abort()
152
+ release()
153
+ const [firstResult, secondResult] = await Promise.allSettled([first, second])
154
+ assert.equal(built, 1)
155
+ assert.equal(firstResult.status, 'rejected')
156
+ assert.equal(secondResult.status, 'fulfilled')
157
+ })
158
+
159
+ test('原方法抛错时错误照旧透传给调用方', async () => {
160
+ const { ctx, controller } = makeContext(async () => {
161
+ throw new Error('宿主故障')
162
+ })
163
+ installListSlim(ctx, { warmupDelayMs: -1 })
164
+ await assert.rejects(() => controller.list({}, undefined), /宿主故障/)
165
+ })
166
+
167
+ test('响应形状不符时原样透传,不缓存', async () => {
168
+ let built = 0
169
+ const { ctx, controller } = makeContext(async () => {
170
+ built += 1
171
+ return { sessions: sample() }
172
+ })
173
+ installListSlim(ctx, { warmupDelayMs: -1 })
174
+ const first = await controller.list({}, undefined)
175
+ const second = await controller.list({}, undefined)
176
+ assert.deepEqual(Object.keys(first), ['sessions'])
177
+ assert.equal(built, 2)
178
+ assert.notEqual(second, first)
179
+ })
180
+
181
+ test('把调用方 request 原样交给原方法', async () => {
182
+ const seen = []
183
+ const { ctx, controller } = makeContext(async (request) => {
184
+ seen.push(request)
185
+ return { items: sample() }
186
+ })
187
+ installListSlim(ctx, { warmupDelayMs: -1 })
188
+ const request = { cursor: 'abc' }
189
+ await controller.list(request, undefined)
190
+ assert.equal(seen[0], request)
191
+ })
192
+
193
+ test('预热在延迟后构建一次', async () => {
194
+ let built = 0
195
+ const { ctx } = makeContext(async () => {
196
+ built += 1
197
+ return { items: sample() }
198
+ })
199
+ installListSlim(ctx, { warmupDelayMs: 0 })
200
+ await new Promise((resolve) => setTimeout(resolve, 20))
201
+ assert.equal(built, 1)
202
+ })
203
+
204
+ test('卸载时恢复原方法', async () => {
205
+ const original = async () => ({ items: sample() })
206
+ const { ctx, controller, disposers } = makeContext(original)
207
+ installListSlim(ctx, { warmupDelayMs: -1 })
208
+ assert.notEqual(controller.list, original)
209
+ for (const dispose of disposers) dispose()
210
+ assert.equal(controller.list, original)
211
+ })
212
+
213
+ // Given 另一个插件在本插件之后包装了 list, When 本插件卸载, Then 不摘掉后装包装且链上透传
214
+ test('卸载不摘掉后装的包装', async () => {
215
+ const original = async () => ({ items: sample() })
216
+ const { ctx, controller, disposers } = makeContext(original)
217
+ installListSlim(ctx, { warmupDelayMs: -1 })
218
+ const wrapped = controller.list
219
+ const later = async function (request, signal) {
220
+ return wrapped.call(this, request, signal)
221
+ }
222
+ controller.list = later
223
+ for (const dispose of disposers) dispose()
224
+ assert.equal(controller.list, later)
225
+ const value = await controller.list({}, undefined)
226
+ assert.equal(value.items[0].projections.values.contextHeaders[0].pad.length, 64, '卸载后链上的旧包装应透传')
227
+ })
228
+
229
+ // Given 服务代理每次读取 list 都返回新引用(真实 cordis traceable), When 卸载, Then 按包装标记仍恢复原方法
230
+ test('服务代理返回新引用时卸载仍恢复原方法', async () => {
231
+ const original = async () => ({ items: sample() })
232
+ let current = original
233
+ const service = {
234
+ ctx: {},
235
+ get list() {
236
+ return new Proxy(current, {})
237
+ },
238
+ set list(value) {
239
+ current = value
240
+ },
241
+ }
242
+ const { ctx, disposers } = makeContext(undefined, { controller: service })
243
+ installListSlim(ctx, { warmupDelayMs: -1 })
244
+ const installed = current
245
+ assert.notEqual(service.list, installed, '代理每次返回新引用')
246
+ for (const dispose of disposers) dispose()
247
+ assert.notEqual(current, installed, '卸载后不再指向包装')
248
+ const value = await current({})
249
+ assert.equal(value.items[0].projections.values.contextHeaders[0].pad.length, 64, '恢复后透传完整投影')
250
+ })
251
+
252
+ // Given 宿主 effect 注册失败, When 装载, Then 包装与标记回滚,不留下无法卸载的包装
253
+ test('effect 注册失败时回滚包装', () => {
254
+ const original = async () => ({ items: sample() })
255
+ const { ctx, controller, logs } = makeContext(original, { effectError: new Error('inactive') })
256
+ installListSlim(ctx, { warmupDelayMs: -1 })
257
+ assert.equal(controller.list, original)
258
+ assert.equal(controller[Symbol.for('@mzzsfy/dsh-bug-perf/list-slim')], undefined)
259
+ assert.ok(logs.warn.some((message) => message.includes('装载失败')))
260
+ })
261
+
262
+ // Given 第一实例的安装标记不可见, When 第二实例再包装且第一实例卸载, Then 不摘掉第二实例的包装
263
+ test('不摘掉其它实例的包装', () => {
264
+ const original = async () => ({ items: sample() })
265
+ const first = makeContext(original)
266
+ installListSlim(first.ctx, { warmupDelayMs: -1 })
267
+ const firstWrapped = first.controller.list
268
+ delete first.controller[Symbol.for('@mzzsfy/dsh-bug-perf/list-slim')]
269
+ const second = makeContext(undefined, { controller: first.controller })
270
+ installListSlim(second.ctx, { warmupDelayMs: -1 })
271
+ const secondWrapped = first.controller.list
272
+ assert.notEqual(secondWrapped, firstWrapped)
273
+ for (const dispose of first.disposers) dispose()
274
+ assert.equal(first.controller.list, secondWrapped)
275
+ })
276
+
277
+ // Given 所有调用者都取消, When 最后一个等待者结束, Then 中止共享构建,后续调用可重试
278
+ test('所有调用者取消后中止共享构建', async () => {
279
+ let built = 0
280
+ const signals = []
281
+ const original = (request, signal) => {
282
+ built += 1
283
+ signals.push(signal)
284
+ if (built === 1) {
285
+ return new Promise((_, reject) => {
286
+ signal.addEventListener('abort', () => reject(new Error('aborted')), { once: true })
287
+ })
288
+ }
289
+ return Promise.resolve({ items: sample() })
290
+ }
291
+ const { ctx, controller } = makeContext(original)
292
+ installListSlim(ctx, { warmupDelayMs: -1 })
293
+ const first = new AbortController()
294
+ const second = new AbortController()
295
+ const waitFirst = controller.list({}, first.signal)
296
+ const waitSecond = controller.list({}, second.signal)
297
+ first.abort()
298
+ second.abort()
299
+ await Promise.allSettled([waitFirst, waitSecond])
300
+ assert.equal(signals[0].aborted, true, '所有等待者取消后应中止构建')
301
+ const third = await controller.list({}, undefined)
302
+ assert.equal(built, 2)
303
+ assert.deepEqual(third.items.map((entry) => entry.sessionId), ['a', 'b'])
304
+ })
305
+
306
+ // Given 列表构建在途, When 卸载, Then 中止内部构建
307
+ test('卸载中止在途构建', () => {
308
+ let internal
309
+ const original = (request, signal) => {
310
+ internal = signal
311
+ return new Promise(() => {})
312
+ }
313
+ const { ctx, controller, disposers } = makeContext(original)
314
+ installListSlim(ctx, { warmupDelayMs: -1 })
315
+ void controller.list({}, undefined)
316
+ for (const dispose of disposers) dispose()
317
+ assert.equal(internal.aborted, true)
318
+ })
319
+
320
+ // Given 同一服务重复装载, When 再次 install, Then 不叠加第二层包装
321
+ test('重复装载不叠加包装', () => {
322
+ const original = async () => ({ items: sample() })
323
+ const { ctx, controller } = makeContext(original)
324
+ installListSlim(ctx, { warmupDelayMs: -1 })
325
+ const first = controller.list
326
+ installListSlim(ctx, { warmupDelayMs: -1 })
327
+ assert.equal(controller.list, first)
328
+ })
329
+
330
+ // Given 宿主 list 按 this.ctx.invocation 过滤, When viewer 主体调用, Then 包装保留 receiver 且只拿到可见项
331
+ test('包装保留调用方 receiver,非 owner 由宿主逐项过滤', async () => {
332
+ const original = async function list() {
333
+ const principal = this?.ctx?.invocation?.peer?.principal
334
+ const items = principal?.kind === 'viewer' ? [item('shared', { title: 'S' })] : [item('secret', { title: 'X' }), item('shared', { title: 'S' })]
335
+ return { items }
336
+ }
337
+ const { ctx } = makeContext(original)
338
+ installListSlim(ctx, { warmupDelayMs: -1 })
339
+ const value = await ctx.get('sessionController').list.call(receiverOf({ kind: 'viewer' }), {}, undefined)
340
+ assert.deepEqual(value.items.map((entry) => entry.sessionId), ['shared'])
341
+ })
342
+
343
+ // Given owner 已把全量列表写入缓存, When viewer 调用, Then 不命中 owner 缓存、仍由宿主过滤
344
+ test('非 owner 不命中 owner 缓存', async () => {
345
+ let built = 0
346
+ const original = async function list() {
347
+ built += 1
348
+ const principal = this?.ctx?.invocation?.peer?.principal
349
+ const items = principal?.kind === 'viewer' ? [item('shared', { title: 'S' })] : [item('secret', { title: 'X' }), item('shared', { title: 'S' })]
350
+ return { items }
351
+ }
352
+ const { ctx, controller } = makeContext(original)
353
+ installListSlim(ctx, { warmupDelayMs: -1 })
354
+ const owner = await controller.list({}, undefined)
355
+ assert.deepEqual(owner.items.map((entry) => entry.sessionId), ['secret', 'shared'])
356
+ const viewer = await controller.list.call(receiverOf({ kind: 'viewer' }), {}, undefined)
357
+ assert.deepEqual(viewer.items.map((entry) => entry.sessionId), ['shared'])
358
+ assert.equal(built, 2, 'viewer 调用必须重新走原方法')
359
+ })
360
+
361
+ // Given 预热已构建 owner 缓存, When viewer 调用, Then 仍不命中缓存
362
+ test('预热缓存不对非 owner 生效', async () => {
363
+ let built = 0
364
+ const original = async function list() {
365
+ built += 1
366
+ const principal = this?.ctx?.invocation?.peer?.principal
367
+ const items = principal?.kind === 'viewer' ? [item('shared', { title: 'S' })] : [item('secret', { title: 'X' }), item('shared', { title: 'S' })]
368
+ return { items }
369
+ }
370
+ const { ctx, controller } = makeContext(original)
371
+ installListSlim(ctx, { warmupDelayMs: 0 })
372
+ await new Promise((resolve) => setTimeout(resolve, 20))
373
+ assert.equal(built, 1)
374
+ const viewer = await controller.list.call(receiverOf({ kind: 'viewer' }), {}, undefined)
375
+ assert.deepEqual(viewer.items.map((entry) => entry.sessionId), ['shared'])
376
+ assert.equal(built, 2)
377
+ })
378
+
379
+ // Given 列表构建在途, When 会话增删事件触发且同 signal 再次调用, Then 不复用失效前的构建
380
+ test('在途构建遇到失效后新调用重建', async () => {
381
+ let built = 0
382
+ const releases = []
383
+ const original = () => {
384
+ built += 1
385
+ return new Promise((resolve) => {
386
+ releases.push(() => resolve({ items: sample() }))
387
+ })
388
+ }
389
+ const { ctx, controller, handlers } = makeContext(original)
390
+ installListSlim(ctx, { warmupDelayMs: -1 })
391
+ const signal = new AbortController().signal
392
+ const pending = controller.list({}, signal)
393
+ handlers.get('api-session/added')()
394
+ const second = controller.list({}, signal)
395
+ assert.equal(built, 2, '失效后的新调用必须重建')
396
+ releases[0]()
397
+ releases[1]()
398
+ await Promise.all([pending, second])
399
+ })
400
+
401
+ // Given 在途构建期间失效, When 旧构建完成, Then 旧结果不写回缓存
402
+ test('在途构建遇到失效后不写回旧快照', async () => {
403
+ let built = 0
404
+ const releases = []
405
+ const original = () => {
406
+ built += 1
407
+ return new Promise((resolve) => {
408
+ releases.push(() => resolve({ items: sample() }))
409
+ })
410
+ }
411
+ const { ctx, controller, handlers } = makeContext(original)
412
+ installListSlim(ctx, { warmupDelayMs: -1 })
413
+ const pending = controller.list({}, undefined)
414
+ handlers.get('api-session/added')()
415
+ releases[0]()
416
+ await pending
417
+ const second = controller.list({}, undefined)
418
+ assert.equal(built, 2, '失效期间的结果不得作为缓存命中')
419
+ releases[1]()
420
+ await second
421
+ })