ranuts 0.4.0-alpha.5 → 0.4.1-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/CLAUDE.md +180 -32
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/{color-DPV7TsVU.js → color-BTvZUuf-.js} +1 -1
- package/dist/i18n-DW21b_DJ.js +1 -0
- package/dist/index.js +1 -1
- package/dist/src/conversation/engine.d.ts +109 -0
- package/dist/src/conversation/index.d.ts +12 -0
- package/dist/src/conversation/index.js +1 -0
- package/dist/src/conversation/types.d.ts +99 -0
- package/dist/src/node/index.js +1 -1
- package/dist/src/stream/accumulator.d.ts +51 -0
- package/dist/src/stream/budget.d.ts +89 -0
- package/dist/src/stream/index.d.ts +25 -0
- package/dist/src/stream/index.js +1 -0
- package/dist/src/stream/sse.d.ts +51 -0
- package/dist/src/stream/types.d.ts +106 -0
- package/dist/src/utils/binary.d.ts +33 -0
- package/dist/src/utils/canvas.d.ts +1 -0
- package/dist/src/utils/device.d.ts +8 -0
- package/dist/src/utils/diff.d.ts +51 -0
- package/dist/src/utils/i18n/index.js +1 -1
- package/dist/src/utils/index.d.ts +12 -7
- package/dist/src/utils/index.js +1 -1
- package/dist/src/utils/number.d.ts +20 -0
- package/dist/src/utils/placement.d.ts +42 -5
- package/dist/src/utils/scroll.d.ts +93 -0
- package/dist/src/utils/secure.d.ts +58 -0
- package/dist/src/utils/str.d.ts +75 -0
- package/dist/test/budget.test.d.ts +1 -0
- package/dist/test/conversation.test.d.ts +1 -0
- package/dist/test/diff.test.d.ts +1 -0
- package/dist/test/formatBytes.test.d.ts +1 -0
- package/dist/test/scroll.test.d.ts +1 -0
- package/dist/test/stream.test.d.ts +1 -0
- package/dist/test/utils/secure.test.d.ts +1 -0
- package/dist/test/utils/text-boundary.test.d.ts +1 -0
- package/dist/umd/index.umd.cjs +1 -1
- package/dist/umd/utils/utils.umd.cjs +1 -1
- package/dist/utils-B4FnyUzW.js +1 -0
- package/docs/API.md +94 -6
- package/package.json +11 -2
- package/dist/i18n-Bq-f7htr.js +0 -1
- package/dist/utils-BSvtPyGl.js +0 -1
package/CLAUDE.md
CHANGED
|
@@ -13,7 +13,8 @@ and Node server tooling. Multi-entry, tree-shakeable, ships ESM + UMD.
|
|
|
13
13
|
|
|
14
14
|
- **[docs/API.md](docs/API.md)** — ⭐ generated, authoritative list of **every exported
|
|
15
15
|
symbol** with its signature + one-line description, grouped by entry point. Regenerate
|
|
16
|
-
with `npm run doc:api` after changing any public API
|
|
16
|
+
with `npm run doc:api` after changing any public API — CI fails if you forget. Read
|
|
17
|
+
this to find what exists.
|
|
17
18
|
- **This file** — orientation: which entry to import, runtime constraints, conventions,
|
|
18
19
|
and gotchas. Read this to decide _where_ things go and _how_ to add them.
|
|
19
20
|
|
|
@@ -27,15 +28,17 @@ from the **subpath that owns it** (below) — not from a deep source path.
|
|
|
27
28
|
Each subpath is an independent, tree-shakeable barrel. Import from the subpath, never from
|
|
28
29
|
`ranuts/dist/...` or `@/...` (that alias is internal to the source).
|
|
29
30
|
|
|
30
|
-
| Import from
|
|
31
|
-
|
|
|
32
|
-
| `ranuts`
|
|
33
|
-
| `ranuts/utils`
|
|
34
|
-
| `ranuts/node`
|
|
35
|
-
| `ranuts/visual`
|
|
36
|
-
| `ranuts/i18n`
|
|
37
|
-
| `ranuts/sw`
|
|
38
|
-
| `ranuts/vnode`
|
|
31
|
+
| Import from | Source | What | Runtime |
|
|
32
|
+
| --------------------- | --------------------------- | --------------------------------------------------------------- | ------------------ |
|
|
33
|
+
| `ranuts` | `index.ts` | Root barrel — re-exports the utils + visual surface | browser + node |
|
|
34
|
+
| `ranuts/utils` | `src/utils/index.ts` | DOM/BOM, string, object, number, color, time, etc. | browser + node\* |
|
|
35
|
+
| `ranuts/node` | `src/node/index.ts` | HTTP server, router, ws, fs, streams, middleware | **node only** |
|
|
36
|
+
| `ranuts/visual` | `src/utils/visual/index.ts` | 2D rendering engine (Canvas / WebGL / WebGPU) | **browser only** |
|
|
37
|
+
| `ranuts/i18n` | `src/utils/i18n.ts` | `I18nCore` / `createI18n` / `useI18n` — DOM-free | browser + node |
|
|
38
|
+
| `ranuts/sw` | `src/sw/index.ts` | Cache strategies + the precache protocol's SW half | **service worker** |
|
|
39
|
+
| `ranuts/vnode` | `src/vnode/index.ts` | Snabbdom-style virtual DOM (`h`, `init`, modules) | browser |
|
|
40
|
+
| `ranuts/stream` | `src/stream/index.ts` | SSE parsing + provider-neutral model-stream fold + token budget | browser + node |
|
|
41
|
+
| `ranuts/conversation` | `src/conversation/index.ts` | Event log → renderable conversation nodes | browser + node |
|
|
39
42
|
|
|
40
43
|
\* `ranuts/utils` is broad: most functions are browser-oriented (touch `window`/`document`),
|
|
41
44
|
but pure helpers (`str`, `obj`, `number`, `compose`, `cloneDeep`, …) run anywhere. Functions
|
|
@@ -73,10 +76,14 @@ packages/ranuts/
|
|
|
73
76
|
│ │ ├── canvas.ts # Canvas 2D geometry — roundRectByArc, fanShapedByArc, getLinearGradient
|
|
74
77
|
│ │ ├── placement.ts # computePlacement — flip/shift floating-panel placement (re-exported by ranui/utils/placement)
|
|
75
78
|
│ │ ├── tween.ts # easing curves (quad/cubic/quart/quint/sine/expo/circ)
|
|
79
|
+
│ │ ├── scroll.ts # createBottomFollower — follow the floor without fighting the reader
|
|
80
|
+
│ │ ├── diff.ts # diffLines — line-level diff as unified-style hunks
|
|
76
81
|
│ │ ├── visual/ # ranuts/visual — 2D rendering engine (see below)
|
|
77
82
|
│ │ └── totp/ # TOTP + hand-rolled SHA
|
|
78
83
|
│ ├── node/ # ranuts/node — mini HTTP framework
|
|
79
|
-
│
|
|
84
|
+
│ ├── vnode/ # ranuts/vnode — virtual DOM
|
|
85
|
+
│ ├── stream/ # ranuts/stream — SSE + StreamChunk + accumulator + budget
|
|
86
|
+
│ └── conversation/ # ranuts/conversation — event log → nodes, with publication cadence
|
|
80
87
|
├── bin/
|
|
81
88
|
│ ├── build.sh # build (tsc types + vite es/umd)
|
|
82
89
|
│ └── generate-api-docs.ts # ⭐ doc:api — emits docs/API.md from source + JSDoc
|
|
@@ -126,6 +133,137 @@ in `utils/number.ts`; `srgbToLinear`/`luma`/`blend*`/`saturation`/`cosinePalette
|
|
|
126
133
|
|
|
127
134
|
---
|
|
128
135
|
|
|
136
|
+
## Streaming a model response
|
|
137
|
+
|
|
138
|
+
`ranuts/stream` is four layers, each usable alone. The split is the point: only the
|
|
139
|
+
middle one is vendor-specific, and it is the one ranuts does **not** ship.
|
|
140
|
+
|
|
141
|
+
1. `parseEventStream(source)` — bytes → `ServerSentEvent`. Transport only, no model
|
|
142
|
+
concepts. Handles the parts that bite: a chunk boundary anywhere (including inside a
|
|
143
|
+
multi-byte character or between `\r` and `\n`), repeated `data:` joined with `\n`, one
|
|
144
|
+
space stripped after the colon, `:` comment keep-alives, a leading BOM, and a trailing
|
|
145
|
+
block the server never terminated.
|
|
146
|
+
2. `StreamChunk` — the provider-neutral vocabulary. **You write the mapping** from your
|
|
147
|
+
provider's event shape onto it; `mapEventStream(source, map)` is the seam, and returning
|
|
148
|
+
`[]` from the mapping is how a keep-alive or a `[DONE]` sentinel is dropped.
|
|
149
|
+
3. `createStreamAccumulator()` — folds chunks into blocks, so a view reads `snapshot()`,
|
|
150
|
+
`text()`, `reasoning()`, or `toolCalls()` instead of concatenating deltas itself.
|
|
151
|
+
4. `estimateTokens` / `addUsage` / `planCompaction` — the budget. A chat client that skips
|
|
152
|
+
this works for a week and then stops: every turn carries the whole history, the request
|
|
153
|
+
grows monotonically, and one day the provider refuses it as a wall.
|
|
154
|
+
|
|
155
|
+
Non-obvious rules the vocabulary encodes:
|
|
156
|
+
|
|
157
|
+
- **Group by `index`, never by arrival order.** Reasoning and text interleave, and several
|
|
158
|
+
tool calls open at once.
|
|
159
|
+
- **Tool arguments stay raw JSON text.** Half a JSON document is not a value. The
|
|
160
|
+
accumulator never parses `argumentsDelta`; parse once, after `finish`.
|
|
161
|
+
- **`block-start` is optional.** Several providers open a block with its first delta, so
|
|
162
|
+
the accumulator opens one on demand. Do not require it in your mapping either.
|
|
163
|
+
- **`block-end` wins.** When a provider sends an assembled block, it replaces whatever the
|
|
164
|
+
deltas accumulated.
|
|
165
|
+
- **`finish` terminates.** `usage` arrives before it; nothing after it.
|
|
166
|
+
|
|
167
|
+
Non-obvious rules the budget encodes:
|
|
168
|
+
|
|
169
|
+
- **`estimateTokens` is an estimate, and CJK is not four characters a token.** A BPE
|
|
170
|
+
vocabulary trained mostly on Latin script rarely merges CJK, so counting it at the Latin
|
|
171
|
+
rate underestimates a Chinese conversation about fourfold — the difference between
|
|
172
|
+
compacting in time and compacting after the provider refuses. A real tokenizer is a
|
|
173
|
+
per-model dependency measured in megabytes, and this decides _when to act_, not billing.
|
|
174
|
+
- **`addUsage` keeps a field one side omitted.** Providers differ in what they send, and a
|
|
175
|
+
running total that lost `outputTokens` the moment one response omitted it is worse than
|
|
176
|
+
no total.
|
|
177
|
+
- **`planCompaction` counts the summary it will add.** Without that it returns "fits" for a
|
|
178
|
+
request that does not. It also never touches the protected tail, and reports `fits: false`
|
|
179
|
+
rather than compacting it anyway — the caller decides, because nothing can be done there
|
|
180
|
+
that does not lose the user's own words.
|
|
181
|
+
- **It takes sizes, not messages.** What replaces a folded prefix, and which cut points a
|
|
182
|
+
wire format allows, are the caller's business.
|
|
183
|
+
|
|
184
|
+
## Projecting an event log into a conversation
|
|
185
|
+
|
|
186
|
+
`ranuts/conversation` turns an append-only log into the nodes a view renders. The
|
|
187
|
+
alternative — a view that switches on event type and mutates a component tree — puts
|
|
188
|
+
ordering, identity and partial-update reconciliation in the view, where every new kind of
|
|
189
|
+
content has to be threaded through by hand.
|
|
190
|
+
|
|
191
|
+
Here each kind of content is an independently registered state machine:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const engine = createConversationEngine<Event>({ definitions: [message, toolCall] });
|
|
195
|
+
engine.subscribe((nodes) => render(nodes));
|
|
196
|
+
engine.push(event);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A definition says which events are its own (`match`), folds them into its own state
|
|
200
|
+
(`start` / `update`), and never learns that the others exist. Adding a kind is adding a
|
|
201
|
+
definition, not editing a renderer.
|
|
202
|
+
|
|
203
|
+
Semantics you need before writing one:
|
|
204
|
+
|
|
205
|
+
- **Every definition sees every event.** Claims are independent; the engine does not stop
|
|
206
|
+
at the first match, so one log event can drive two nodes.
|
|
207
|
+
- **Order is fixed at `start`.** A node that keeps updating stays where it opened, so a
|
|
208
|
+
streaming message does not jump to the end of the list on every delta.
|
|
209
|
+
- **An `update` for an id with no open node is dropped.** That is honest when the start
|
|
210
|
+
event was trimmed from a paged window; inventing a node from a partial update would
|
|
211
|
+
render something that never existed.
|
|
212
|
+
- **`publication` is the streaming throttle.** `animation-frame` coalesces every delta
|
|
213
|
+
between two paints into one notification; `immediate` is for discrete facts, where
|
|
214
|
+
waiting a frame only adds latency; `none` records without waking the view. Cadence
|
|
215
|
+
escalates and never relaxes — an `immediate` while a frame is pending fires now and
|
|
216
|
+
cancels the frame.
|
|
217
|
+
- **`reader.previous(kind)` is backward-only**, so replaying the same log reproduces the
|
|
218
|
+
same view. A definition that could see nodes started after it would not.
|
|
219
|
+
- **`batch(run)` for any replay.** Without it, restoring a stored conversation is
|
|
220
|
+
quadratic: every event publishes, every publication hands subscribers the whole node
|
|
221
|
+
list, and a DOM subscriber walks all of it. Measured on a 600-message transcript that was
|
|
222
|
+
5.4 seconds of blocked main thread; batched it is 158ms. A live stream does not need it —
|
|
223
|
+
one delta changes one row.
|
|
224
|
+
- **A subscriber gets the keys that changed.** On a long transcript nearly every row is
|
|
225
|
+
unchanged on every frame, and rewriting all of them once per delta is the difference
|
|
226
|
+
between a transcript that streams and one that stalls. `undefined` means every node
|
|
227
|
+
changed, which is what a reset reports.
|
|
228
|
+
- **`truncate(key)` is what editing, regenerating and branching are made of.** All three
|
|
229
|
+
mean "the conversation diverges here", and what follows the divergence is no longer part
|
|
230
|
+
of it. It cuts by `seq` rather than by position — a node that opened before the cut
|
|
231
|
+
survives even if its latest update came after it, which is the same rule node order
|
|
232
|
+
follows — and returns how many nodes went, because zero is the caller's cue that its own
|
|
233
|
+
idea of the conversation is stale. Without it the only way back is `reset()` and a full
|
|
234
|
+
replay, which discards every node that did not change and makes the view flash.
|
|
235
|
+
|
|
236
|
+
`scheduler` is injectable, which is how the cadence tests run without a paint. The default
|
|
237
|
+
uses `requestAnimationFrame` in a browser and a microtask elsewhere.
|
|
238
|
+
|
|
239
|
+
`<r-conversation>` in ranui is the DOM consumer of all of this — see
|
|
240
|
+
[ranui/CLAUDE.md](../ranui/CLAUDE.md#r-conversation).
|
|
241
|
+
|
|
242
|
+
## Following the bottom of a scroller
|
|
243
|
+
|
|
244
|
+
`createBottomFollower` (from `ranuts/utils`) keeps an append-only view — a streaming
|
|
245
|
+
transcript, a log tail, a terminal — pinned to its floor without fighting the reader.
|
|
246
|
+
|
|
247
|
+
The hard part is telling your own scroll writes apart from the reader's. It uses an
|
|
248
|
+
**observed-top ledger**: every programmatic write records the `scrollTop` it produced, and
|
|
249
|
+
a scroll event whose position deviates from the ledger is the reader. That covers wheel,
|
|
250
|
+
touch, scrollbar, keyboard and stray `scrollIntoView` calls at once, without listening for
|
|
251
|
+
any input device — no list of device listeners is ever complete.
|
|
252
|
+
|
|
253
|
+
Using it:
|
|
254
|
+
|
|
255
|
+
- Call `follow()` whenever content changes; it is a no-op while the reader is scrolled up.
|
|
256
|
+
- Pass `observe: [column]` for growth that appends no node — streaming text grows an
|
|
257
|
+
existing node, so it has to be observed rather than announced.
|
|
258
|
+
- Before loading older content, `captureAnchor(row)` on a row that is actually on screen,
|
|
259
|
+
and `restoreAnchor()` after the prepend lands. Resolving to `null` means the row did not
|
|
260
|
+
survive; omitting the resolver reuses the captured element.
|
|
261
|
+
- A shrink-clamp never transfers ownership. A reader who had scrolled up stays unpinned
|
|
262
|
+
even when the clamp leaves them at the floor, and re-pins on their next scroll —
|
|
263
|
+
re-pinning on a clamp would take control back with no input from them.
|
|
264
|
+
- Scrolling is always instant. Smooth scrolling animates toward a moving target during
|
|
265
|
+
streaming, and its intermediate positions read as reader input on the next event.
|
|
266
|
+
|
|
129
267
|
## Conventions
|
|
130
268
|
|
|
131
269
|
### Adding a function to an existing module
|
|
@@ -214,6 +352,7 @@ npm run tsc # type-check only (tsc --noEmit)
|
|
|
214
352
|
npm run test # vitest run (node env)
|
|
215
353
|
npm run test:coverage
|
|
216
354
|
npm run doc:api # regenerate docs/API.md from source + JSDoc
|
|
355
|
+
npm run doc:api:check # verify it is fresh without writing (what CI runs)
|
|
217
356
|
```
|
|
218
357
|
|
|
219
358
|
- **Tests**: Vitest, **node environment** (no jsdom) — `vitest.config.ts` has no `environment`
|
|
@@ -222,32 +361,41 @@ npm run doc:api # regenerate docs/API.md from source + JSDoc
|
|
|
222
361
|
`getRgb`'s CSS fallback returns black without `document`).
|
|
223
362
|
- **`onConsoleLog` throws**: the test config fails any test that logs to console — don't leave
|
|
224
363
|
stray `console.*` in code under test.
|
|
364
|
+
- **Never assert on unseeded randomness**: sampling the real CSPRNG can only estimate the property
|
|
365
|
+
under test, and a window tight enough to catch a real bias also fails on chance every few
|
|
366
|
+
thousand runs — which reads as a mystery CI failure on one matrix leg. Replace the sample with
|
|
367
|
+
scripted bytes and assert the mapping exactly; `test/utils/secure.test.ts` has the `withBytes`
|
|
368
|
+
helper that stubs `crypto.getRandomValues` for the length of one draw.
|
|
225
369
|
|
|
226
370
|
---
|
|
227
371
|
|
|
228
372
|
## Gotchas
|
|
229
373
|
|
|
230
|
-
| Pitfall | Fix
|
|
231
|
-
| -------------------------------------------------------------------------------- |
|
|
232
|
-
| Importing from a deep source path or `@/…` | Import from the public subpath (`ranuts/utils`, `ranuts/visual`, …). `@/…` is build-internal only.
|
|
233
|
-
| Importing `ranuts/node` in browser code | It externalizes `fs`/`http`/`child_process` — server-only. Use `ranuts/utils` for browser helpers.
|
|
234
|
-
| `new Application()` then `render()` with WebGPU | Device init is async — use `await Application.create(...)`, then `app.start()`.
|
|
235
|
-
| Hand-editing `docs/API.md` | It's generated. Edit the source JSDoc and run `npm run doc:api`.
|
|
236
|
-
| Adding an export but it's missing from `docs/API.md` | Re-export it from the module's `index.ts` barrel, then `npm run doc:api`.
|
|
237
|
-
| New entry point not importable as `ranuts/foo` | Wire all three: `package.json` exports + `vite.config.ts` es entry + generator `ENTRIES`.
|
|
238
|
-
| `console.log` left in a function that a test calls | `vitest.config.ts` `onConsoleLog` throws — remove it or the test fails.
|
|
239
|
-
| Assuming a GPU/Canvas test can run in CI | Test env is node (no DOM/GPU). Test pure logic; gate visual checks behind a browser demo.
|
|
240
|
-
| Passing a non-`#rrggbb` color to the GPU backend and expecting it to fail | It won't — `getRgb` resolves any CSS color via the browser parser, matching the Canvas backend.
|
|
241
|
-
| Branching on `isClient` in new code | It's a module-load-time constant — wrong after SSR, unstubbable in tests. Check `typeof window === 'undefined'` inside the function.
|
|
242
|
-
| `
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
374
|
+
| Pitfall | Fix |
|
|
375
|
+
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
376
|
+
| Importing from a deep source path or `@/…` | Import from the public subpath (`ranuts/utils`, `ranuts/visual`, …). `@/…` is build-internal only. |
|
|
377
|
+
| Importing `ranuts/node` in browser code | It externalizes `fs`/`http`/`child_process` — server-only. Use `ranuts/utils` for browser helpers. |
|
|
378
|
+
| `new Application()` then `render()` with WebGPU | Device init is async — use `await Application.create(...)`, then `app.start()`. |
|
|
379
|
+
| Hand-editing `docs/API.md` | It's generated. Edit the source JSDoc and run `npm run doc:api`. |
|
|
380
|
+
| Adding an export but it's missing from `docs/API.md` | Re-export it from the module's `index.ts` barrel, then `npm run doc:api`. |
|
|
381
|
+
| New entry point not importable as `ranuts/foo` | Wire all three: `package.json` exports + `vite.config.ts` es entry + generator `ENTRIES`. |
|
|
382
|
+
| `console.log` left in a function that a test calls | `vitest.config.ts` `onConsoleLog` throws — remove it or the test fails. |
|
|
383
|
+
| Assuming a GPU/Canvas test can run in CI | Test env is node (no DOM/GPU). Test pure logic; gate visual checks behind a browser demo. |
|
|
384
|
+
| Passing a non-`#rrggbb` color to the GPU backend and expecting it to fail | It won't — `getRgb` resolves any CSS color via the browser parser, matching the Canvas backend. |
|
|
385
|
+
| Branching on `isClient` in new code | It's a module-load-time constant — wrong after SSR, unstubbable in tests. Check `typeof window === 'undefined'` inside the function. |
|
|
386
|
+
| Reaching for `randomString` / `getRandomString` to make a token or code | Both are `Math.random()` based and predictable. Use `secureRandomString` / `secureToken` from `secure.ts` for anything an attacker would want to guess. |
|
|
387
|
+
| Comparing a secret with `===` | Returns early at the first differing byte, leaking the shared prefix through timing. Use `safeEqual`. |
|
|
388
|
+
| Asserting a distribution drawn from the real CSPRNG | The window that catches the bias also fires on sampling noise. Script the bytes with `withBytes` (`test/utils/secure.test.ts`) and assert an equality. |
|
|
389
|
+
| Decoding chunked text per chunk | A multi-byte character straddling a boundary becomes replacement characters. `concatBytes` first, decode once. |
|
|
390
|
+
| `window.setTimeout` in a `ranuts/utils` module | Throws outside a document. Use the bare `setTimeout`. |
|
|
391
|
+
| A module-level factory that keeps mutable state shared by everything it produces | Give each produced function its own state (this is why `generateThrottle` was removed). |
|
|
392
|
+
| Reaching for `memoize` expecting per-argument caching | It only runs once and ignores later arguments. Use `once` (its new name), `singleFlight` for async, or a `Map` for real keyed caching. |
|
|
393
|
+
| Assuming `createSignal` deep-compares | It uses `Object.is` since 0.3. Pass `{ equals: isEqual }` when you want deep comparison. |
|
|
394
|
+
| Testing a boolean URL flag with `getAllQueryString(url).embed` | `?embed` (no value) is the usual spelling and reads as `''`, which is falsy. Use `queryFlag('embed')`. |
|
|
395
|
+
| `Promise.race([task, timeoutPromise])` for a deadline | Leaks the timer when the task wins. Use `withTimeout` / `withTimeoutFallback` — they always clear it. |
|
|
396
|
+
| Expecting `rewriteZip` to recompress | Rewritten and injected entries are written STORED, so the output is larger. Untouched entries keep their original compressed bytes. |
|
|
397
|
+
| Expecting `zip.ts` to open a >4 GiB archive | No ZIP64, no encryption, no multi-disk. `readZipEntries` returns `[]` for anything it cannot parse. |
|
|
398
|
+
| Calling `createHandoff().take()` twice | It is one-shot by design — the read and the delete share a transaction. The second call resolves `null`. |
|
|
251
399
|
|
|
252
400
|
---
|
|
253
401
|
|
package/README.md
CHANGED
|
@@ -61,6 +61,8 @@ Import as required. You can select:
|
|
|
61
61
|
- `ranuts/visual` — 2D rendering engine (Canvas / WebGL / WebGPU, **browser only**)
|
|
62
62
|
- `ranuts/sw` — cache strategies + the precache protocol's service-worker half (**service worker only**)
|
|
63
63
|
- `ranuts/vnode` — Snabbdom-style virtual DOM
|
|
64
|
+
- `ranuts/stream` — Server-Sent Events parsing, a provider-neutral fold of a streamed model response, and the token budget that decides when a history stops fitting
|
|
65
|
+
- `ranuts/conversation` — project an append-only event log into renderable conversation nodes
|
|
64
66
|
- `ranuts/i18n` — the i18n engine on its own, without the rest of `utils`
|
|
65
67
|
|
|
66
68
|
```js
|
package/README.zh-CN.md
CHANGED
|
@@ -60,6 +60,8 @@ npm install ranuts@latest --save
|
|
|
60
60
|
- `ranuts/visual` —— 2D 渲染引擎(Canvas / WebGL / WebGPU,**仅浏览器**)
|
|
61
61
|
- `ranuts/sw` —— 缓存策略 + 预缓存协议的 Service Worker 一侧(**仅 Service Worker**)
|
|
62
62
|
- `ranuts/vnode` —— Snabbdom 风格的虚拟 DOM
|
|
63
|
+
- `ranuts/stream` —— Server-Sent Events 解析、对流式模型响应的厂商中立折叠,以及判断历史何时装不下的 token 预算
|
|
64
|
+
- `ranuts/conversation` —— 将只追加的事件日志投影为可渲染的对话节点
|
|
63
65
|
- `ranuts/i18n` —— 单独的 i18n 引擎,不牵入 `utils` 的其余部分
|
|
64
66
|
|
|
65
67
|
```js
|