-
Notifications
You must be signed in to change notification settings - Fork 0
docs: add doc comments and immutability design notes #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,41 +1,64 @@ | ||||||||||||||||||||||||
| # recollect | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| `tiye/cumulo/recollect` is the standalone structural diff/patch package extracted from the larger Cumulo demo. | ||||||||||||||||||||||||
| `tiye/recollect` is a structural JSON diff/patch library for MoonBit. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| It is intentionally kept as a leaf package inside this repository so it can be split into its own module later with minimal work. | ||||||||||||||||||||||||
| It computes a minimal sequence of `PatchOp` operations between two `Json` trees | ||||||||||||||||||||||||
| and applies them to reconstruct the target — suitable for efficient network | ||||||||||||||||||||||||
| transport in realtime sync architectures. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| It focuses on a small JSON-first API: | ||||||||||||||||||||||||
| ## Immutable data design | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - `diff_json(old_json, new_json)` computes structural patch operations between two `Json` trees. | ||||||||||||||||||||||||
| - `diff_value(old_value, new_value)` computes patches from typed MoonBit values via `ToJson`. | ||||||||||||||||||||||||
| - `apply_patch(root, patch)` applies a single patch operation to a `Json` tree. | ||||||||||||||||||||||||
| - `apply_patches(root, patches)` applies a patch sequence to a `Json` tree. | ||||||||||||||||||||||||
| - `apply_to_value(value, patches)` applies patches to a typed MoonBit value via `ToJson` and `FromJson`. | ||||||||||||||||||||||||
| `recollect` is designed for **immutable-data** workflows, as used in | ||||||||||||||||||||||||
| [Cumulo](https://github.com/Cumulo/moonbit-cumulo) and | ||||||||||||||||||||||||
| [Respo](https://github.com/Respo/respo.mbt). | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| The patch format is exposed through two public enums: | ||||||||||||||||||||||||
| - `diff_json` / `diff_value` — **read-only**; inputs are never modified. | ||||||||||||||||||||||||
| - `apply_patch` / `apply_patches` / `apply_to_value` — return a **new** `Json` | ||||||||||||||||||||||||
| tree (or typed value); unchanged subtrees are shared structurally with the | ||||||||||||||||||||||||
| original. The input is never mutated. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - `PathSegment`: `Field(String)` and `Index(Int)` | ||||||||||||||||||||||||
| - `PatchOp`: `Set`, `Remove`, `Insert`, `Delete`, `Move` | ||||||||||||||||||||||||
| This means it is safe to hold references to both the old and new state at the | ||||||||||||||||||||||||
| same time, which is required for Respo's render-loop equality check and for | ||||||||||||||||||||||||
| Cumulo's server-side twig/diff logic. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Current package boundary: | ||||||||||||||||||||||||
| ```moonbit nocheck | ||||||||||||||||||||||||
| // old_remote and new_remote are independent values — old is never touched | ||||||||||||||||||||||||
| let new_remote = @recollect.apply_to_value(old_remote, patches) | ||||||||||||||||||||||||
| catch { _ => old_remote } | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - source: `recollect/recollect.mbt` | ||||||||||||||||||||||||
| - tests: `recollect/recollect_test.mbt` | ||||||||||||||||||||||||
| - only dependency: `moonbitlang/core/json` | ||||||||||||||||||||||||
| > ⚠️ Do **not** wrap your state struct in a `mut` field and mutate it in place. | ||||||||||||||||||||||||
| > Doing so breaks structural equality (`==`) and makes the render loop skip | ||||||||||||||||||||||||
| > updates. Always replace the whole value with the newly returned one. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Import | ||||||||||||||||||||||||
| ## API | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Add the package to a `moon.pkg` import list: | ||||||||||||||||||||||||
| - `diff_json(old, new)` — compute `Array[PatchOp]` between two `Json` trees | ||||||||||||||||||||||||
| - `diff_value(old, new)` — same, via `ToJson` serialization of typed values | ||||||||||||||||||||||||
| - `apply_patch(root, patch)` — apply one `PatchOp`, return new `Json` | ||||||||||||||||||||||||
| - `apply_patches(root, patches)` — apply a sequence, return new `Json` | ||||||||||||||||||||||||
| - `apply_to_value(value, patches)` — apply patches to a typed value via `ToJson`/`FromJson` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```moonbit | ||||||||||||||||||||||||
| Patch format (both enums are `pub(all)` with `ToJson`/`FromJson`): | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - `PathSegment`: `Field(String)` | `Index(Int)` | ||||||||||||||||||||||||
| - `PatchOp`: `Set` | `Remove` | `Insert` | `Delete` | `Move` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| `Insert` / `Delete` / `Move` are only emitted for **keyed arrays** — arrays | ||||||||||||||||||||||||
| where every element is a JSON object with a unique string `"id"` field. | ||||||||||||||||||||||||
| Plain arrays fall back to positional `Set` patches. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Import | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||
| "tiye/cumulo/recollect" @recollect, | ||||||||||||||||||||||||
| "tiye/recollect" @recollect, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Json example | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```moonbit | ||||||||||||||||||||||||
| ```moonbit nocheck | ||||||||||||||||||||||||
| let before : Json = { | ||||||||||||||||||||||||
| "todos": [{ "id": "t-1", "title": "A", "done": false }], | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
@@ -47,6 +70,43 @@ let after : Json = { | |||||||||||||||||||||||
| ], | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| let patches = @recollect.diff_json(before, after) | ||||||||||||||||||||||||
| let rebuilt = @recollect.apply_patches(before, patches) | ||||||||||||||||||||||||
| // rebuilt == after | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Typed value example | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| ## Typed value example | |
| ## Typed value example (structs) |
Copilot
AI
May 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Typed value example code block is closed with }, which breaks Markdown rendering, and the following let after : Json = { ... } snippet appears to be stray/duplicated content outside any fenced block. Fix the fence terminator to ``` and remove the unrelated duplicated JSON snippet so the README renders and copy/paste examples work.
| ```} | |
| let after : Json = { | |
| "todos": [ | |
| { "id": "t-1", "title": "A", "done": true }, | |
| { "id": "t-2", "title": "B", "done": false }, | |
| ], | |
| } | |
| let patches = @recollect.diff_json(before, after) | |
| let rebuilt = @recollect.apply_patches(before, patches) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,33 @@ | ||||||||||||||||||||||||||||||
| ///| | ||||||||||||||||||||||||||||||
| /// A single segment in a path through a `Json` tree. | ||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||
| /// Paths are sequences of segments used by `PatchOp` to address nested | ||||||||||||||||||||||||||||||
| /// locations. `Field("key")` traverses into a JSON object; `Index(n)` | ||||||||||||||||||||||||||||||
| /// traverses into a JSON array. | ||||||||||||||||||||||||||||||
| pub(all) enum PathSegment { | ||||||||||||||||||||||||||||||
| Field(String) | ||||||||||||||||||||||||||||||
| Index(Int) | ||||||||||||||||||||||||||||||
| } derive(Eq, Debug, FromJson, ToJson) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ///| | ||||||||||||||||||||||||||||||
| /// A single structural edit operation on a `Json` tree. | ||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||
| /// Patch operations are always **non-destructive** with respect to the input: | ||||||||||||||||||||||||||||||
| /// every `apply_*` function returns a new tree rather than mutating the | ||||||||||||||||||||||||||||||
| /// original. This makes the patch format safe to use with immutable-data | ||||||||||||||||||||||||||||||
| /// architectures (e.g. Cumulo server state, Respo stores). | ||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||
| /// | Variant | Meaning | | ||||||||||||||||||||||||||||||
| /// |---------|---------| | ||||||||||||||||||||||||||||||
| /// | `Set` | Overwrite a value at `path` (creates intermediate nodes if absent) | | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| /// | `Set` | Overwrite a value at `path` (creates intermediate nodes if absent) | | |
| /// | `Set` | Overwrite a value at `path` (creates the leaf only when the parent path exists) | |
Copilot
AI
May 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
diff_json is documented as producing a "minimal" patch sequence, but the current positional array diff emits per-index Set operations for shifts (e.g. inserting at the front of a plain array) and is not minimal in terms of number of ops. Consider relaxing the wording (e.g. "structural" / "deterministic") or defining what "minimal" means here and ensuring the implementation meets it.
| /// Compute a minimal sequence of `PatchOp`s that transforms `old_json` into | |
| /// `new_json`. | |
| /// | |
| /// The diff is **structural**: objects are compared field-by-field, arrays are | |
| /// compared element-by-element (or by `"id"` key when all elements carry | |
| /// unique string ids). Unchanged subtrees produce no patches. | |
| /// Compute a deterministic structural sequence of `PatchOp`s that transforms | |
| /// `old_json` into `new_json`. | |
| /// | |
| /// The diff is **structural**: objects are compared field-by-field, arrays are | |
| /// compared element-by-element (or by `"id"` key when all elements carry | |
| /// unique string ids). Unchanged subtrees produce no patches. For plain | |
| /// arrays, the patch sequence is positional and not guaranteed to be minimal | |
| /// in number of operations. |
Copilot
AI
May 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The apply_patches doc example is self-referential and will always pass (assert_eq(new_json, new_json)), and it also reuses new_json as both the expected value and the result name. Update the example to compare against an expected/target value so it actually demonstrates correctness.
| /// let new_json = apply_patches(old_json, diff_json(old_json, new_json)) | |
| /// assert_eq(new_json, new_json) | |
| /// let patched_json = apply_patches(old_json, diff_json(old_json, target_json)) | |
| /// assert_eq(patched_json, target_json) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
README claims the library computes a "minimal" sequence of
PatchOps. The implementation falls back to positional array diffing for non-keyed arrays, which can produce non-minimal sequences for insertions/deletions (it may emit multipleSets instead of a singleInsert/Delete). Consider adjusting the wording to avoid promising minimality unless it’s guaranteed.