Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion apps/site/src/generated/repo-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,42 @@ export const repoCatalog = {
"headless"
]
},
{
"path": "labs/extensions/causal-patch-inbox",
"name": "@interactive-os/json-document-causal-patch-inbox",
"status": "lab-extension",
"private": true,
"publishable": false,
"version": "0.1.0",
"description": "Lab causal JSON Patch inbox for json-document documents.",
"license": "MIT",
"summary": "Lab module for admitting dependency-declared JSON Patch envelopes, holding\nout-of-order work, and applying each ready envelope to one `JSONDocument`.",
"guidance": {
"useFor": "queue and deduplicate dependency-declared envelopes, applying each as one core JSON Patch batch",
"notFor": "CRDT/OT convergence, transport, persistence, ready-time rebase, or conflict resolution"
},
"publicExports": [
"CausalPatchEnvelope",
"CausalPatchInbox",
"CausalPatchInboxSnapshot",
"CausalPatchIngestError",
"CausalPatchIngestErrorCode",
"CausalPatchIngestOk",
"CausalPatchIngestResult",
"FailedCausalPatch",
"FaultedCausalPatch",
"QueuedCausalPatch",
"createCausalPatchInbox"
],
"publicExportCount": 11,
"keywords": [
"@interactive-os/json-document",
"causal",
"headless",
"inbox",
"patch"
]
},
{
"path": "labs/extensions/change-case",
"name": "@interactive-os/json-document-change-case",
Expand Down Expand Up @@ -3315,7 +3351,7 @@ export const repoCatalog = {
"totals": {
"packages": 20,
"officialExtensions": 19,
"labExtensions": 40,
"labExtensions": 41,
"apps": 12
}
} as const;
Expand Down
3 changes: 2 additions & 1 deletion docs/generated/extensions-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Official extensions: 19
| `@interactive-os/json-document-search-replace` | 23 | find and replace text across document string fields | regex engines, rendered text extraction, or search UI | Official headless search and replace extension for text fields in `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-snippets` | 12 | insert reusable JSON payloads with schema-safe paste checks | slash palette UI or snippet storage | Official headless snippet insertion extension for `@interactive-os/json-document` documents. |

Lab extensions: 40
Lab extensions: 41

Lab extensions are private candidates. They are listed to show product pressure, not as shipped packages.

Expand All @@ -39,6 +39,7 @@ Lab extensions are private candidates. They are listed to show product pressure,
| `@interactive-os/json-document-batch-update` | lab-only | 10 | set a field across a list of selected item pointers to a constant or computed value | selecting which items to edit, or JSONPath query-driven replacement | Lab batch-update extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-bookmarks` | lab-only | 8 | keep named JSON Pointer locations stable across edits | browser bookmarks or route state | Headless bookmark tracking extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-calculated-fields` | lab-only | 12 | sync host-computed derived JSON fields | formula languages or dependency runtimes | Lab calculated field extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-causal-patch-inbox` | lab-only | 11 | queue and deduplicate dependency-declared envelopes, applying each as one core JSON Patch batch | CRDT/OT convergence, transport, persistence, ready-time rebase, or conflict resolution | Lab module for admitting dependency-declared JSON Patch envelopes, holding out-of-order work, and applying each ready envelope to one `JSONDocument`. |
| `@interactive-os/json-document-change-case` | lab-only | 9 | apply case/whitespace transforms (upper, lower, trim, title) to a string field | locale-aware casing, rich text formatting toolbars, or find/replace | Lab change-case extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-checkpoints` | lab-only | 13 | name and restore document snapshots | durable version graphs or cloud backup | Lab checkpoint extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-clear-contents` | lab-only | 10 | reset selected fields to schema-derived empty values, keeping structure | structural delete, caller-supplied bulk replace, or enum/object default policy | Lab clear-contents extension for `@interactive-os/json-document` documents. |
Expand Down
38 changes: 37 additions & 1 deletion docs/generated/repo-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1834,6 +1834,42 @@
"headless"
]
},
{
"path": "labs/extensions/causal-patch-inbox",
"name": "@interactive-os/json-document-causal-patch-inbox",
"status": "lab-extension",
"private": true,
"publishable": false,
"version": "0.1.0",
"description": "Lab causal JSON Patch inbox for json-document documents.",
"license": "MIT",
"summary": "Lab module for admitting dependency-declared JSON Patch envelopes, holding\nout-of-order work, and applying each ready envelope to one `JSONDocument`.",
"guidance": {
"useFor": "queue and deduplicate dependency-declared envelopes, applying each as one core JSON Patch batch",
"notFor": "CRDT/OT convergence, transport, persistence, ready-time rebase, or conflict resolution"
},
"publicExports": [
"CausalPatchEnvelope",
"CausalPatchInbox",
"CausalPatchInboxSnapshot",
"CausalPatchIngestError",
"CausalPatchIngestErrorCode",
"CausalPatchIngestOk",
"CausalPatchIngestResult",
"FailedCausalPatch",
"FaultedCausalPatch",
"QueuedCausalPatch",
"createCausalPatchInbox"
],
"publicExportCount": 11,
"keywords": [
"@interactive-os/json-document",
"causal",
"headless",
"inbox",
"patch"
]
},
{
"path": "labs/extensions/change-case",
"name": "@interactive-os/json-document-change-case",
Expand Down Expand Up @@ -3314,7 +3350,7 @@
"totals": {
"packages": 20,
"officialExtensions": 19,
"labExtensions": 40,
"labExtensions": 41,
"apps": 12
}
}
151 changes: 151 additions & 0 deletions labs/extensions/causal-patch-inbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# @interactive-os/json-document-causal-patch-inbox

Lab module for admitting dependency-declared JSON Patch envelopes, holding
out-of-order work, and applying each ready envelope to one `JSONDocument`.

```ts
const inbox = createCausalPatchInbox(doc);

inbox.ingest({
id: "change-2",
dependsOn: ["change-1"],
operations: [
{ op: "test", path: "/title", value: "Ready" },
{ op: "replace", path: "/title", value: "Reviewed" },
],
});
// queued: change-2 is waiting for change-1

const result = inbox.ingest({
id: "change-1",
dependsOn: [],
operations: [
{ op: "test", path: "/status", value: "draft" },
{ op: "replace", path: "/status", value: "ready" },
],
});

if (result.ok) {
result.applied; // ["change-1", "change-2"]
result.pending; // ids from this input that remain queued
}
```

The envelope operations are already-materialized JSON Patch. They can be raw
protocol output or guarded operations produced by another planner. This module
owns causal admission and scheduling, not target recovery or merge policy.

An `ingest` batch is preflighted before any new envelope is admitted. An exact
duplicate is a no-op; the same id with different dependencies or operations is
`duplicate_mismatch`. Dependency order has set semantics, while operation and
array order remain significant. A known graph cycle rejects the closing batch.

Ready envelopes currently known to one drain are applied by locale-independent
id order. Each envelope is one atomic `doc.patch`; causal frontier state moves
only after that patch succeeds. A batch is not one transaction: earlier
envelopes remain applied when a later envelope fails. Patch failure globally
blocks this first tracer, preserving the failed envelope and its descendants
for inspection. Recovery, skipping, and conflict resolution need a later
policy rather than an implicit retry.

`current().frontier` is the maximal set in the declared dependency DAG. It is
not an actor clock or version vector. `queued[].missing` contains dependencies
that have not been applied by this inbox. Snapshot status `active` describes a
usable inbox, including one that has pending dependencies; it does not mean
that a ready envelope currently exists. `blocked` preserves a structured patch
failure, while `faulted` preserves an unexpected thrown execution failure that
has no `JSONResult`. Full queue snapshots are produced only by `current()`.
Successful `ingest` results report only this call's `applied`, `pending`, and
`duplicates` ids.

## Scope

- Defensively validate and copy one envelope or a batch before admission.
- Queue envelopes whose declared dependencies have not been applied.
- Distinguish exact duplicate delivery from an id/payload mismatch.
- Reject empty ids, invalid dependency ids, self/duplicate dependencies,
accessor/non-enumerable data, non-JSON payloads, and cycles closed by new
dependency edges.
- Choose the smallest currently ready id with a private min-heap.
- Wake dependents through a reverse dependency index rather than rescanning the
whole queue after every successful patch.
- Apply one envelope through one public `doc.patch` call and advance causal
state only after success.
- Permit an empty projection patch to advance causal state.
- Stop with structured `patch_failed` state for non-strict documents; preserve
the core `JSONDocumentError` throw policy for strict documents.
- Move to terminal `faulted` state when patch execution throws without changing
the projection and without supplying a structured `JSONDocumentError`.
- Reject reentrant ingestion as `busy` while a document patch is publishing.
- Detect direct document mutation and move the inbox to `diverged` rather than
inferring a causal envelope from an observed patch.
- Dispose the document subscription explicitly.

## Performance

- Envelope validation, defensive copying, and exact comparison are linear in
envelope payload size; dependency normalization also sorts the dependency
ids.
- Known-id lookup is average `O(1)`. Ready selection is `O(log V)`, and each
reverse dependency edge is released once.
- Cycle preflight first consults the reverse dependency index. A new id with no
incoming pending edge cannot close a cycle and needs no graph walk. Other new
edges walk only reachable dependency paths with an iterative stack; the
implementation neither rebuilds unrelated graph state nor overflows the
JavaScript call stack on deep chains.
- `current()` sorts and defensively copies frontier and queue observations, so
its cost grows with the exposed state. `ingest()` does not call `current()`;
callers pay that observation cost only when they request a snapshot.
- The module does not clone, diff, or stringify the whole document. Core patch
application and schema/refinement validation can still dominate runtime.
- Exact dedup retains accepted envelope payloads, and the causal ledger has no
compaction in this lab. Memory therefore grows with accepted work.

## Non-goals

- No CRDT, OT, LWW policy, convergence claim, total-order protocol, sequencer,
watermark, actor clock, tombstone, or replicated undo.
- No arrival-order-independent result across separate `ingest` calls. Only the
ready set known to the current drain is ordered deterministically.
- No local submit/envelope generation, acknowledgement, transport, reconnect,
offline queue, persistence, checkpoint, or compaction format.
- No generic protocol or materialization Adapter interface yet.
- No ready-time positional rebase, stable-id lookup, target repair, automatic
retry, conflict resolution, or independent-branch skipping after failure.
- No selection, presence, DOM input, render scheduling, or focus policy.
- No support for multiple causal owners mutating the same document projection.
- No change to the `JSONDocument` public interface or core state.

## Friction report

The public document subscription, active envelope id, metadata marker, and
publication count distinguish this module's synchronous patch from ordinary
host publications. Reusing a previously observed marker outside that active
publication is rejected, and a nested patch that copies current metadata adds
a second publication and causes divergence. The marker remains a cooperative
in-process convention rather than a security boundary. Running another inbox
against the same document makes the first one diverge instead of trying to
merge two causal ledgers.

Only mutations published through the public `JSONDocument` interface are
observable. In-place mutation of an object reachable through `doc.value` emits
no subscription event and cannot be detected here; callers must treat document
values as owned projection state rather than a side channel for mutation.

Typed patch failure is a clean seam: core keeps the JSON projection atomic, so
the inbox can leave its frontier unchanged. The first strict
`JSONDocumentError` still propagates but its structured result is retained as
the global blocker, so later delivery does not retry implicitly. An error
callback or schema path can instead throw before returning a `JSONResult`; when
the projection is unchanged, the original exception propagates once and the
inbox becomes `faulted` rather than retrying. A host `onChange` or subscriber
can throw after core has published, however. The public interfaces cannot roll
that publication back across modules. This lab detects the changed projection
and halts as `diverged`; it does not claim crash-atomicity across arbitrary host
callbacks.

Already-materialized guarded operations can become stale while waiting for
dependencies. The inbox then blocks without rewriting their intent. The next
useful experiment is ready-time materialization with at least two concrete
policies, such as positional patch rebase and stable-id target recovery. That
evidence should come before exposing a generic Adapter seam or choosing OT/CRDT.
49 changes: 49 additions & 0 deletions labs/extensions/causal-patch-inbox/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@interactive-os/json-document-causal-patch-inbox",
"version": "0.1.0",
"description": "Lab causal JSON Patch inbox for json-document documents.",
"private": true,
"type": "module",
"license": "MIT",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/developer-1px/json-document.git",
"directory": "labs/extensions/causal-patch-inbox"
},
"keywords": [
"@interactive-os/json-document",
"causal",
"inbox",
"patch",
"headless"
],
"files": [
"README.md",
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"clean": "rm -rf dist",
"build": "npm run clean && tsc -p tsconfig.json",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc -p tsconfig.test.json --noEmit",
"verify": "npm run typecheck && npm test && npm run build"
},
"peerDependencies": {
"@interactive-os/json-document": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^4.1.7",
"zod": "^4.0.0",
"@interactive-os/json-document": "*"
}
}
Loading