@earendil-works/chord 1.4.0 → 1.5.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/CHANGELOG.md +41 -0
- package/README.md +79 -14
- package/dist/cell.d.ts +7 -7
- package/dist/cell.d.ts.map +1 -1
- package/dist/cell.js.map +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/contract.d.ts +51 -52
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +19 -24
- package/dist/contract.js.map +1 -1
- package/dist/delta/apply-immutable-trusted.js +3 -4
- package/dist/delta/apply-immutable-trusted.js.map +1 -1
- package/dist/delta/diff.d.ts.map +1 -1
- package/dist/delta/diff.js +5 -3
- package/dist/delta/diff.js.map +1 -1
- package/dist/delta/index.d.ts +9 -1
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +15 -11
- package/dist/delta/index.js.map +1 -1
- package/dist/delta/pending.d.ts +36 -0
- package/dist/delta/pending.d.ts.map +1 -0
- package/dist/delta/pending.js +143 -0
- package/dist/delta/pending.js.map +1 -0
- package/dist/delta/tracker.d.ts +12 -0
- package/dist/delta/tracker.d.ts.map +1 -1
- package/dist/delta/tracker.js +109 -39
- package/dist/delta/tracker.js.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +9 -7
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/next/contracts/accepts.d.ts +81 -0
- package/dist/next/contracts/accepts.d.ts.map +1 -0
- package/dist/next/contracts/accepts.js +123 -0
- package/dist/next/contracts/accepts.js.map +1 -0
- package/dist/next/contracts/contract.d.ts +121 -0
- package/dist/next/contracts/contract.d.ts.map +1 -0
- package/dist/next/contracts/contract.js +50 -0
- package/dist/next/contracts/contract.js.map +1 -0
- package/dist/next/contracts/declare.d.ts +107 -0
- package/dist/next/contracts/declare.d.ts.map +1 -0
- package/dist/next/contracts/declare.js +136 -0
- package/dist/next/contracts/declare.js.map +1 -0
- package/dist/next/contracts/evolve.d.ts +92 -0
- package/dist/next/contracts/evolve.d.ts.map +1 -0
- package/dist/next/contracts/evolve.js +179 -0
- package/dist/next/contracts/evolve.js.map +1 -0
- package/dist/next/contracts/fingerprint.d.ts +16 -0
- package/dist/next/contracts/fingerprint.d.ts.map +1 -0
- package/dist/next/contracts/fingerprint.js +50 -0
- package/dist/next/contracts/fingerprint.js.map +1 -0
- package/dist/next/contracts/history.d.ts +71 -0
- package/dist/next/contracts/history.d.ts.map +1 -0
- package/dist/next/contracts/history.js +3 -0
- package/dist/next/contracts/history.js.map +1 -0
- package/dist/next/contracts/host-kind.d.ts +40 -0
- package/dist/next/contracts/host-kind.d.ts.map +1 -0
- package/dist/next/contracts/host-kind.js +37 -0
- package/dist/next/contracts/host-kind.js.map +1 -0
- package/dist/next/contracts/index.d.ts +7 -0
- package/dist/next/contracts/index.d.ts.map +1 -0
- package/dist/next/contracts/index.js +6 -0
- package/dist/next/contracts/index.js.map +1 -0
- package/dist/next/contracts/internal.d.ts +10 -0
- package/dist/next/contracts/internal.d.ts.map +1 -0
- package/dist/next/contracts/internal.js +9 -0
- package/dist/next/contracts/internal.js.map +1 -0
- package/dist/next/contracts/names.d.ts +3 -0
- package/dist/next/contracts/names.d.ts.map +1 -0
- package/dist/next/contracts/names.js +13 -0
- package/dist/next/contracts/names.js.map +1 -0
- package/dist/next/contracts/params.d.ts +28 -0
- package/dist/next/contracts/params.d.ts.map +1 -0
- package/dist/next/contracts/params.js +50 -0
- package/dist/next/contracts/params.js.map +1 -0
- package/dist/next/contracts/schema.d.ts +102 -0
- package/dist/next/contracts/schema.d.ts.map +1 -0
- package/dist/next/contracts/schema.js +103 -0
- package/dist/next/contracts/schema.js.map +1 -0
- package/dist/next/contracts/snapshot.d.ts +16 -0
- package/dist/next/contracts/snapshot.d.ts.map +1 -0
- package/dist/next/contracts/snapshot.js +167 -0
- package/dist/next/contracts/snapshot.js.map +1 -0
- package/dist/next/data/args.d.ts +50 -0
- package/dist/next/data/args.d.ts.map +1 -0
- package/dist/next/data/args.js +83 -0
- package/dist/next/data/args.js.map +1 -0
- package/dist/next/data/errors.d.ts +73 -0
- package/dist/next/data/errors.d.ts.map +1 -0
- package/dist/next/data/errors.js +107 -0
- package/dist/next/data/errors.js.map +1 -0
- package/dist/next/data/failure.d.ts +26 -0
- package/dist/next/data/failure.d.ts.map +1 -0
- package/dist/next/data/failure.js +72 -0
- package/dist/next/data/failure.js.map +1 -0
- package/dist/next/data/index.d.ts +4 -0
- package/dist/next/data/index.d.ts.map +1 -0
- package/dist/next/data/index.js +4 -0
- package/dist/next/data/index.js.map +1 -0
- package/dist/next/data/internal.d.ts +7 -0
- package/dist/next/data/internal.d.ts.map +1 -0
- package/dist/next/data/internal.js +8 -0
- package/dist/next/data/internal.js.map +1 -0
- package/dist/next/data/params.d.ts +20 -0
- package/dist/next/data/params.d.ts.map +1 -0
- package/dist/next/data/params.js +31 -0
- package/dist/next/data/params.js.map +1 -0
- package/dist/next/data/principal.d.ts +17 -0
- package/dist/next/data/principal.d.ts.map +1 -0
- package/dist/next/data/principal.js +41 -0
- package/dist/next/data/principal.js.map +1 -0
- package/dist/next/data/validator.d.ts +12 -0
- package/dist/next/data/validator.d.ts.map +1 -0
- package/dist/next/data/validator.js +34 -0
- package/dist/next/data/validator.js.map +1 -0
- package/dist/next/documents/address-source.d.ts +20 -0
- package/dist/next/documents/address-source.d.ts.map +1 -0
- package/dist/next/documents/address-source.js +44 -0
- package/dist/next/documents/address-source.js.map +1 -0
- package/dist/next/documents/definition.d.ts +84 -0
- package/dist/next/documents/definition.d.ts.map +1 -0
- package/dist/next/documents/definition.js +73 -0
- package/dist/next/documents/definition.js.map +1 -0
- package/dist/next/documents/documents.d.ts +106 -0
- package/dist/next/documents/documents.d.ts.map +1 -0
- package/dist/next/documents/documents.js +224 -0
- package/dist/next/documents/documents.js.map +1 -0
- package/dist/next/documents/fence.d.ts +9 -0
- package/dist/next/documents/fence.d.ts.map +1 -0
- package/dist/next/documents/fence.js +15 -0
- package/dist/next/documents/fence.js.map +1 -0
- package/dist/next/documents/handles.d.ts +76 -0
- package/dist/next/documents/handles.d.ts.map +1 -0
- package/dist/next/documents/handles.js +66 -0
- package/dist/next/documents/handles.js.map +1 -0
- package/dist/next/documents/index.d.ts +4 -0
- package/dist/next/documents/index.d.ts.map +1 -0
- package/dist/next/documents/index.js +3 -0
- package/dist/next/documents/index.js.map +1 -0
- package/dist/next/documents/internal.d.ts +9 -0
- package/dist/next/documents/internal.d.ts.map +1 -0
- package/dist/next/documents/internal.js +9 -0
- package/dist/next/documents/internal.js.map +1 -0
- package/dist/next/documents/keyed-slot.d.ts +103 -0
- package/dist/next/documents/keyed-slot.d.ts.map +1 -0
- package/dist/next/documents/keyed-slot.js +395 -0
- package/dist/next/documents/keyed-slot.js.map +1 -0
- package/dist/next/documents/line.d.ts +7 -0
- package/dist/next/documents/line.d.ts.map +1 -0
- package/dist/next/documents/line.js +14 -0
- package/dist/next/documents/line.js.map +1 -0
- package/dist/next/documents/persistence.d.ts +104 -0
- package/dist/next/documents/persistence.d.ts.map +1 -0
- package/dist/next/documents/persistence.js +128 -0
- package/dist/next/documents/persistence.js.map +1 -0
- package/dist/next/documents/residency.d.ts +31 -0
- package/dist/next/documents/residency.d.ts.map +1 -0
- package/dist/next/documents/residency.js +93 -0
- package/dist/next/documents/residency.js.map +1 -0
- package/dist/next/documents/slot.d.ts +109 -0
- package/dist/next/documents/slot.d.ts.map +1 -0
- package/dist/next/documents/slot.js +181 -0
- package/dist/next/documents/slot.js.map +1 -0
- package/dist/next/documents/transaction.d.ts +133 -0
- package/dist/next/documents/transaction.d.ts.map +1 -0
- package/dist/next/documents/transaction.js +409 -0
- package/dist/next/documents/transaction.js.map +1 -0
- package/dist/next/host/activity.d.ts +31 -0
- package/dist/next/host/activity.d.ts.map +1 -0
- package/dist/next/host/activity.js +40 -0
- package/dist/next/host/activity.js.map +1 -0
- package/dist/next/host/adapted.d.ts +49 -0
- package/dist/next/host/adapted.d.ts.map +1 -0
- package/dist/next/host/adapted.js +214 -0
- package/dist/next/host/adapted.js.map +1 -0
- package/dist/next/host/attempt.d.ts +278 -0
- package/dist/next/host/attempt.d.ts.map +1 -0
- package/dist/next/host/attempt.js +1072 -0
- package/dist/next/host/attempt.js.map +1 -0
- package/dist/next/host/backoff.d.ts +29 -0
- package/dist/next/host/backoff.d.ts.map +1 -0
- package/dist/next/host/backoff.js +69 -0
- package/dist/next/host/backoff.js.map +1 -0
- package/dist/next/host/ctx.d.ts +47 -0
- package/dist/next/host/ctx.d.ts.map +1 -0
- package/dist/next/host/ctx.js +108 -0
- package/dist/next/host/ctx.js.map +1 -0
- package/dist/next/host/deadline.d.ts +12 -0
- package/dist/next/host/deadline.d.ts.map +1 -0
- package/dist/next/host/deadline.js +40 -0
- package/dist/next/host/deadline.js.map +1 -0
- package/dist/next/host/dependencies.d.ts +65 -0
- package/dist/next/host/dependencies.d.ts.map +1 -0
- package/dist/next/host/dependencies.js +115 -0
- package/dist/next/host/dependencies.js.map +1 -0
- package/dist/next/host/destination.d.ts +48 -0
- package/dist/next/host/destination.d.ts.map +1 -0
- package/dist/next/host/destination.js +101 -0
- package/dist/next/host/destination.js.map +1 -0
- package/dist/next/host/facet.d.ts +79 -0
- package/dist/next/host/facet.d.ts.map +1 -0
- package/dist/next/host/facet.js +55 -0
- package/dist/next/host/facet.js.map +1 -0
- package/dist/next/host/follow.d.ts +52 -0
- package/dist/next/host/follow.d.ts.map +1 -0
- package/dist/next/host/follow.js +207 -0
- package/dist/next/host/follow.js.map +1 -0
- package/dist/next/host/host.d.ts +495 -0
- package/dist/next/host/host.d.ts.map +1 -0
- package/dist/next/host/host.js +1075 -0
- package/dist/next/host/host.js.map +1 -0
- package/dist/next/host/index.d.ts +17 -0
- package/dist/next/host/index.d.ts.map +1 -0
- package/dist/next/host/index.js +6 -0
- package/dist/next/host/index.js.map +1 -0
- package/dist/next/host/internal.d.ts +10 -0
- package/dist/next/host/internal.d.ts.map +1 -0
- package/dist/next/host/internal.js +8 -0
- package/dist/next/host/internal.js.map +1 -0
- package/dist/next/host/members.d.ts +50 -0
- package/dist/next/host/members.d.ts.map +1 -0
- package/dist/next/host/members.js +107 -0
- package/dist/next/host/members.js.map +1 -0
- package/dist/next/host/native.d.ts +55 -0
- package/dist/next/host/native.d.ts.map +1 -0
- package/dist/next/host/native.js +92 -0
- package/dist/next/host/native.js.map +1 -0
- package/dist/next/host/object.d.ts +290 -0
- package/dist/next/host/object.d.ts.map +1 -0
- package/dist/next/host/object.js +1127 -0
- package/dist/next/host/object.js.map +1 -0
- package/dist/next/host/parked.d.ts +23 -0
- package/dist/next/host/parked.d.ts.map +1 -0
- package/dist/next/host/parked.js +79 -0
- package/dist/next/host/parked.js.map +1 -0
- package/dist/next/host/presence.d.ts +34 -0
- package/dist/next/host/presence.d.ts.map +1 -0
- package/dist/next/host/presence.js +117 -0
- package/dist/next/host/presence.js.map +1 -0
- package/dist/next/host/ref-state.d.ts +53 -0
- package/dist/next/host/ref-state.d.ts.map +1 -0
- package/dist/next/host/ref-state.js +83 -0
- package/dist/next/host/ref-state.js.map +1 -0
- package/dist/next/host/reference-keys.d.ts +36 -0
- package/dist/next/host/reference-keys.d.ts.map +1 -0
- package/dist/next/host/reference-keys.js +168 -0
- package/dist/next/host/reference-keys.js.map +1 -0
- package/dist/next/host/reference-source.d.ts +65 -0
- package/dist/next/host/reference-source.d.ts.map +1 -0
- package/dist/next/host/reference-source.js +285 -0
- package/dist/next/host/reference-source.js.map +1 -0
- package/dist/next/host/reference.d.ts +308 -0
- package/dist/next/host/reference.d.ts.map +1 -0
- package/dist/next/host/reference.js +775 -0
- package/dist/next/host/reference.js.map +1 -0
- package/dist/next/host/retry-on.d.ts +28 -0
- package/dist/next/host/retry-on.d.ts.map +1 -0
- package/dist/next/host/retry-on.js +52 -0
- package/dist/next/host/retry-on.js.map +1 -0
- package/dist/next/host/revocation.d.ts +16 -0
- package/dist/next/host/revocation.d.ts.map +1 -0
- package/dist/next/host/revocation.js +27 -0
- package/dist/next/host/revocation.js.map +1 -0
- package/dist/next/host/root.d.ts +27 -0
- package/dist/next/host/root.d.ts.map +1 -0
- package/dist/next/host/root.js +68 -0
- package/dist/next/host/root.js.map +1 -0
- package/dist/next/host/route.d.ts +64 -0
- package/dist/next/host/route.d.ts.map +1 -0
- package/dist/next/host/route.js +190 -0
- package/dist/next/host/route.js.map +1 -0
- package/dist/next/host/routines.d.ts +76 -0
- package/dist/next/host/routines.d.ts.map +1 -0
- package/dist/next/host/routines.js +350 -0
- package/dist/next/host/routines.js.map +1 -0
- package/dist/next/host/sleep.d.ts +9 -0
- package/dist/next/host/sleep.d.ts.map +1 -0
- package/dist/next/host/sleep.js +40 -0
- package/dist/next/host/sleep.js.map +1 -0
- package/dist/next/host/staging.d.ts +94 -0
- package/dist/next/host/staging.d.ts.map +1 -0
- package/dist/next/host/staging.js +302 -0
- package/dist/next/host/staging.js.map +1 -0
- package/dist/next/host/unbound.d.ts +30 -0
- package/dist/next/host/unbound.d.ts.map +1 -0
- package/dist/next/host/unbound.js +67 -0
- package/dist/next/host/unbound.js.map +1 -0
- package/dist/next/host/uuid.d.ts +3 -0
- package/dist/next/host/uuid.d.ts.map +1 -0
- package/dist/next/host/uuid.js +38 -0
- package/dist/next/host/uuid.js.map +1 -0
- package/dist/next/index.d.ts +28 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +18 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/jobs/definition.d.ts +96 -0
- package/dist/next/jobs/definition.d.ts.map +1 -0
- package/dist/next/jobs/definition.js +83 -0
- package/dist/next/jobs/definition.js.map +1 -0
- package/dist/next/jobs/index.d.ts +5 -0
- package/dist/next/jobs/index.d.ts.map +1 -0
- package/dist/next/jobs/index.js +3 -0
- package/dist/next/jobs/index.js.map +1 -0
- package/dist/next/jobs/internal.d.ts +3 -0
- package/dist/next/jobs/internal.d.ts.map +1 -0
- package/dist/next/jobs/internal.js +4 -0
- package/dist/next/jobs/internal.js.map +1 -0
- package/dist/next/jobs/job.d.ts +152 -0
- package/dist/next/jobs/job.d.ts.map +1 -0
- package/dist/next/jobs/job.js +320 -0
- package/dist/next/jobs/job.js.map +1 -0
- package/dist/next/jobs/persisted.d.ts +38 -0
- package/dist/next/jobs/persisted.d.ts.map +1 -0
- package/dist/next/jobs/persisted.js +95 -0
- package/dist/next/jobs/persisted.js.map +1 -0
- package/dist/next/jobs/records.d.ts +231 -0
- package/dist/next/jobs/records.d.ts.map +1 -0
- package/dist/next/jobs/records.js +869 -0
- package/dist/next/jobs/records.js.map +1 -0
- package/dist/next/jobs/run-states.d.ts +69 -0
- package/dist/next/jobs/run-states.d.ts.map +1 -0
- package/dist/next/jobs/run-states.js +228 -0
- package/dist/next/jobs/run-states.js.map +1 -0
- package/dist/next/jobs/run.d.ts +113 -0
- package/dist/next/jobs/run.d.ts.map +1 -0
- package/dist/next/jobs/run.js +200 -0
- package/dist/next/jobs/run.js.map +1 -0
- package/dist/next/jobs/running.d.ts +44 -0
- package/dist/next/jobs/running.d.ts.map +1 -0
- package/dist/next/jobs/running.js +117 -0
- package/dist/next/jobs/running.js.map +1 -0
- package/dist/next/jobs/stored.d.ts +63 -0
- package/dist/next/jobs/stored.d.ts.map +1 -0
- package/dist/next/jobs/stored.js +55 -0
- package/dist/next/jobs/stored.js.map +1 -0
- package/dist/next/link/binary.d.ts +4 -0
- package/dist/next/link/binary.d.ts.map +1 -0
- package/dist/next/link/binary.js +534 -0
- package/dist/next/link/binary.js.map +1 -0
- package/dist/next/link/channel.d.ts +63 -0
- package/dist/next/link/channel.d.ts.map +1 -0
- package/dist/next/link/channel.js +202 -0
- package/dist/next/link/channel.js.map +1 -0
- package/dist/next/link/errors.d.ts +7 -0
- package/dist/next/link/errors.d.ts.map +1 -0
- package/dist/next/link/errors.js +21 -0
- package/dist/next/link/errors.js.map +1 -0
- package/dist/next/link/index.d.ts +8 -0
- package/dist/next/link/index.d.ts.map +1 -0
- package/dist/next/link/index.js +8 -0
- package/dist/next/link/index.js.map +1 -0
- package/dist/next/link/internal.d.ts +9 -0
- package/dist/next/link/internal.d.ts.map +1 -0
- package/dist/next/link/internal.js +10 -0
- package/dist/next/link/internal.js.map +1 -0
- package/dist/next/link/ipc.d.ts +22 -0
- package/dist/next/link/ipc.d.ts.map +1 -0
- package/dist/next/link/ipc.js +64 -0
- package/dist/next/link/ipc.js.map +1 -0
- package/dist/next/link/link.d.ts +34 -0
- package/dist/next/link/link.d.ts.map +1 -0
- package/dist/next/link/link.js +87 -0
- package/dist/next/link/link.js.map +1 -0
- package/dist/next/link/message.d.ts +249 -0
- package/dist/next/link/message.d.ts.map +1 -0
- package/dist/next/link/message.js +123 -0
- package/dist/next/link/message.js.map +1 -0
- package/dist/next/link/part.d.ts +80 -0
- package/dist/next/link/part.d.ts.map +1 -0
- package/dist/next/link/part.js +164 -0
- package/dist/next/link/part.js.map +1 -0
- package/dist/next/link/text.d.ts +6 -0
- package/dist/next/link/text.d.ts.map +1 -0
- package/dist/next/link/text.js +335 -0
- package/dist/next/link/text.js.map +1 -0
- package/dist/next/link/values.d.ts +40 -0
- package/dist/next/link/values.d.ts.map +1 -0
- package/dist/next/link/values.js +117 -0
- package/dist/next/link/values.js.map +1 -0
- package/dist/next/link/websocket.d.ts +26 -0
- package/dist/next/link/websocket.d.ts.map +1 -0
- package/dist/next/link/websocket.js +91 -0
- package/dist/next/link/websocket.js.map +1 -0
- package/dist/next/registries/contribution.d.ts +50 -0
- package/dist/next/registries/contribution.d.ts.map +1 -0
- package/dist/next/registries/contribution.js +129 -0
- package/dist/next/registries/contribution.js.map +1 -0
- package/dist/next/registries/entries.d.ts +18 -0
- package/dist/next/registries/entries.d.ts.map +1 -0
- package/dist/next/registries/entries.js +42 -0
- package/dist/next/registries/entries.js.map +1 -0
- package/dist/next/registries/index.d.ts +3 -0
- package/dist/next/registries/index.d.ts.map +1 -0
- package/dist/next/registries/index.js +5 -0
- package/dist/next/registries/index.js.map +1 -0
- package/dist/next/registries/internal.d.ts +4 -0
- package/dist/next/registries/internal.d.ts.map +1 -0
- package/dist/next/registries/internal.js +5 -0
- package/dist/next/registries/internal.js.map +1 -0
- package/dist/next/registries/record.d.ts +94 -0
- package/dist/next/registries/record.d.ts.map +1 -0
- package/dist/next/registries/record.js +253 -0
- package/dist/next/registries/record.js.map +1 -0
- package/dist/next/registries/registry.d.ts +74 -0
- package/dist/next/registries/registry.d.ts.map +1 -0
- package/dist/next/registries/registry.js +14 -0
- package/dist/next/registries/registry.js.map +1 -0
- package/dist/next/registries/value.d.ts +48 -0
- package/dist/next/registries/value.d.ts.map +1 -0
- package/dist/next/registries/value.js +199 -0
- package/dist/next/registries/value.js.map +1 -0
- package/dist/next/remote/calls.d.ts +25 -0
- package/dist/next/remote/calls.d.ts.map +1 -0
- package/dist/next/remote/calls.js +62 -0
- package/dist/next/remote/calls.js.map +1 -0
- package/dist/next/remote/consumer.d.ts +42 -0
- package/dist/next/remote/consumer.d.ts.map +1 -0
- package/dist/next/remote/consumer.js +343 -0
- package/dist/next/remote/consumer.js.map +1 -0
- package/dist/next/remote/hold.d.ts +130 -0
- package/dist/next/remote/hold.d.ts.map +1 -0
- package/dist/next/remote/hold.js +537 -0
- package/dist/next/remote/hold.js.map +1 -0
- package/dist/next/remote/host-link.d.ts +77 -0
- package/dist/next/remote/host-link.d.ts.map +1 -0
- package/dist/next/remote/host-link.js +182 -0
- package/dist/next/remote/host-link.js.map +1 -0
- package/dist/next/remote/internal.d.ts +6 -0
- package/dist/next/remote/internal.d.ts.map +1 -0
- package/dist/next/remote/internal.js +6 -0
- package/dist/next/remote/internal.js.map +1 -0
- package/dist/next/remote/path.d.ts +46 -0
- package/dist/next/remote/path.d.ts.map +1 -0
- package/dist/next/remote/path.js +73 -0
- package/dist/next/remote/path.js.map +1 -0
- package/dist/next/remote/provider.d.ts +76 -0
- package/dist/next/remote/provider.d.ts.map +1 -0
- package/dist/next/remote/provider.js +506 -0
- package/dist/next/remote/provider.js.map +1 -0
- package/dist/next/remote/relay.d.ts +58 -0
- package/dist/next/remote/relay.d.ts.map +1 -0
- package/dist/next/remote/relay.js +117 -0
- package/dist/next/remote/relay.js.map +1 -0
- package/dist/next/remote/remote-object.d.ts +133 -0
- package/dist/next/remote/remote-object.d.ts.map +1 -0
- package/dist/next/remote/remote-object.js +411 -0
- package/dist/next/remote/remote-object.js.map +1 -0
- package/dist/next/remote/replica.d.ts +59 -0
- package/dist/next/remote/replica.d.ts.map +1 -0
- package/dist/next/remote/replica.js +125 -0
- package/dist/next/remote/replica.js.map +1 -0
- package/dist/next/state/batch.d.ts +64 -0
- package/dist/next/state/batch.d.ts.map +1 -0
- package/dist/next/state/batch.js +163 -0
- package/dist/next/state/batch.js.map +1 -0
- package/dist/next/state/derive.d.ts +52 -0
- package/dist/next/state/derive.d.ts.map +1 -0
- package/dist/next/state/derive.js +182 -0
- package/dist/next/state/derive.js.map +1 -0
- package/dist/next/state/index.d.ts +6 -0
- package/dist/next/state/index.d.ts.map +1 -0
- package/dist/next/state/index.js +4 -0
- package/dist/next/state/index.js.map +1 -0
- package/dist/next/state/internal.d.ts +6 -0
- package/dist/next/state/internal.d.ts.map +1 -0
- package/dist/next/state/internal.js +8 -0
- package/dist/next/state/internal.js.map +1 -0
- package/dist/next/state/native-source.d.ts +106 -0
- package/dist/next/state/native-source.d.ts.map +1 -0
- package/dist/next/state/native-source.js +240 -0
- package/dist/next/state/native-source.js.map +1 -0
- package/dist/next/state/watchable.d.ts +114 -0
- package/dist/next/state/watchable.d.ts.map +1 -0
- package/dist/next/state/watchable.js +156 -0
- package/dist/next/state/watchable.js.map +1 -0
- package/dist/next/state/writable.d.ts +32 -0
- package/dist/next/state/writable.d.ts.map +1 -0
- package/dist/next/state/writable.js +49 -0
- package/dist/next/state/writable.js.map +1 -0
- package/dist/next/store/adapter.d.ts +67 -0
- package/dist/next/store/adapter.d.ts.map +1 -0
- package/dist/next/store/adapter.js +118 -0
- package/dist/next/store/adapter.js.map +1 -0
- package/dist/next/store/allocator.d.ts +17 -0
- package/dist/next/store/allocator.d.ts.map +1 -0
- package/dist/next/store/allocator.js +30 -0
- package/dist/next/store/allocator.js.map +1 -0
- package/dist/next/store/applier.d.ts +8 -0
- package/dist/next/store/applier.d.ts.map +1 -0
- package/dist/next/store/applier.js +375 -0
- package/dist/next/store/applier.js.map +1 -0
- package/dist/next/store/apply-batch.d.ts +12 -0
- package/dist/next/store/apply-batch.d.ts.map +1 -0
- package/dist/next/store/apply-batch.js +83 -0
- package/dist/next/store/apply-batch.js.map +1 -0
- package/dist/next/store/archive.d.ts +32 -0
- package/dist/next/store/archive.d.ts.map +1 -0
- package/dist/next/store/archive.js +129 -0
- package/dist/next/store/archive.js.map +1 -0
- package/dist/next/store/backend.d.ts +10 -0
- package/dist/next/store/backend.d.ts.map +1 -0
- package/dist/next/store/backend.js +15 -0
- package/dist/next/store/backend.js.map +1 -0
- package/dist/next/store/catalog.d.ts +24 -0
- package/dist/next/store/catalog.d.ts.map +1 -0
- package/dist/next/store/catalog.js +17 -0
- package/dist/next/store/catalog.js.map +1 -0
- package/dist/next/store/claims.d.ts +23 -0
- package/dist/next/store/claims.d.ts.map +1 -0
- package/dist/next/store/claims.js +65 -0
- package/dist/next/store/claims.js.map +1 -0
- package/dist/next/store/columns.d.ts +77 -0
- package/dist/next/store/columns.d.ts.map +1 -0
- package/dist/next/store/columns.js +77 -0
- package/dist/next/store/columns.js.map +1 -0
- package/dist/next/store/engine.d.ts +176 -0
- package/dist/next/store/engine.d.ts.map +1 -0
- package/dist/next/store/engine.js +283 -0
- package/dist/next/store/engine.js.map +1 -0
- package/dist/next/store/evaluate.d.ts +17 -0
- package/dist/next/store/evaluate.d.ts.map +1 -0
- package/dist/next/store/evaluate.js +128 -0
- package/dist/next/store/evaluate.js.map +1 -0
- package/dist/next/store/export.d.ts +17 -0
- package/dist/next/store/export.d.ts.map +1 -0
- package/dist/next/store/export.js +200 -0
- package/dist/next/store/export.js.map +1 -0
- package/dist/next/store/expression.d.ts +150 -0
- package/dist/next/store/expression.d.ts.map +1 -0
- package/dist/next/store/expression.js +268 -0
- package/dist/next/store/expression.js.map +1 -0
- package/dist/next/store/import.d.ts +10 -0
- package/dist/next/store/import.d.ts.map +1 -0
- package/dist/next/store/import.js +276 -0
- package/dist/next/store/import.js.map +1 -0
- package/dist/next/store/index.d.ts +16 -0
- package/dist/next/store/index.d.ts.map +1 -0
- package/dist/next/store/index.js +13 -0
- package/dist/next/store/index.js.map +1 -0
- package/dist/next/store/json-column.d.ts +33 -0
- package/dist/next/store/json-column.d.ts.map +1 -0
- package/dist/next/store/json-column.js +61 -0
- package/dist/next/store/json-column.js.map +1 -0
- package/dist/next/store/jsonl.d.ts +9 -0
- package/dist/next/store/jsonl.d.ts.map +1 -0
- package/dist/next/store/jsonl.js +61 -0
- package/dist/next/store/jsonl.js.map +1 -0
- package/dist/next/store/outbox.d.ts +26 -0
- package/dist/next/store/outbox.d.ts.map +1 -0
- package/dist/next/store/outbox.js +80 -0
- package/dist/next/store/outbox.js.map +1 -0
- package/dist/next/store/query.d.ts +39 -0
- package/dist/next/store/query.d.ts.map +1 -0
- package/dist/next/store/query.js +97 -0
- package/dist/next/store/query.js.map +1 -0
- package/dist/next/store/refusal.d.ts +15 -0
- package/dist/next/store/refusal.d.ts.map +1 -0
- package/dist/next/store/refusal.js +36 -0
- package/dist/next/store/refusal.js.map +1 -0
- package/dist/next/store/rows.d.ts +80 -0
- package/dist/next/store/rows.d.ts.map +1 -0
- package/dist/next/store/rows.js +183 -0
- package/dist/next/store/rows.js.map +1 -0
- package/dist/next/store/schema.d.ts +25 -0
- package/dist/next/store/schema.d.ts.map +1 -0
- package/dist/next/store/schema.js +115 -0
- package/dist/next/store/schema.js.map +1 -0
- package/dist/next/store/sql.d.ts +73 -0
- package/dist/next/store/sql.d.ts.map +1 -0
- package/dist/next/store/sql.js +234 -0
- package/dist/next/store/sql.js.map +1 -0
- package/dist/next/store/step.d.ts +48 -0
- package/dist/next/store/step.d.ts.map +1 -0
- package/dist/next/store/step.js +50 -0
- package/dist/next/store/step.js.map +1 -0
- package/dist/next/store/store.d.ts +161 -0
- package/dist/next/store/store.d.ts.map +1 -0
- package/dist/next/store/store.js +627 -0
- package/dist/next/store/store.js.map +1 -0
- package/dist/next/store/table-changes.d.ts +29 -0
- package/dist/next/store/table-changes.d.ts.map +1 -0
- package/dist/next/store/table-changes.js +102 -0
- package/dist/next/store/table-changes.js.map +1 -0
- package/dist/next/store/table-evolve.d.ts +74 -0
- package/dist/next/store/table-evolve.d.ts.map +1 -0
- package/dist/next/store/table-evolve.js +201 -0
- package/dist/next/store/table-evolve.js.map +1 -0
- package/dist/next/store/table-handle.d.ts +67 -0
- package/dist/next/store/table-handle.d.ts.map +1 -0
- package/dist/next/store/table-handle.js +101 -0
- package/dist/next/store/table-handle.js.map +1 -0
- package/dist/next/store/table-history.d.ts +75 -0
- package/dist/next/store/table-history.d.ts.map +1 -0
- package/dist/next/store/table-history.js +42 -0
- package/dist/next/store/table-history.js.map +1 -0
- package/dist/next/store/table-migration.d.ts +16 -0
- package/dist/next/store/table-migration.d.ts.map +1 -0
- package/dist/next/store/table-migration.js +202 -0
- package/dist/next/store/table-migration.js.map +1 -0
- package/dist/next/store/table-writes.d.ts +125 -0
- package/dist/next/store/table-writes.d.ts.map +1 -0
- package/dist/next/store/table-writes.js +165 -0
- package/dist/next/store/table-writes.js.map +1 -0
- package/dist/next/store/table.d.ts +80 -0
- package/dist/next/store/table.d.ts.map +1 -0
- package/dist/next/store/table.js +103 -0
- package/dist/next/store/table.js.map +1 -0
- package/dist/next/store/text.d.ts +14 -0
- package/dist/next/store/text.d.ts.map +1 -0
- package/dist/next/store/text.js +51 -0
- package/dist/next/store/text.js.map +1 -0
- package/dist/next/store/writes.d.ts +5 -0
- package/dist/next/store/writes.d.ts.map +1 -0
- package/dist/next/store/writes.js +4 -0
- package/dist/next/store/writes.js.map +1 -0
- package/dist/next-node/compression.d.ts +7 -0
- package/dist/next-node/compression.d.ts.map +1 -0
- package/dist/next-node/compression.js +24 -0
- package/dist/next-node/compression.js.map +1 -0
- package/dist/next-node/http/admission.d.ts +63 -0
- package/dist/next-node/http/admission.d.ts.map +1 -0
- package/dist/next-node/http/admission.js +134 -0
- package/dist/next-node/http/admission.js.map +1 -0
- package/dist/next-node/http/authenticate.d.ts +21 -0
- package/dist/next-node/http/authenticate.d.ts.map +1 -0
- package/dist/next-node/http/authenticate.js +34 -0
- package/dist/next-node/http/authenticate.js.map +1 -0
- package/dist/next-node/http/cookies.d.ts +24 -0
- package/dist/next-node/http/cookies.d.ts.map +1 -0
- package/dist/next-node/http/cookies.js +62 -0
- package/dist/next-node/http/cookies.js.map +1 -0
- package/dist/next-node/http/file.d.ts +23 -0
- package/dist/next-node/http/file.d.ts.map +1 -0
- package/dist/next-node/http/file.js +94 -0
- package/dist/next-node/http/file.js.map +1 -0
- package/dist/next-node/http/headers.d.ts +37 -0
- package/dist/next-node/http/headers.d.ts.map +1 -0
- package/dist/next-node/http/headers.js +129 -0
- package/dist/next-node/http/headers.js.map +1 -0
- package/dist/next-node/http/http.d.ts +100 -0
- package/dist/next-node/http/http.d.ts.map +1 -0
- package/dist/next-node/http/http.js +462 -0
- package/dist/next-node/http/http.js.map +1 -0
- package/dist/next-node/http/incoming.d.ts +19 -0
- package/dist/next-node/http/incoming.d.ts.map +1 -0
- package/dist/next-node/http/incoming.js +31 -0
- package/dist/next-node/http/incoming.js.map +1 -0
- package/dist/next-node/http/index.d.ts +11 -0
- package/dist/next-node/http/index.d.ts.map +1 -0
- package/dist/next-node/http/index.js +12 -0
- package/dist/next-node/http/index.js.map +1 -0
- package/dist/next-node/http/node.d.ts +78 -0
- package/dist/next-node/http/node.d.ts.map +1 -0
- package/dist/next-node/http/node.js +291 -0
- package/dist/next-node/http/node.js.map +1 -0
- package/dist/next-node/http/request.d.ts +54 -0
- package/dist/next-node/http/request.d.ts.map +1 -0
- package/dist/next-node/http/request.js +116 -0
- package/dist/next-node/http/request.js.map +1 -0
- package/dist/next-node/http/response.d.ts +63 -0
- package/dist/next-node/http/response.d.ts.map +1 -0
- package/dist/next-node/http/response.js +202 -0
- package/dist/next-node/http/response.js.map +1 -0
- package/dist/next-node/http/router.d.ts +114 -0
- package/dist/next-node/http/router.d.ts.map +1 -0
- package/dist/next-node/http/router.js +261 -0
- package/dist/next-node/http/router.js.map +1 -0
- package/dist/next-node/http/upgrades.d.ts +58 -0
- package/dist/next-node/http/upgrades.d.ts.map +1 -0
- package/dist/next-node/http/upgrades.js +184 -0
- package/dist/next-node/http/upgrades.js.map +1 -0
- package/dist/next-node/plugins/build.d.ts +57 -0
- package/dist/next-node/plugins/build.d.ts.map +1 -0
- package/dist/next-node/plugins/build.js +334 -0
- package/dist/next-node/plugins/build.js.map +1 -0
- package/dist/next-node/plugins/discover.d.ts +36 -0
- package/dist/next-node/plugins/discover.d.ts.map +1 -0
- package/dist/next-node/plugins/discover.js +91 -0
- package/dist/next-node/plugins/discover.js.map +1 -0
- package/dist/next-node/plugins/evaluate.d.ts +33 -0
- package/dist/next-node/plugins/evaluate.d.ts.map +1 -0
- package/dist/next-node/plugins/evaluate.js +98 -0
- package/dist/next-node/plugins/evaluate.js.map +1 -0
- package/dist/next-node/plugins/index.d.ts +9 -0
- package/dist/next-node/plugins/index.d.ts.map +1 -0
- package/dist/next-node/plugins/index.js +11 -0
- package/dist/next-node/plugins/index.js.map +1 -0
- package/dist/next-node/plugins/install.d.ts +13 -0
- package/dist/next-node/plugins/install.d.ts.map +1 -0
- package/dist/next-node/plugins/install.js +37 -0
- package/dist/next-node/plugins/install.js.map +1 -0
- package/dist/next-node/plugins/plugins.d.ts +135 -0
- package/dist/next-node/plugins/plugins.d.ts.map +1 -0
- package/dist/next-node/plugins/plugins.js +206 -0
- package/dist/next-node/plugins/plugins.js.map +1 -0
- package/dist/next-node/plugins/runtime.d.ts +41 -0
- package/dist/next-node/plugins/runtime.d.ts.map +1 -0
- package/dist/next-node/plugins/runtime.js +136 -0
- package/dist/next-node/plugins/runtime.js.map +1 -0
- package/dist/next-node/plugins/stack.d.ts +16 -0
- package/dist/next-node/plugins/stack.d.ts.map +1 -0
- package/dist/next-node/plugins/stack.js +61 -0
- package/dist/next-node/plugins/stack.js.map +1 -0
- package/dist/next-node/plugins/watcher.d.ts +58 -0
- package/dist/next-node/plugins/watcher.d.ts.map +1 -0
- package/dist/next-node/plugins/watcher.js +281 -0
- package/dist/next-node/plugins/watcher.js.map +1 -0
- package/dist/next-node/workers/control.d.ts +83 -0
- package/dist/next-node/workers/control.d.ts.map +1 -0
- package/dist/next-node/workers/control.js +77 -0
- package/dist/next-node/workers/control.js.map +1 -0
- package/dist/next-node/workers/index.d.ts +5 -0
- package/dist/next-node/workers/index.d.ts.map +1 -0
- package/dist/next-node/workers/index.js +7 -0
- package/dist/next-node/workers/index.js.map +1 -0
- package/dist/next-node/workers/lock.d.ts +21 -0
- package/dist/next-node/workers/lock.d.ts.map +1 -0
- package/dist/next-node/workers/lock.js +137 -0
- package/dist/next-node/workers/lock.js.map +1 -0
- package/dist/next-node/workers/main.d.ts +46 -0
- package/dist/next-node/workers/main.d.ts.map +1 -0
- package/dist/next-node/workers/main.js +230 -0
- package/dist/next-node/workers/main.js.map +1 -0
- package/dist/next-node/workers/process.d.ts +42 -0
- package/dist/next-node/workers/process.d.ts.map +1 -0
- package/dist/next-node/workers/process.js +125 -0
- package/dist/next-node/workers/process.js.map +1 -0
- package/dist/next-node/workers/workers.d.ts +80 -0
- package/dist/next-node/workers/workers.d.ts.map +1 -0
- package/dist/next-node/workers/workers.js +572 -0
- package/dist/next-node/workers/workers.js.map +1 -0
- package/dist/object.d.ts +4 -4
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +3 -4
- package/dist/object.js.map +1 -1
- package/dist/output.d.ts +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/output.js.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +2 -2
- package/dist/remote.js.map +1 -1
- package/dist/schema.d.ts +8 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +180 -5
- package/dist/schema.js.map +1 -1
- package/dist/sqlite/cloudflare.d.ts +2 -0
- package/dist/sqlite/cloudflare.d.ts.map +1 -1
- package/dist/sqlite/cloudflare.js +10 -0
- package/dist/sqlite/cloudflare.js.map +1 -1
- package/dist/sqlite/database.d.ts +5 -2
- package/dist/sqlite/database.d.ts.map +1 -1
- package/dist/sqlite/database.js.map +1 -1
- package/dist/sqlite/node.d.ts +4 -1
- package/dist/sqlite/node.d.ts.map +1 -1
- package/dist/sqlite/node.js +22 -5
- package/dist/sqlite/node.js.map +1 -1
- package/dist/watch.d.ts +194 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +383 -0
- package/dist/watch.js.map +1 -0
- package/dist/wire.d.ts +174 -193
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +81 -80
- package/dist/wire.js.map +1 -1
- package/docs/guide.md +15 -15
- package/docs/next/benchmarks.md +959 -0
- package/docs/next/decisions.md +2957 -0
- package/docs/next/dependencies/book.md +1671 -0
- package/docs/next/dependencies/index.html +2641 -0
- package/docs/next/dependencies/model.md +330 -0
- package/docs/next/design.md +2554 -0
- package/docs/next/durable-observers/book.md +432 -0
- package/docs/next/durable-observers/index.html +517 -0
- package/docs/next/flow-control/book.md +333 -0
- package/docs/next/flow-control/fold/index.html +265 -0
- package/docs/next/flow-control/index.html +548 -0
- package/docs/next/flow-control/window/index.html +228 -0
- package/docs/next/guide-plugins-hosts/book.md +607 -0
- package/docs/next/guide-plugins-hosts/index.html +652 -0
- package/docs/next/http-workers/book.md +378 -0
- package/docs/next/http-workers/index.html +515 -0
- package/docs/next/layer-1.md +311 -0
- package/docs/next/layer-2.md +216 -0
- package/docs/next/plugin-model/book.md +186 -0
- package/docs/next/plugin-model/index.html +310 -0
- package/docs/next/plugin-primer/book.md +273 -0
- package/docs/next/plugin-primer/index.html +430 -0
- package/docs/next/slice-1.md +236 -0
- package/docs/next/slice-10.md +285 -0
- package/docs/next/slice-11.md +93 -0
- package/docs/next/slice-2.md +174 -0
- package/docs/next/slice-3a.md +296 -0
- package/docs/next/slice-3b.md +197 -0
- package/docs/next/slice-4.md +146 -0
- package/docs/next/slice-4a.md +418 -0
- package/docs/next/slice-4b.md +208 -0
- package/docs/next/slice-5.md +281 -0
- package/docs/next/slice-6a.md +676 -0
- package/docs/next/slice-6b.md +332 -0
- package/docs/next/slice-7a.md +130 -0
- package/docs/next/slice-7b/book.md +608 -0
- package/docs/next/slice-7b/index.html +842 -0
- package/docs/next/slice-7b.md +255 -0
- package/docs/next/slice-7c.md +165 -0
- package/docs/next/slice-8.md +194 -0
- package/docs/next/slices.md +230 -0
- package/docs/next/store/book.md +665 -0
- package/docs/next/store/index.html +861 -0
- package/docs/next/store/walkthrough.md +193 -0
- package/docs/spec.md +9 -9
- package/docs/tutorial.md +26 -25
- package/package.json +22 -6
- package/src/delta/README.md +69 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Flow control for Chord links
|
|
3
|
+
cover: Slow / Links
|
|
4
|
+
kicker: A design note · Chord v2 slice 4 · 2026-10-22
|
|
5
|
+
subtitle: How a phone on a bad train connection keeps seeing the newest state of a streamed reply, and why the transport's own backpressure cannot do that
|
|
6
|
+
stats:
|
|
7
|
+
- 9.4 s | how old the newest state was with transport backpressure only
|
|
8
|
+
- 0.0 s | with an acknowledged window of two messages
|
|
9
|
+
- 345 ms | another client's p95 during a call flood, without a call window
|
|
10
|
+
- ~200 | lines for the converged design
|
|
11
|
+
note: |
|
|
12
|
+
Written from the measurements and the cross-checked reports of the reviewers Astra and Fable (2026-10-22): Node 24.4.1,
|
|
13
|
+
`ws` 8.22, separate server and client processes, a throttling proxy at 64 KiB/s. The numbers are single synthetic
|
|
14
|
+
runs: they show failure modes, not production capacity. Relays: first decided as per-hop acknowledgements (b) on
|
|
15
|
+
2026-10-23, then replaced by flow control per origin on 2026-10-24 after the slice 4a benchmarks (chapter VI).
|
|
16
|
+
colophon: Flow control for Chord links · fourth edition · 2026-10-24 · based on the reviewers' experiments and design §3, §22
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Before we begin {.preface}
|
|
20
|
+
|
|
21
|
+
Mario is on a train. His phone shows a p3 session in which the agent is writing a long reply. Every token is a small
|
|
22
|
+
state change: the worker commits it, the server relays it, the phone applies it. The train enters a tunnel and the
|
|
23
|
+
phone's link drops to a trickle, 64 KB per second. The worker keeps producing tokens at full speed.
|
|
24
|
+
|
|
25
|
+
Without flow control, every token still gets sent. The bytes pile up somewhere between the worker and the phone, and
|
|
26
|
+
when the train leaves the tunnel the phone has to work through a backlog of old tokens before it sees the current text.
|
|
27
|
+
It is not only slow: what it shows is minutes old, and nothing tells it so.
|
|
28
|
+
|
|
29
|
+
This note explains what keeps the phone current, why the obvious answer (let the transport push back) fails, and the
|
|
30
|
+
design Astra and Fable converged on after measuring it.
|
|
31
|
+
|
|
32
|
+
## Six things to know
|
|
33
|
+
|
|
34
|
+
:::findings
|
|
35
|
+
1. **Transport backpressure arrives too late.** The kernel's and a proxy's buffers take megabytes before a WebSocket
|
|
36
|
+
reports anything buffered; the phone sees state 4 to 10 seconds old.
|
|
37
|
+
2. **Acknowledgements keep state fresh.** The receiving application acknowledges each message; at most two are in
|
|
38
|
+
flight; the newest state was 0.0 seconds old on the same 64 KB/s link.
|
|
39
|
+
3. **Count messages, not bytes.** Each message carries everything that piled up since the last acknowledgement, so
|
|
40
|
+
throughput adapts to the link by itself.
|
|
41
|
+
4. **Merge ops first, snapshot last.** 2,000 merged token appends cost 26 KB, sent one by one 46 KB; a snapshot of a
|
|
42
|
+
member with 500 KB of history would cost 508 KB.
|
|
43
|
+
5. **Calls need their own window.** A tab firing 100,000 calls pushed another client's p95 to 345 ms; with a window of
|
|
44
|
+
64 respected by the caller it stayed at 0.66 ms and the flood waited in the buggy tab.
|
|
45
|
+
6. **Nothing is refused for being too many** (decision K16): extra state is merged, extra calls wait.
|
|
46
|
+
:::
|
|
47
|
+
|
|
48
|
+
## Words used throughout
|
|
49
|
+
|
|
50
|
+
A **link** connects two hosts. The **provider** owns an object and sends its state; the **consumer** holds a
|
|
51
|
+
reference to it. A **hold** is one reference over a link, with its own stream of **ops** (Delta's changes). A
|
|
52
|
+
**snapshot** is a hold's whole state. An **acknowledgement** (`ack`) is a tiny message the consumer sends after it
|
|
53
|
+
applied a state message. **In flight** means sent and not yet acknowledged. **Coalescing** means merging the ops that
|
|
54
|
+
wait to be sent. A **relay** is a host in the middle (the server between worker and tab).
|
|
55
|
+
|
|
56
|
+
# Where the Bytes Hide {#hidden lede="Why the sender cannot see a slow phone."}
|
|
57
|
+
|
|
58
|
+
A WebSocket in Node reports `bufferedAmount`: bytes the library still holds. It looks like the perfect signal: stop
|
|
59
|
+
sending when it grows. The experiment says otherwise.
|
|
60
|
+
|
|
61
|
+
:::sequence animate | A flood of state messages to a slow phone through a proxy. The sender's buffer stays small; the kernel's and the proxy's do not.
|
|
62
|
+
actor worker "worker" "commits tokens" accent
|
|
63
|
+
actor ws "server WebSocket" "bufferedAmount"
|
|
64
|
+
actor kernel "kernel + proxy buffers" muted
|
|
65
|
+
actor phone "phone" "64 KB/s" blue
|
|
66
|
+
worker -> ws: token 1 … token 9,000
|
|
67
|
+
ws -> kernel: written at once (bufferedAmount stays near 0)
|
|
68
|
+
kernel -> phone: drains at 64 KB/s
|
|
69
|
+
note kernel: megabytes waiting
|
|
70
|
+
worker -> ws: token 9,001 (the newest)
|
|
71
|
+
ws -> kernel: queued behind everything
|
|
72
|
+
kernel --> phone: arrives seconds later
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
| Flood, ~9,800 × 128 B per second, 64 KB/s link | Sender memory | Newest state the phone saw |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| no flow control | 7.0 MB queued in the WebSocket, heap +18.5 MB | 9.4 s old |
|
|
78
|
+
| cap `bufferedAmount` at 64 KiB, merge pending state | +0.46 MB | **still 9.4 s old** |
|
|
79
|
+
| cap at 1 MiB (Fable's rerun) | small | 3.8 s, growing to 10.4 s |
|
|
80
|
+
| at most 1 to 4 acknowledged messages in flight, merge pending state | +0.4 MB | **0.0 s** |
|
|
81
|
+
|
|
82
|
+
The transport's backpressure protects the sender's memory, but freshness depends on buffers the sender cannot see:
|
|
83
|
+
the operating system's socket buffer, autotuned to hundreds of kilobytes, and every proxy on the way. A normal
|
|
84
|
+
streamed reply (50 tokens per second, 23 bytes each) never fills them; a burst or a tunnel does. Only the receiving
|
|
85
|
+
application knows what it has applied.
|
|
86
|
+
|
|
87
|
+
# Acknowledged Messages {#acks lede="A window of two messages in flight, and why it adapts to any link."}
|
|
88
|
+
|
|
89
|
+
The rule: a provider has at most **K = 2** state messages in flight per link. The consumer sends `ack` (three bytes in
|
|
90
|
+
the binary codec) after it applied one. While two are in flight, new commits do not produce messages; their ops wait,
|
|
91
|
+
merged per hold, and go out in the next message once an `ack` arrives.
|
|
92
|
+
|
|
93
|
+
:::steps | Two messages in flight on a slow link
|
|
94
|
+
+++ Fast link
|
|
95
|
+
Commits 1, 2, 3 each become a message; each is acknowledged before the next commit. Nothing waits: the window never
|
|
96
|
+
fills, so a normal stream behaves exactly as without flow control.
|
|
97
|
+
+++ The link slows
|
|
98
|
+
Messages 1 and 2 are in flight. Commits 3 to 400 arrive; their ops are folded into the pending queue of each member
|
|
99
|
+
they touch (chapter III). Memory: the net change, not the history; at most 10,000 ops per member.
|
|
100
|
+
+++ An ack arrives
|
|
101
|
+
`ack` for message 1. The provider sends message 3, carrying the folded ops of commits 3 to 400, encoded once.
|
|
102
|
+
The phone applies them as one batch and shows the newest text.
|
|
103
|
+
+++ Self-clocking
|
|
104
|
+
The slower the link, the more each message carries; the faster, the smaller and more frequent. Nobody measures
|
|
105
|
+
bandwidth: the acknowledgements pace the sender.
|
|
106
|
+
:::
|
|
107
|
+
|
|
108
|
+
:::raw
|
|
109
|
+
<div id="fc-sim" style="font-family: var(--sans, system-ui); margin: 1.5rem 0; padding: 1rem; border: 1px solid #d8d2c6; border-radius: 6px; background: #fbf9f4;">
|
|
110
|
+
<div style="display:flex; gap:1rem; flex-wrap:wrap; align-items:center; font-size:0.9rem;">
|
|
111
|
+
<label>Link <input id="fc-rate" type="range" min="2" max="60" value="8"> <span id="fc-rate-v"></span> msgs/s</label>
|
|
112
|
+
<label>Commits <input id="fc-prod" type="range" min="5" max="120" value="60"> <span id="fc-prod-v"></span> /s</label>
|
|
113
|
+
<button id="fc-reset" style="font:inherit; padding:0.2rem 0.6rem;">Restart</button>
|
|
114
|
+
</div>
|
|
115
|
+
<canvas id="fc-canvas" width="640" height="220" style="width:100%; max-width:640px; display:block; margin-top:0.8rem;"></canvas>
|
|
116
|
+
<div id="fc-stats" style="font-size:0.85rem; margin-top:0.4rem; color:#444;"></div>
|
|
117
|
+
</div>
|
|
118
|
+
<script>
|
|
119
|
+
(() => {
|
|
120
|
+
const c = document.getElementById("fc-canvas"), g = c.getContext("2d");
|
|
121
|
+
const rate = document.getElementById("fc-rate"), prod = document.getElementById("fc-prod");
|
|
122
|
+
const rv = document.getElementById("fc-rate-v"), pv = document.getElementById("fc-prod-v");
|
|
123
|
+
const stats = document.getElementById("fc-stats");
|
|
124
|
+
let s;
|
|
125
|
+
function reset() {
|
|
126
|
+
s = { t: 0, commit: 0, naive: { queue: [], shown: 0, sent: 0 }, ack: { inflight: [], pending: 0, shown: 0, sent: 0, merged: 0 } };
|
|
127
|
+
}
|
|
128
|
+
function step(dt) {
|
|
129
|
+
const r = +rate.value, p = +prod.value; rv.textContent = r; pv.textContent = p;
|
|
130
|
+
s.t += dt;
|
|
131
|
+
const commits = Math.floor((s.t) * p) - s.commit;
|
|
132
|
+
for (let i = 0; i < commits; i++) {
|
|
133
|
+
s.commit++;
|
|
134
|
+
s.naive.queue.push(s.commit);
|
|
135
|
+
s.ack.pending = s.commit;
|
|
136
|
+
}
|
|
137
|
+
// link capacity: r messages per second, shared model: each delivers one message per 1/r s
|
|
138
|
+
s.naive.budget = (s.naive.budget || 0) + dt * r;
|
|
139
|
+
while (s.naive.budget >= 1 && s.naive.queue.length) { s.naive.budget -= 1; s.naive.shown = s.naive.queue.shift(); s.naive.sent++; }
|
|
140
|
+
s.ack.budget = (s.ack.budget || 0) + dt * r;
|
|
141
|
+
while (s.ack.budget >= 1 && s.ack.inflight.length) { s.ack.budget -= 1; s.ack.shown = s.ack.inflight.shift(); }
|
|
142
|
+
while (s.ack.inflight.length < 2 && s.ack.pending > (s.ack.inflight.at(-1) ?? s.ack.shown)) {
|
|
143
|
+
s.ack.merged += s.ack.pending - (s.ack.inflight.at(-1) ?? s.ack.shown) - 1;
|
|
144
|
+
s.ack.inflight.push(s.ack.pending); s.ack.sent++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function lane(y, label, queue, shown, color) {
|
|
148
|
+
g.fillStyle = "#222"; g.font = "13px system-ui"; g.fillText(label, 8, y - 26);
|
|
149
|
+
g.fillStyle = "#e9e3d6"; g.fillRect(8, y - 18, 520, 26);
|
|
150
|
+
const n = Math.min(queue, 130);
|
|
151
|
+
g.fillStyle = color;
|
|
152
|
+
for (let i = 0; i < n; i++) g.fillRect(10 + i * 4, y - 15, 3, 20);
|
|
153
|
+
g.fillStyle = "#222"; g.fillText(`waiting: ${queue}`, 540, y);
|
|
154
|
+
const age = s.commit - shown;
|
|
155
|
+
g.fillText(`phone shows commit ${shown}, ${age} behind`, 8, y + 28);
|
|
156
|
+
}
|
|
157
|
+
function draw() {
|
|
158
|
+
g.clearRect(0, 0, c.width, c.height);
|
|
159
|
+
lane(48, "No flow control: every commit is sent and queued", s.naive.queue.length, s.naive.shown, "#b5523b");
|
|
160
|
+
lane(150, "Acknowledged window (K = 2), pending ops merged", s.ack.inflight.length, s.ack.shown, "#3b6fb5");
|
|
161
|
+
stats.textContent = `newest commit ${s.commit} · window: ${s.ack.sent} messages carried ${s.commit} commits (${s.ack.merged} merged)`;
|
|
162
|
+
}
|
|
163
|
+
let last = performance.now();
|
|
164
|
+
function frame(now) { step(Math.min((now - last) / 1000, 0.1)); last = now; draw(); requestAnimationFrame(frame); }
|
|
165
|
+
document.getElementById("fc-reset").onclick = reset;
|
|
166
|
+
reset(); requestAnimationFrame(frame);
|
|
167
|
+
})();
|
|
168
|
+
</script>
|
|
169
|
+
:::
|
|
170
|
+
|
|
171
|
+
The simulation above shows the same effect in miniature. Drag the link below the commit rate: the upper lane's queue
|
|
172
|
+
grows without bound and the phone falls further behind every second; the lower lane never has more than two messages
|
|
173
|
+
waiting, and the phone is at most a message behind.
|
|
174
|
+
|
|
175
|
+
# Fold the Backlog {#merge lede="What a waiting hold keeps while the window is full, and why it never measures sizes."}
|
|
176
|
+
|
|
177
|
+
While messages are in flight, each hold member keeps the ops of new commits in a **pending queue** and **folds** them:
|
|
178
|
+
a new op merges into the last pending op on the same path when that is safe, so a backlog keeps the net change, not
|
|
179
|
+
the history. When an acknowledgement frees a slot, the queue is encoded once into one message.
|
|
180
|
+
|
|
181
|
+
| Last pending op | New op | Kept |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `a text "Hel"` | `a text "lo"` | one `a text "Hello"` |
|
|
184
|
+
| `s count 41` or an append | `s count 42` | the `s`: it overwrites what came before |
|
|
185
|
+
| anything | `r value` | only the `r`: a replacement makes everything before it moot |
|
|
186
|
+
| anything on a path in the index | a structural op (`p`, `m`, `d`, `t`, `s` of an object) | appended; the index is cleared |
|
|
187
|
+
|
|
188
|
+
The merge partner is found through a **leaf index**: a map from path to the last pending append or primitive set on
|
|
189
|
+
that path. Leaf ops on different paths commute, so a new op may merge into an earlier one, not only into its
|
|
190
|
+
neighbour; a status field changing between two tokens no longer stops the reply from folding. Any structural op
|
|
191
|
+
clears the index, because after a splice `entries[7]` may name another entry. Past 10,000 pending ops the queue keeps
|
|
192
|
+
only "replace with the current value", which then costs nothing while the consumer stays slow.
|
|
193
|
+
|
|
194
|
+
**Why no size rule.** The first idea was to send a snapshot once the merged ops outgrow the member. The reviewers
|
|
195
|
+
measured it on real p2 data and it failed three ways: "snapshot bytes plus sent op bytes" is no bound (an interned path
|
|
196
|
+
lets a value grow faster than its ops: 5,404 B of snapshot after 832 B of ops), it drifts 1,254 times too high under
|
|
197
|
+
churn, and an exact size needs a size on every node of the value. Folding alone came within about 10% of an exact
|
|
198
|
+
oracle, with no measuring at all.
|
|
199
|
+
|
|
200
|
+
| 60 s stall, all workloads | Uncompressed | Compressed |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| merged ops, no folding | 466 KB | 166 KB |
|
|
203
|
+
| adjacent folding | 399 KB | 147 KB |
|
|
204
|
+
| leaf index (the decision) | 372 KB | 145 KB |
|
|
205
|
+
| exact oracle (not buildable) | 335 KB | 136 KB |
|
|
206
|
+
|
|
207
|
+
**One queue for every slow consumer.** The same queue serves Chord's shared watch handle: a listener still busy with
|
|
208
|
+
the last frame gets the commits made meanwhile as one call with the newest value and the folded ops, instead of
|
|
209
|
+
today's "after 100 frames, replace everything". With a listener taking one second, a conversation view's watch went
|
|
210
|
+
from 402 calls and 17.9 MB of ops to 312 calls and 257 KB. Durable's event stream keeps a queue of its own, because
|
|
211
|
+
events are not state: folding would drop them.
|
|
212
|
+
|
|
213
|
+
:::raw
|
|
214
|
+
<p>Animation of the fold: <a href="https://radius.earendil.com/artifact/01m5nc9s24e9kscppen2fxgwye">How folding works</a>.</p>
|
|
215
|
+
:::
|
|
216
|
+
|
|
217
|
+
# Calls Get Their Own Window {#calls lede="Why acknowledged state does not protect against a call flood."}
|
|
218
|
+
|
|
219
|
+
Calls are small and the provider acknowledges nothing until they finish, so a byte or message window on state says
|
|
220
|
+
nothing about how many handlers run. A tab with a bug:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
for (const note of notes) ref.add(note, ctx); // 100,000 calls, none awaited
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| 100,000 calls, handlers awaiting 10 ms | Finished in | Provider memory | Another client's p95 round trip |
|
|
227
|
+
| --- | --- | --- | --- |
|
|
228
|
+
| no limit | 0.66 s | +56.5 MB, 43,824 running | 129 ms (345 ms with trivial handlers) |
|
|
229
|
+
| provider-side semaphore of 64 | 15.9 s | +42.5 MB, 96,736 queued on the provider | 0.38 ms |
|
|
230
|
+
| caller-side window of 64 | 17.1 s | **+4 MB, nothing queued on the provider** | 0.66 ms |
|
|
231
|
+
| handlers doing 10 ms of CPU work | | | blocked about 1 s, with or without a limit |
|
|
232
|
+
|
|
233
|
+
The provider's `hello` announces a call window **N** (256 by default) per origin (chapter VI). A consumer keeps at
|
|
234
|
+
most N of its own calls unanswered per link; further calls wait in the link's call queue, in order. Nothing is refused (K16); a peer that sends
|
|
235
|
+
more than N is a protocol error. The flood waits in the tab that caused it. CPU-heavy handlers block the event loop
|
|
236
|
+
whatever the window: that stays the handler author's responsibility.
|
|
237
|
+
|
|
238
|
+
# Relays {#relays lede="One link to a worker carries many tabs. Decided: flow control per origin."}
|
|
239
|
+
|
|
240
|
+
The server relays every tab's holds to a session's worker over one link. Three ways to apply the acknowledged window
|
|
241
|
+
across that hop were considered:
|
|
242
|
+
|
|
243
|
+
:::flow wide animate | (a) End-to-end acknowledgements: the server acknowledges the worker only after the tab did.
|
|
244
|
+
row "Worker" accent
|
|
245
|
+
box w "worker" "K = 2 per link"
|
|
246
|
+
row "Server" muted
|
|
247
|
+
box s "server" "acks upstream after downstream"
|
|
248
|
+
row "Tabs" blue
|
|
249
|
+
box fast "fast tab" "desktop"
|
|
250
|
+
box slow "slow tab" "phone in a tunnel" dashed
|
|
251
|
+
arrow w -> s "state"
|
|
252
|
+
arrow s -> fast "state"
|
|
253
|
+
arrow s -> slow "state, slow"
|
|
254
|
+
arrow slow -> s "ack (late)" dashed
|
|
255
|
+
arrow s -> w "ack only when every tab acked" accent
|
|
256
|
+
note "Simplest, but the slow phone holds the worker's one window, so the fast tab's updates arrive in coarser, later batches too."
|
|
257
|
+
:::
|
|
258
|
+
|
|
259
|
+
:::flow wide animate | (b) Acknowledgements per hop: the server acknowledges at once and resets a slow tab.
|
|
260
|
+
row "Worker" accent
|
|
261
|
+
box w2 "worker" "K = 2 per link"
|
|
262
|
+
row "Server" muted
|
|
263
|
+
box s2 "server" "acks at once; window per tab"
|
|
264
|
+
row "Tabs" blue
|
|
265
|
+
box fast2 "fast tab" "desktop"
|
|
266
|
+
box slow2 "slow tab" "phone in a tunnel" dashed
|
|
267
|
+
arrow w2 -> s2 "state"
|
|
268
|
+
arrow s2 -> w2 "ack at once" accent
|
|
269
|
+
arrow s2 -> fast2 "state, current"
|
|
270
|
+
arrow s2 -> slow2 "window full: its parts dropped" dashed
|
|
271
|
+
arrow s2 -> w2 "reset: fresh snapshots" accent
|
|
272
|
+
note "The server cannot fold opaque parts, so it drops them and asks for snapshots. Built and measured in slice 4a: a 1 s stall cost three full snapshots (~1.5 MiB) and ~24 s to catch up under streaming."
|
|
273
|
+
:::
|
|
274
|
+
|
|
275
|
+
:::flow wide animate | (c) Flow control per origin (decided 2026-10-24): the worker keeps a window per tab; the relay only forwards.
|
|
276
|
+
row "Worker" accent
|
|
277
|
+
box w3 "worker" "K = 2 per (link, origin)"
|
|
278
|
+
row "Server" muted
|
|
279
|
+
box s3 "server" "forwards; FIFO of senders"
|
|
280
|
+
row "Tabs" blue
|
|
281
|
+
box fast3 "fast tab" "origin 1"
|
|
282
|
+
box slow3 "slow tab" "origin 2, in a tunnel" dashed
|
|
283
|
+
arrow w3 -> s3 "state @1, state @2"
|
|
284
|
+
arrow s3 -> fast3 "state, current"
|
|
285
|
+
arrow s3 -> slow3 "state, slow" dashed
|
|
286
|
+
arrow slow3 -> s3 "ack (late)" dashed
|
|
287
|
+
arrow s3 -> w3 "ack @2 when the tab acked" accent
|
|
288
|
+
note "The relay numbers each tab (an origin) and forwards at once; each tab's ack goes back to the worker that sent the message. The worker folds the slow tab's commits in that origin's pending queues, exactly as for a direct tab; the fast tab is another origin with its own window."
|
|
289
|
+
:::
|
|
290
|
+
|
|
291
|
+
How (c) works:
|
|
292
|
+
|
|
293
|
+
- **Origins.** The relay gives each downstream link a number, never reused; every message upstream carries it
|
|
294
|
+
(`origin`), and the worker keys holds, calls, windows and acks by (link, origin, id). Ids are not renumbered.
|
|
295
|
+
- **What the relay keeps.** Per relayed hold its upstream link and its original `hold` message (to route calls and
|
|
296
|
+
hold again after a worker restart); per tab a FIFO of which worker sent each message in flight, so each in-order
|
|
297
|
+
`ack` goes back to the right worker. Nothing is parsed, folded, dropped or reset.
|
|
298
|
+
- **Calls.** N is per origin at the worker; the relay forwards calls at once, each tab bounded by its own window.
|
|
299
|
+
- **A tab leaves.** Its link closes; the relay sends `closed { origin }` to each worker it relayed to, which releases
|
|
300
|
+
that origin's holds and aborts its calls.
|
|
301
|
+
- **Cost.** A tab served by W workers may have 2W messages in flight (plus the relay's own 2). Two bytes of origin per
|
|
302
|
+
message on the worker link.
|
|
303
|
+
|
|
304
|
+
# What Changes in the Design {.epilogue lede="The converged scheme, what it replaces, and the relay decision."}
|
|
305
|
+
|
|
306
|
+
**Converged** (Astra and Fable, after cross-checking each other's experiments): at most K = 2 acknowledged state
|
|
307
|
+
messages in flight; pending ops folded per hold member through a leaf index, capped at 10,000; a call window N
|
|
308
|
+
announced in `hello`, extra calls queued by the caller; holds unlimited; size limits stay a separate check.
|
|
309
|
+
|
|
310
|
+
**Replaced** from the design (§3, §22): the byte window and its credit messages, snapshot admission and debt (R19, Q3),
|
|
311
|
+
cut C12's call-credit accounting, any adapter backlog API. IPC and WebSocket behave the same, because acknowledgements
|
|
312
|
+
are messages.
|
|
313
|
+
|
|
314
|
+
**Relays (owner):** per-hop acknowledgements (b) were decided on 2026-10-23 and built in slice 4a; the benchmarks
|
|
315
|
+
showed a slow tab recovering through resets far worse than a direct tab (24 s and three snapshots after a 1 s stall).
|
|
316
|
+
On 2026-10-24 they were replaced by **flow control per origin** (c): the worker keeps window, pending queues and calls
|
|
317
|
+
per (link, origin), the relay only forwards and returns acks. Measured after the rework: a slow tab through the relay
|
|
318
|
+
catches up exactly like a direct one. The circuit breaker stayed undecided; the heartbeat (15 s ping, 30 s close)
|
|
319
|
+
was built instead.
|
|
320
|
+
|
|
321
|
+
# Measurements {.appendix}
|
|
322
|
+
|
|
323
|
+
| Experiment | Setup | Result |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| normal stream | 50 × 23 B/s through 64 KiB/s | no buffering, frames ≤ 2 ms old |
|
|
326
|
+
| flood, no control | ~9,800 × 128 B/s, 10 s | 7.0 MB in `bufferedAmount`, heap +18.5 MB, newest state 9.4 s old |
|
|
327
|
+
| flood, `bufferedAmount` ≤ 64 KiB + merging | same | heap +0.46 MB, newest state 9.4 s old |
|
|
328
|
+
| flood, 1 to 4 acked messages + merging | same | heap +0.4 MB, newest state 0.0 s old, 98 % fewer messages |
|
|
329
|
+
| client stops reading (loopback) | 20k × 90 B/s, 15 s | 23.4 MiB buffered, RSS +121 MB; merging above 1 MiB: flat |
|
|
330
|
+
| IPC to a busy child | 623k sends | `send()` false from ~64 KB, parent +1,035 MB; the callback acknowledges the write, not the handler |
|
|
331
|
+
| call flood | 100,000 calls, see chapter IV | caller-side window: +4 MB, other client p95 0.66 ms |
|
|
332
|
+
| slow tab through a relay, (b) resets | 64 KiB/s, 100 tokens/s into a 500 KB view, 1 s stall | ~24 s to catch up, three full snapshots (~1.5 MiB) |
|
|
333
|
+
| slow tab through a relay, (c) per origin | same, 1 s / 10 s / 60 s stall | caught up after 0.02 / 0.08 / 0.38 s, 485 B / 4.1 KiB / 23.7 KiB (same as direct) |
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>How folding works</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #faf8f4; --ink: #222; --muted: #777; --line: #ddd6ca;
|
|
10
|
+
--a: #2f6fb3; --s: #b5562b; --p: #4f8a3c; --snap: #7a4fb0; --ack: #2a9d8f;
|
|
11
|
+
}
|
|
12
|
+
* { box-sizing: border-box; }
|
|
13
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 16px/1.45 Georgia, "Times New Roman", serif; }
|
|
14
|
+
main { max-width: 760px; margin: 0 auto; padding: 20px 16px 60px; }
|
|
15
|
+
h1 { font-size: 26px; margin: 8px 0 4px; }
|
|
16
|
+
.lede { color: var(--muted); margin: 0 0 18px; }
|
|
17
|
+
h2 { font-size: 19px; margin: 28px 0 8px; }
|
|
18
|
+
p { margin: 8px 0; }
|
|
19
|
+
code { font: 13px/1.3 ui-monospace, Menlo, Consolas, monospace; background: #efe9de; padding: 1px 4px; border-radius: 3px; }
|
|
20
|
+
.stage { background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 12px; margin: 12px 0; }
|
|
21
|
+
.controls { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
|
|
22
|
+
button { font: 14px system-ui, sans-serif; border: 1px solid var(--line); background: #f4efe6; border-radius: 6px; padding: 6px 10px; cursor: pointer; }
|
|
23
|
+
button.on { background: var(--ink); color: #fff; border-color: var(--ink); }
|
|
24
|
+
.lanes { display: grid; grid-template-columns: 92px 1fr; gap: 8px 10px; align-items: center; font: 13px system-ui, sans-serif; }
|
|
25
|
+
.label { color: var(--muted); text-align: right; }
|
|
26
|
+
.lanes > div { min-width: 0; }
|
|
27
|
+
.lane { min-height: 44px; border: 1px dashed var(--line); border-radius: 8px; padding: 6px; display: flex; flex-wrap: wrap; gap: 5px; align-items: center; position: relative; }
|
|
28
|
+
.chip { font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace; color: #fff; border-radius: 5px; padding: 4px 6px; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
29
|
+
transition: transform .35s ease, opacity .35s ease, background-color .3s; }
|
|
30
|
+
.chip.a { background: var(--a); } .chip.s { background: var(--s); } .chip.p { background: var(--p); } .chip.snap { background: var(--snap); }
|
|
31
|
+
.chip.enter { transform: translateY(-14px) scale(.8); opacity: 0; }
|
|
32
|
+
.chip.merge { transform: translateX(-26px) scale(.6); opacity: 0; }
|
|
33
|
+
.chip.flash { box-shadow: 0 0 0 3px #ffd36b; }
|
|
34
|
+
.msg { font: 12px ui-monospace, Menlo, Consolas, monospace; border: 1px solid #c9c0b0; background: #f7f3ec; border-radius: 6px; padding: 4px 6px; }
|
|
35
|
+
.msg.fly { animation: fly 1.2s ease-in forwards; }
|
|
36
|
+
@keyframes fly { to { transform: translateX(240px); opacity: 0; } }
|
|
37
|
+
.stats { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 10px; font: 13px system-ui, sans-serif; color: var(--muted); }
|
|
38
|
+
.stats b { color: var(--ink); font-weight: 600; }
|
|
39
|
+
.note { font: 13px system-ui, sans-serif; color: var(--muted); margin-top: 8px; min-height: 2.6em; }
|
|
40
|
+
.tunnel { display: inline-block; padding: 1px 6px; border-radius: 4px; background: #333; color: #fff; font: 12px system-ui, sans-serif; }
|
|
41
|
+
.free { display: inline-block; padding: 1px 6px; border-radius: 4px; background: var(--ack); color: #fff; font: 12px system-ui, sans-serif; }
|
|
42
|
+
table { border-collapse: collapse; font: 14px system-ui, sans-serif; margin: 8px 0; width: 100%; }
|
|
43
|
+
td, th { border-bottom: 1px solid var(--line); padding: 5px 6px; text-align: left; vertical-align: top; }
|
|
44
|
+
th { color: var(--muted); font-weight: 600; }
|
|
45
|
+
.legend { display: flex; gap: 10px; flex-wrap: wrap; font: 12px system-ui, sans-serif; color: var(--muted); margin: 6px 0 2px; }
|
|
46
|
+
.legend span.chip { display: inline-block; }
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<main>
|
|
51
|
+
<h1>How folding works</h1>
|
|
52
|
+
<p class="lede">What a hold does with commits while its link's window is full (two messages unacknowledged), and why folding needs no size measuring.</p>
|
|
53
|
+
|
|
54
|
+
<h2>The rule</h2>
|
|
55
|
+
<p>While the window is full, a hold keeps the ops of each new commit <b>raw</b> in a pending list. Before appending an op, it looks at the <b>last pending op only</b>:</p>
|
|
56
|
+
<table>
|
|
57
|
+
<tr><th>last pending</th><th>new op</th><th>result</th></tr>
|
|
58
|
+
<tr><td><code>a path "Hel"</code></td><td><code>a path "lo"</code></td><td>one <code>a path "Hello"</code> (text joined)</td></tr>
|
|
59
|
+
<tr><td><code>s path 41</code> or <code>a path …</code></td><td><code>s path 42</code></td><td>one <code>s path 42</code> (the set overwrites what came before)</td></tr>
|
|
60
|
+
<tr><td>anything on another path</td><td>anything</td><td>appended as it is</td></tr>
|
|
61
|
+
</table>
|
|
62
|
+
<p>Applied in order, the folded list gives the same value as the unfolded one: only neighbours on the same path merge, so no op in between can see the difference. Cost: one path comparison per op. When an ack frees a slot, the pending list is encoded <b>once</b> into one message.</p>
|
|
63
|
+
<div class="legend"><span class="chip a">a = append text</span><span class="chip s">s = set value</span><span class="chip p">p = insert into list</span><span class="chip snap">snapshot</span></div>
|
|
64
|
+
|
|
65
|
+
<div class="stage">
|
|
66
|
+
<div class="controls" id="scenarios">
|
|
67
|
+
<button data-s="stream" class="on">streamed reply</button>
|
|
68
|
+
<button data-s="counter">counter, 100k sets</button>
|
|
69
|
+
<button data-s="interleaved">reply + status, interleaved</button>
|
|
70
|
+
<button data-s="list">new list items</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="controls">
|
|
73
|
+
<button id="play">play</button>
|
|
74
|
+
<button id="step">one commit</button>
|
|
75
|
+
<button id="ack">ack arrives</button>
|
|
76
|
+
<button id="reset">reset</button>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="lanes">
|
|
79
|
+
<div class="label">link</div><div><span id="linkState" class="tunnel">window full: 2 unacked (tunnel)</span></div>
|
|
80
|
+
<div class="label">commits</div><div class="lane" id="incoming"></div>
|
|
81
|
+
<div class="label">pending (raw)</div><div class="lane" id="pending"></div>
|
|
82
|
+
<div class="label">sent</div><div class="lane" id="sent"></div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="stats">
|
|
85
|
+
<span>commits <b id="commits">0</b></span>
|
|
86
|
+
<span>pending ops <b id="pendingOps">0</b></span>
|
|
87
|
+
<span>merged or dropped <b id="folded">0</b></span>
|
|
88
|
+
<span>encodes <b id="encodes">0</b></span>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="note" id="note"></div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<h2>What the four cases show</h2>
|
|
94
|
+
<table>
|
|
95
|
+
<tr><th>case</th><th>pending after the stall</th><th>why</th></tr>
|
|
96
|
+
<tr><td>streamed reply</td><td>1 op</td><td>every token appends to the same path</td></tr>
|
|
97
|
+
<tr><td>counter, 100,000 sets</td><td>1 op</td><td>each set overwrites the previous one</td></tr>
|
|
98
|
+
<tr><td>reply + status, interleaved</td><td>grows</td><td>the neighbour is always on the other path: adjacent-only folding cannot merge</td></tr>
|
|
99
|
+
<tr><td>new list items</td><td>grows</td><td>inserts carry new data: nothing to fold, this is the real change</td></tr>
|
|
100
|
+
</table>
|
|
101
|
+
<p>The last two cases are why a memory bound is still needed. It can be a plain count of pending ops after folding (say 1,000): past it the hold drops its list and marks “send the current value”, which then costs nothing more while the stall lasts.</p>
|
|
102
|
+
|
|
103
|
+
<h2>Why not time since the last ack?</h2>
|
|
104
|
+
<p>A time trigger (“no ack for T seconds: drop the list, send a snapshot later”) punishes the most common case: a phone in a tunnel while a reply streams into a long conversation. Folded, the 60 s stall costs one append of about 12 KB; a snapshot of the conversation costs its whole size (Astra's corpus: 4.6 KB of updates against 168 KB, recovery 0.2 s against 2.7 s). And before T, a fast producer on many paths still piles up rate × T ops: time bounds duration, not memory. With folding, the counter case (100,000 sets) is one op whatever the time, so time adds nothing there.</p>
|
|
105
|
+
</main>
|
|
106
|
+
|
|
107
|
+
<script>
|
|
108
|
+
const scenarios = {
|
|
109
|
+
stream: {
|
|
110
|
+
note: "Every token is an append to the same path: each new chip melts into the one before it.",
|
|
111
|
+
make: (i) => ({ verb: "a", path: "entries[7].text", value: ["The", " stall", " is", " over", " and", " the", " phone", " gets", " one", " append", "."][i % 11] }),
|
|
112
|
+
},
|
|
113
|
+
counter: {
|
|
114
|
+
note: "100,000 sets of one field: each set overwrites the previous one, so the list never holds more than one op.",
|
|
115
|
+
make: (i) => ({ verb: "s", path: "count", value: String(41 + i) }),
|
|
116
|
+
},
|
|
117
|
+
interleaved: {
|
|
118
|
+
note: "Tokens and a status field alternate: the last pending op is always on the other path, so adjacent-only folding cannot merge anything.",
|
|
119
|
+
make: (i) => i % 2 === 0
|
|
120
|
+
? { verb: "a", path: "entries[7].text", value: [" one", " two", " three", " four", " five", " six"][(i / 2) % 6] }
|
|
121
|
+
: { verb: "s", path: "status", value: i % 4 === 1 ? '"typing"' : '"tool"' },
|
|
122
|
+
},
|
|
123
|
+
list: {
|
|
124
|
+
note: "Each commit inserts a new item: that is new data, nothing to fold. A count bound turns a long list into “send the current value”.",
|
|
125
|
+
make: (i) => ({ verb: "p", path: "sessions", value: `+{id:${100 + i}}` }),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const $ = (id) => document.getElementById(id);
|
|
130
|
+
let scenario = "stream", pending = [], commits = 0, folded = 0, encodes = 0, timer = undefined, windowFull = true;
|
|
131
|
+
const CAP = 12; // shown small; the proposal is about 1,000
|
|
132
|
+
|
|
133
|
+
function label(op) {
|
|
134
|
+
const text = op.value.length > 22 ? `…${op.value.slice(-22)}` : op.value;
|
|
135
|
+
return op.verb === "a" ? `a ${op.path} "${text}"` : `${op.verb} ${op.path} ${op.value}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function chip(op, cls = "") {
|
|
139
|
+
const el = document.createElement("span");
|
|
140
|
+
el.className = `chip ${op.verb} ${cls}`;
|
|
141
|
+
el.textContent = op.verb === "snap" ? "snapshot needed" : label(op);
|
|
142
|
+
return el;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function render() {
|
|
146
|
+
$("commits").textContent = commits;
|
|
147
|
+
$("pendingOps").textContent = pending.filter((op) => op.verb !== "snap").length;
|
|
148
|
+
$("folded").textContent = folded;
|
|
149
|
+
$("encodes").textContent = encodes;
|
|
150
|
+
$("linkState").className = windowFull ? "tunnel" : "free";
|
|
151
|
+
$("linkState").textContent = windowFull ? "window full: 2 unacked (tunnel)" : "window open";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function step() {
|
|
155
|
+
const op = scenarios[scenario].make(commits);
|
|
156
|
+
commits++;
|
|
157
|
+
const incoming = $("incoming");
|
|
158
|
+
const shown = chip(op, "enter");
|
|
159
|
+
incoming.replaceChildren(shown);
|
|
160
|
+
requestAnimationFrame(() => shown.classList.remove("enter"));
|
|
161
|
+
if (!windowFull) {
|
|
162
|
+
encodes++;
|
|
163
|
+
send([op]);
|
|
164
|
+
render();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
setTimeout(() => enqueue(op), 380);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function enqueue(op) {
|
|
171
|
+
const lane = $("pending");
|
|
172
|
+
const last = pending.at(-1);
|
|
173
|
+
if (last?.verb === "snap") {
|
|
174
|
+
folded++;
|
|
175
|
+
render();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (last && last.path === op.path && ((last.verb === "a" && op.verb === "a") || ((last.verb === "a" || last.verb === "s") && op.verb === "s"))) {
|
|
179
|
+
const next = op.verb === "a" ? { ...last, value: last.value + op.value } : op;
|
|
180
|
+
pending[pending.length - 1] = next;
|
|
181
|
+
folded++;
|
|
182
|
+
const ghost = chip(op, "flash");
|
|
183
|
+
lane.appendChild(ghost);
|
|
184
|
+
requestAnimationFrame(() => ghost.classList.add("merge"));
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
ghost.remove();
|
|
187
|
+
const target = lane.lastElementChild;
|
|
188
|
+
if (target) {
|
|
189
|
+
target.textContent = label(next);
|
|
190
|
+
target.className = `chip ${next.verb} flash`;
|
|
191
|
+
setTimeout(() => target.classList.remove("flash"), 300);
|
|
192
|
+
}
|
|
193
|
+
}, 350);
|
|
194
|
+
} else if (pending.length >= CAP) {
|
|
195
|
+
folded += pending.length;
|
|
196
|
+
pending = [{ verb: "snap", path: "", value: "" }];
|
|
197
|
+
lane.replaceChildren(chip(pending[0], "flash"));
|
|
198
|
+
$("note").textContent = `Past the bound (${CAP} here, about 1,000 in the proposal): the list is dropped and the hold only remembers “send the current value”. Later commits cost nothing until the window opens.`;
|
|
199
|
+
} else {
|
|
200
|
+
pending.push(op);
|
|
201
|
+
const el = chip(op, "enter");
|
|
202
|
+
lane.appendChild(el);
|
|
203
|
+
requestAnimationFrame(() => el.classList.remove("enter"));
|
|
204
|
+
}
|
|
205
|
+
render();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function send(ops) {
|
|
209
|
+
const sentLane = $("sent");
|
|
210
|
+
const msg = document.createElement("span");
|
|
211
|
+
msg.className = "msg";
|
|
212
|
+
msg.textContent = ops[0]?.verb === "snap" ? "1 message: snapshot of the current value" : `1 message: ${ops.length} op${ops.length === 1 ? "" : "s"}`;
|
|
213
|
+
sentLane.replaceChildren(msg);
|
|
214
|
+
requestAnimationFrame(() => msg.classList.add("fly"));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ack() {
|
|
218
|
+
stop();
|
|
219
|
+
if (pending.length > 0) {
|
|
220
|
+
encodes++;
|
|
221
|
+
send(pending);
|
|
222
|
+
$("note").textContent = `The ack frees a slot: the pending list is encoded once, as one message (${commits} commits, ${pending[0].verb === "snap" ? "one snapshot" : pending.length + " op(s)"}).`;
|
|
223
|
+
} else {
|
|
224
|
+
$("note").textContent = "The ack frees a slot; nothing was pending.";
|
|
225
|
+
}
|
|
226
|
+
pending = [];
|
|
227
|
+
$("pending").replaceChildren();
|
|
228
|
+
windowFull = false;
|
|
229
|
+
render();
|
|
230
|
+
setTimeout(() => { windowFull = true; render(); }, 1600);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stop() {
|
|
234
|
+
clearInterval(timer);
|
|
235
|
+
timer = undefined;
|
|
236
|
+
$("play").textContent = "play";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reset() {
|
|
240
|
+
stop();
|
|
241
|
+
pending = []; commits = 0; folded = 0; encodes = 0; windowFull = true;
|
|
242
|
+
for (const id of ["incoming", "pending", "sent"]) $(id).replaceChildren();
|
|
243
|
+
$("note").textContent = scenarios[scenario].note;
|
|
244
|
+
render();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
$("play").onclick = () => {
|
|
248
|
+
if (timer) return stop();
|
|
249
|
+
$("play").textContent = "pause";
|
|
250
|
+
timer = setInterval(step, 700);
|
|
251
|
+
};
|
|
252
|
+
$("step").onclick = () => { stop(); step(); };
|
|
253
|
+
$("ack").onclick = ack;
|
|
254
|
+
$("reset").onclick = reset;
|
|
255
|
+
for (const button of document.querySelectorAll("#scenarios button")) {
|
|
256
|
+
button.onclick = () => {
|
|
257
|
+
for (const other of document.querySelectorAll("#scenarios button")) other.classList.toggle("on", other === button);
|
|
258
|
+
scenario = button.dataset.s;
|
|
259
|
+
reset();
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
reset();
|
|
263
|
+
</script>
|
|
264
|
+
</body>
|
|
265
|
+
</html>
|