@earendil-works/chord 0.85.0 → 0.86.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.
- package/README.md +8 -8
- package/dist/delta/index.d.ts +3 -3
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +1057 -366
- package/dist/delta/index.js.map +1 -1
- package/package.json +2 -2
- package/src/delta/README.md +127 -42
package/src/delta/README.md
CHANGED
|
@@ -19,8 +19,10 @@ replica = apply(replica, tracker.flush());
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
The first `flush()` returns one operation containing the complete value. Each
|
|
22
|
-
later flush returns
|
|
23
|
-
value into the current value. It returns `[]` when
|
|
22
|
+
later flush returns operations whose application transforms the previously
|
|
23
|
+
published value into the current value. It returns `[]` when no tracked mutation
|
|
24
|
+
is pending, but a mutation window that restores its starting value may still
|
|
25
|
+
produce a redundant batch.
|
|
24
26
|
|
|
25
27
|
`applyImmutable()` copies only containers along changed paths and shares
|
|
26
28
|
unchanged subtrees. It does not mutate, clone, or freeze either complete input.
|
|
@@ -151,7 +153,7 @@ tracker.state.messages.push(message);
|
|
|
151
153
|
delete tracker.state.retry;
|
|
152
154
|
```
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
Operations are coalesced within a flush window when doing so is cheap and safe:
|
|
155
157
|
|
|
156
158
|
```ts
|
|
157
159
|
tracker.state.status = "starting";
|
|
@@ -159,8 +161,12 @@ tracker.state.status = "running";
|
|
|
159
161
|
tracker.flush(); // one set to "running"
|
|
160
162
|
```
|
|
161
163
|
|
|
164
|
+
The operation sequence is not canonical. Equivalent changes may use different
|
|
165
|
+
verbs, and mutations that cancel can still produce a nonempty batch. Consumers
|
|
166
|
+
must depend on the resulting value, not the exact tuples or their minimality.
|
|
167
|
+
|
|
162
168
|
Replacing an object or array is valid. Delta compares its properties and elements
|
|
163
|
-
with the
|
|
169
|
+
with the outgoing value at assignment time:
|
|
164
170
|
|
|
165
171
|
```ts
|
|
166
172
|
tracker.state.settings = {
|
|
@@ -180,8 +186,8 @@ Appending text produces an `a` operation:
|
|
|
180
186
|
tracker.state.output += "next line\n";
|
|
181
187
|
```
|
|
182
188
|
|
|
183
|
-
Moving a bounded text window forward produces `t` followed by `a` when the
|
|
184
|
-
suffix matches the new prefix:
|
|
189
|
+
Moving a bounded text window forward produces `t` followed by `a` when the
|
|
190
|
+
previous suffix matches the new prefix:
|
|
185
191
|
|
|
186
192
|
```ts
|
|
187
193
|
tracker.state.output = tracker.state.output.slice(200) + nextChunk;
|
|
@@ -199,29 +205,77 @@ tracker.state.messages.push(second);
|
|
|
199
205
|
tracker.state.messages.splice(3, 1, replacement);
|
|
200
206
|
```
|
|
201
207
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
Adjacent `push()` calls are normally coalesced into one tail `p`. Intervening
|
|
209
|
+
operations may keep them separate to preserve ordering. Changes to older
|
|
210
|
+
elements remain separate, and changes to newly pushed elements may be folded
|
|
211
|
+
into the pushed values when no structural operation intervenes.
|
|
212
|
+
|
|
213
|
+
Front or middle insertion and removal are recorded directly. Edits before and
|
|
214
|
+
after an index-changing operation remain ordered against the array generation
|
|
215
|
+
they addressed. Sorting, reversing, `fill()`, and `copyWithin()` emit a snapshot
|
|
216
|
+
of the affected array; repeated whole-array mutators can therefore produce a
|
|
217
|
+
redundant snapshot even when their combined result restores the prior value.
|
|
218
|
+
|
|
219
|
+
Sparse arrays are unsupported. Writing beyond the next index throws. Increasing
|
|
220
|
+
`length` creates explicit `null` elements; decreasing it removes elements.
|
|
205
221
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
`fill()` and `copyWithin()` keep normal JavaScript reference semantics; an object
|
|
223
|
+
they place at several indices is published at each.
|
|
224
|
+
|
|
225
|
+
### Large mutation windows
|
|
226
|
+
|
|
227
|
+
#### Build once, assign once
|
|
228
|
+
|
|
229
|
+
Every object or array assignment is diffed immediately. Do not repeatedly assign
|
|
230
|
+
large intermediate values before one flush:
|
|
209
231
|
|
|
210
232
|
```ts
|
|
211
|
-
|
|
212
|
-
tracker.state.
|
|
213
|
-
|
|
233
|
+
// Avoid: traverses every intermediate tree.
|
|
234
|
+
for (const frame of frames) tracker.state.view = render(frame);
|
|
235
|
+
|
|
236
|
+
// Prefer: only the final tree crosses the tracked boundary.
|
|
237
|
+
const nextView = frames.reduce((view, frame) => renderInto(view, frame), initialView);
|
|
238
|
+
tracker.state.view = nextView;
|
|
214
239
|
```
|
|
215
240
|
|
|
216
|
-
|
|
217
|
-
array, rather than only the changed element. When batching is under your control,
|
|
218
|
-
flush the structural change before editing elements at their new indices.
|
|
241
|
+
For a few changes, mutate the leaves directly:
|
|
219
242
|
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
```ts
|
|
244
|
+
for (const update of updates) {
|
|
245
|
+
tracker.state.view.rows[update.index]!.status = update.status;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Do not cancel whole-array mutators
|
|
250
|
+
|
|
251
|
+
`sort()`, `reverse()`, `fill()`, and `copyWithin()` record snapshots. Cancelling
|
|
252
|
+
them still publishes the final snapshot:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// Avoid: final value is unchanged, but a snapshot may still be sent.
|
|
256
|
+
tracker.state.items.reverse();
|
|
257
|
+
tracker.state.items.reverse();
|
|
222
258
|
|
|
223
|
-
|
|
224
|
-
|
|
259
|
+
// Prefer: decide before mutating tracked state.
|
|
260
|
+
if (needsReverse) tracker.state.items.reverse();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Publish large inserts and edit sets in chunks
|
|
264
|
+
|
|
265
|
+
Pending inserted values are cloned for replica ownership. Very large unflushed
|
|
266
|
+
pushes therefore temporarily retain both the live values and their operation
|
|
267
|
+
payloads. Long operation/path histories may also collapse to a complete base
|
|
268
|
+
batch, increasing snapshot and wire cost.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
for (const chunk of chunks(items, 1_000)) {
|
|
272
|
+
tracker.state.items.push(...chunk);
|
|
273
|
+
replica = apply(replica, tracker.flush()); // send each batch in a real producer
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Use the same pattern for large sets of unrelated edits: apply a bounded chunk,
|
|
278
|
+
publish it, then continue.
|
|
225
279
|
|
|
226
280
|
### Optional properties
|
|
227
281
|
|
|
@@ -243,42 +297,65 @@ array position or explicit empty value must remain present.
|
|
|
243
297
|
|
|
244
298
|
## State ownership
|
|
245
299
|
|
|
246
|
-
The object passed to `track()` becomes tracker-owned
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
After insertion, a retained reference may be read but must not be mutated or
|
|
250
|
-
inserted at another live location. The tracker relies on this ownership rule; it
|
|
251
|
-
does not recursively validate values or detect aliases:
|
|
300
|
+
The object passed to `track()` becomes tracker-owned, as does any object later
|
|
301
|
+
assigned into state or inserted into an array. Mutate through `tracker.state`:
|
|
252
302
|
|
|
253
303
|
```ts
|
|
254
304
|
const item = { status: "new" };
|
|
255
305
|
tracker.state.item = item;
|
|
256
306
|
|
|
257
|
-
tracker.state.item.status = "ready"; //
|
|
258
|
-
item.status = "broken"; //
|
|
259
|
-
tracker.state.other = item; // unsupported: one object at two live paths
|
|
307
|
+
tracker.state.item.status = "ready"; // tracked
|
|
308
|
+
item.status = "broken"; // NOT tracked: silently diverges from the replica
|
|
260
309
|
```
|
|
261
310
|
|
|
262
|
-
|
|
311
|
+
A reference retained from `tracker.state` stays correct across operations that
|
|
312
|
+
renumber it, and across the removal of the element it points at:
|
|
263
313
|
|
|
264
314
|
```ts
|
|
265
|
-
tracker.state.items
|
|
266
|
-
tracker.state.items.
|
|
315
|
+
const held = tracker.state.items[2];
|
|
316
|
+
tracker.state.items.unshift(other);
|
|
317
|
+
held.name = "edited"; // publishes items[3].name
|
|
318
|
+
|
|
319
|
+
tracker.state.items.splice(3, 1);
|
|
320
|
+
held.name = "gone"; // element is no longer in the tree: mutated, nothing published
|
|
267
321
|
```
|
|
268
322
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
323
|
+
One object may occupy several paths. Each live path is published:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
tracker.state.a = tracker.state.items[0];
|
|
327
|
+
tracker.state.items[0].k = 1; // publishes both a.k and items[0].k
|
|
328
|
+
```
|
|
272
329
|
|
|
273
330
|
Tracked state must be a mutable JSON tree:
|
|
274
331
|
|
|
275
332
|
- strings, booleans, finite numbers, `null`, arrays, and plain objects;
|
|
276
|
-
- no cycles
|
|
333
|
+
- no cycles;
|
|
277
334
|
- no sparse arrays, accessors, frozen objects, symbols, classes, functions,
|
|
278
335
|
`Map`, or `Set`.
|
|
279
336
|
|
|
280
|
-
|
|
281
|
-
|
|
337
|
+
## Proxy lifetime and large reads
|
|
338
|
+
|
|
339
|
+
Proxy caches are weak. Reading a subtree does not permanently retain its proxies
|
|
340
|
+
just because the underlying plain objects remain in the document. A proxy still
|
|
341
|
+
held by application code keeps its identity; held descendants retain the ancestor
|
|
342
|
+
tracking metadata needed to follow array reindexing. Explicit alias locations are
|
|
343
|
+
remembered separately from the lifetime of their public proxies.
|
|
344
|
+
|
|
345
|
+
Collection is automatic, not a `flush()` side effect. JavaScript keeps newly
|
|
346
|
+
created or dereferenced `WeakRef` targets alive until the current job ends, and
|
|
347
|
+
finalizer cleanup can run later. A synchronous traversal can therefore still have
|
|
348
|
+
a substantial allocation peak. Retained-memory measurements must allow event-loop
|
|
349
|
+
turns as well as GC; a synchronous `gc()` immediately after the traversal is not
|
|
350
|
+
sufficient to measure weak-cache reclamation.
|
|
351
|
+
|
|
352
|
+
This does not eliminate proxy construction/trap costs or full comparisons on
|
|
353
|
+
container assignment. `tracker.target` is available for read-only bulk inspection
|
|
354
|
+
without creating proxies. Never mutate through it; all tracked mutations must go
|
|
355
|
+
through `tracker.state` or a proxy obtained from it.
|
|
356
|
+
|
|
357
|
+
See the [delta investigation findings](../../../durable/docs/chord-delta-findings.md)
|
|
358
|
+
for the full-traversal regression, measured trade-offs, and reproduction commands.
|
|
282
359
|
|
|
283
360
|
## Tracker lifecycle
|
|
284
361
|
|
|
@@ -292,6 +369,14 @@ tracker.state = replacement; // replace the root; next flush is complete
|
|
|
292
369
|
`discard()` intentionally prevents current changes from reaching existing
|
|
293
370
|
replicas. Use it only when those replicas do not need the discarded changes.
|
|
294
371
|
|
|
372
|
+
`flush()` guarantees convergence, not a minimal or canonical diff. Any nonempty
|
|
373
|
+
batch advances replicated-state sequence numbers and notifies subscribers, even
|
|
374
|
+
if applying it leaves the value deeply equal to the previous revision. To bound
|
|
375
|
+
pending operation and path history, a sufficiently long mutation window may
|
|
376
|
+
collapse to a complete base batch automatically. This bounds accumulated log
|
|
377
|
+
metadata, not payload bytes or peak allocation, and trades one full-value
|
|
378
|
+
snapshot for an additional recovery point.
|
|
379
|
+
|
|
295
380
|
`apply()` adopts object and array payloads from its input batch. Do not freeze a
|
|
296
381
|
batch before applying it, and do not apply one in-memory batch to multiple
|
|
297
382
|
mutable replicas unless each replica owns that batch. A serialized and decoded
|
|
@@ -308,8 +393,8 @@ changed the replica.
|
|
|
308
393
|
- Delta assumes one authoritative writer and ordered delivery. Sequence numbers,
|
|
309
394
|
gap detection, retries, and persistence policy belong to the surrounding
|
|
310
395
|
protocol or storage format.
|
|
311
|
-
- Object identity is not replicated.
|
|
312
|
-
|
|
396
|
+
- Object identity is not replicated. One object at several paths publishes each
|
|
397
|
+
path separately, and a replica holds a distinct value at each.
|
|
313
398
|
- Object key insertion order is not replicated. Do not compare or hash replicas
|
|
314
399
|
using serialized key order.
|
|
315
400
|
- Array operations that change indices may publish a wider array region, as
|