docblocks-core 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,67 @@
1
+ # docblocks-core
2
+
3
+ ## 0.1.0
4
+
5
+ ### Added
6
+
7
+ - The initial release of the headless CRDT document engine
8
+ (dependencies: `yjs`, `y-protocols`; no UI dependencies — the server,
9
+ the blob resolution, and the persistence are consumer-side concerns).
10
+ - The document data format (JSON `v: 1`): the document meta (`id`,
11
+ `title`, the optional `avatar`, `createdAt`) + the flat block map
12
+ (`id → block`) with the explicit order via `children: [blockId]`; the
13
+ JSON ↔ Yjs layout is isomorphic (every JSON field has exactly one Yjs
14
+ image — the root's `children` live in the registered `rootOrder` array,
15
+ and each non-root block carries a Yjs-layout-only `parent` field that
16
+ never reaches the JSON).
17
+ - The block and mark registry as the single source of truth, with the
18
+ generated discriminated-union types (an invalid attribute is a type
19
+ error at the call site) and the typed command errors (the
20
+ `UnknownBlockError`, the `InvalidRangeError`, the `DetachedBlockError`, ...).
21
+ - The v1 block types: `root`, `paragraph`, `quote`, `heading` (levels
22
+ 1–6), `list-item` (`bullet` / `number` / `task`, the nesting over the
23
+ `indent` attribute), `callout` (the `variant` + the additive
24
+ `color?` / `icon?` overrides), `code`, `divider`, `image` (the opaque
25
+ `src` key), `embed`, the `table` / `table-row` / `table-cell` triple
26
+ (the cells are the mini-docs; the column set is the shared
27
+ `columnOrder` id array + the cell's `columnId` binding), the
28
+ `database` block (the inline `columns` + `rows` form and the external
29
+ `tableId` form), and the five canvas types (`surface`, `shape`,
30
+ `connector`, `frame`, `canvas-text`) with the optional `geometry`.
31
+ - The text model: a single `Y.Text` per text block with the
32
+ character-attribute marks (`bold`, `italic`, `strike`, `underline`,
33
+ `code`, `link`, `color`, `highlight`, `mention`) and the JSON run
34
+ array (`content: [{ text, marks }]`); the formatting is a pure
35
+ attribute op, so the character identity survives the concurrent
36
+ formatting and deletion.
37
+ - The semantic command API (the only path for the normal editing): the
38
+ block commands (`insertBlock` / `updateBlock` / `moveBlock` /
39
+ `removeBlock` / `changeBlockType`), the text commands (`insertText` /
40
+ `deleteText` / `setMark` / `unsetMark`), the document meta
41
+ (`setTitle` / `setAvatar`), the geometry and the z-order
42
+ (`setGeometry`, `setZOrder`), the database row commands, and the table
43
+ row / column commands; `transact` batches the commands into one undo
44
+ step and one wire update.
45
+ - The selection view (points and ranges, the rebase rules over the
46
+ concurrent edits) and the per-document undo / redo history; the
47
+ selection is the client / view state, never document data (the other
48
+ clients' selections travel only in the ephemeral awareness).
49
+ - The CRDT-level power-user API: `encode` / `encodeUpdate` /
50
+ `applyUpdate` / `toJSON` / `fromJSON`, and the explicit `doc.raw:
51
+ Y.Doc` escape hatch (not guaranteed across versions).
52
+ - The transport layer: the `SyncProvider` interface + the
53
+ `WsSyncProvider` (the standard y-protocols over WebSocket,
54
+ wire-compatible with `y-websocket` / `pycrdt`) and the `attach(doc,
55
+ provider)` glue (the origin guard + the full-state bootstrap; the
56
+ send buffers over the not-open socket); the presence over the
57
+ y-protocols awareness (the selection + the `updatedAt` per peer, the
58
+ ~30 s staleness TTL).
59
+ - The frame as the viewport window into its own surface (no children,
60
+ no content, no `zIndex`) + the page frame block (the `frameId?` weak
61
+ reference); the canvas z-order stands the derived dense 1..N mirror
62
+ (`derivedZIndex`, re-derived on load).
63
+ - The complementary reader / validator postures: the strict validator
64
+ (only the known types and marks) and the lenient reader (the unknown
65
+ type → the placeholder fallback, the unknown mark → ignored, the
66
+ unknown attribute → ignored) — the forward-compatibility across the
67
+ minor versions of the major data format.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wereintouch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,168 @@
1
- # Temporary Holding Version
1
+ # docblocks-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The headless CRDT document engine for docblocks — the data half: the
4
+ document model, the editing commands, the selection, and the sync
5
+ transport. There is no UI in this package; rendering and editing live in
6
+ [`docblocks-react-ui`](https://www.npmjs.com/package/docblocks-react-ui).
7
+
8
+ The engine is built on [Yjs](https://yjs.dev) (a CRDT): documents converge
9
+ across peers even when updates arrive out of order, duplicated, or with
10
+ losses. The only runtime dependencies are `yjs`, `y-protocols`, and `lib0`.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm i docblocks-core
16
+ ```
17
+
18
+ The package is ESM-only (`import`, not `require`); it stands Node
19
+ `^22.22.2 || ^24.15.0 || >=26.0.0` (the `engines` field).
20
+
21
+ A document is created, edited with commands, and read back as plain JSON —
22
+ every read returns a **snapshot** (a plain object), never a live Yjs
23
+ reference, so mutating a returned block does nothing.
24
+
25
+ ```ts
26
+ import { createDoc, fromJSON } from "docblocks-core";
27
+
28
+ const doc = createDoc({ id: "my-doc" });
29
+
30
+ const para = doc.insertBlock("paragraph");
31
+ doc.insertText(para, 0, "hello world");
32
+ doc.setMark(para, 0, 5, { type: "bold" });
33
+
34
+ const json = doc.toJSON(); // { v, meta, blocks, children }
35
+ // …persist or send `json` …
36
+ const restored = fromJSON(json); // a fresh Doc over the same data
37
+ ```
38
+
39
+ Commands are `Doc` methods. Each command runs inside a transaction — one
40
+ undo step, one wire update. Batch several into a single step with
41
+ `transact`:
42
+
43
+ ```ts
44
+ doc.transact(() => {
45
+ const h = doc.insertBlock("heading", { level: 1 }, { after: para });
46
+ doc.insertText(h, 0, "Title");
47
+ }); // one undo step, one wire update
48
+ ```
49
+
50
+ The block and mark registries drive the command types: `doc.insertBlock`
51
+ accepts only a known block type, and its `attrs` are required exactly when
52
+ the type demands them — `doc.insertBlock("heading", { level: 7 })` is a
53
+ compile-time error. Unknown input throws a typed error
54
+ (`UnknownBlockError`, `InvalidRangeError`, …); commands never no-op
55
+ silently.
56
+
57
+ ## The Doc
58
+
59
+ `createDoc(options?)` builds a document with a root block and an empty,
60
+ editable first paragraph. The other entry points:
61
+
62
+ - `fromJSON(json)` — build a `Doc` from a JSON snapshot.
63
+ - `loadDoc(bytes)` — build a `Doc` from an encoded full state (a base
64
+ snapshot; apply diffs with `applyUpdate`).
65
+ - `doc.toJSON()` — the JSON snapshot (`{ v, meta, blocks, children }`).
66
+ - `doc.encode()` / `doc.encodeUpdate(stateVector?)` — the encoded full
67
+ state / the incremental update.
68
+ - `doc.applyUpdate(bytes)` — apply an encoded update (the remote-apply path).
69
+
70
+ `doc.raw` exposes the underlying `Y.Doc`; it is an explicit escape hatch
71
+ and its shape is not guaranteed.
72
+
73
+ ## Commands
74
+
75
+ Editing goes through the semantic API (the only path for normal editing).
76
+ The commands translate into `Y.Doc` mutations inside a transaction:
77
+
78
+ - **Blocks** — `insertBlock`, `updateBlock`, `moveBlock`, `removeBlock`,
79
+ `changeBlockType`.
80
+ - **Text** — `insertText`, `deleteText`, `setMark`, `unsetMark`, and the
81
+ range forms (`deleteTextRange`, `setMarkRange`).
82
+ - **Document meta** — `setTitle`, `setAvatar`, `setGeometry`, `setZOrder`.
83
+ - **Database rows** — the row commands (`insertRow`, `setCell`,
84
+ `removeRow`, `moveRow`, …).
85
+ - **Table rows / columns** — `insertTableRow`, `removeTableRow`,
86
+ `insertTableColumn`, `removeTableColumn`, the move / duplicate / color /
87
+ width commands, and `setTableRowColor`.
88
+
89
+ Reads: `getBlock(id)`, `getChildren(id)`, `getRuns(id)`, `getText(id)`,
90
+ `getTextLength(id)`. History: `undo()` / `redo()` (with `canUndo()` /
91
+ `canRedo()`) — one `transact` is one undo step, and a command batched
92
+ into a `transact` is one step across the batch.
93
+
94
+ ## Sync
95
+
96
+ `attach(doc, provider)` is the glue between a `Doc` and a
97
+ `SyncProvider`: it bootstraps the session (sending the full local state
98
+ first), then routes local transactions out and remote updates in. `detach()`
99
+ on the returned session tears the session down.
100
+
101
+ `WsSyncProvider` speaks the standard y-protocols wire over a WebSocket
102
+ (wire-compatible with a standard y-websocket / pycrdt server):
103
+
104
+ ```ts
105
+ import { WsSyncProvider, attach } from "docblocks-core";
106
+
107
+ const provider = new WsSyncProvider("wss://my-server/ws/room-1", {
108
+ token: "…",
109
+ });
110
+ await provider.connect();
111
+ const session = attach(doc, provider);
112
+ // … on teardown:
113
+ session.detach();
114
+ ```
115
+
116
+ Reactivity: `doc.onChange(handler)` fires for every transaction;
117
+ `doc.onRemoteChange(handler)` fires for non-local-origin transactions only
118
+ (the feed a sync consumer's write-back guards hook into).
119
+
120
+ ## Selection
121
+
122
+ Selection is client / view state, not document data — it is not in the JSON
123
+ and not in document updates. `createSelection(doc)` builds a `SelectionView`
124
+ over the doc; other clients' selections travel only in the ephemeral
125
+ awareness channel (the presence payload). `presenceSelectionOf(selection)`
126
+ projects a selection into the presence payload shape.
127
+
128
+ ## Data format
129
+
130
+ The JSON format is `v: 1`: `meta` (the doc `id`, `title`, the optional
131
+ `avatar`) plus a **flat block map** (`id → block`) with explicit order via
132
+ `children: [blockId]`; nesting (lists, table cells) is `children`. The text
133
+ of a text block is `content: [{ text, marks }]` runs. JSON ↔ Yjs is
134
+ isomorphic — every JSON field has exactly one Yjs image.
135
+
136
+ The v1 registry covers the page types (`paragraph`, `quote`, `heading`,
137
+ `list-item`, `callout`, `code`, `divider`, `image`, `embed`, `table` /
138
+ `table-row` / `table-cell`, `database`), the five canvas types (`surface`,
139
+ `shape`, `connector`, `frame`, `canvas-text`), and `root`; the v1 marks
140
+ are `bold`, `italic`, `strike`, `underline`, `code` (exclusive), `link`,
141
+ `color`, `highlight`, and `mention`. The type lists stand exported
142
+ (`PAGE_BLOCK_TYPES`, `LEAF_BLOCK_TYPES`, `BLOCK_REGISTRY`,
143
+ `MARK_REGISTRY`, `CALLOUT_ICONS`).
144
+
145
+ The block and mark registries are the extension point. Adding a block type
146
+ or mark is additive (a minor): the registry entry plus a renderer; the
147
+ validator and the commands stay registry-driven. The reader tolerates the
148
+ unknown (an unknown block type renders a placeholder, an unknown mark is
149
+ ignored and its text stays); the validator is strict (`validateDoc` reports
150
+ only known types and marks — it protects the data you own). A breaking
151
+ change is a major: the reader rejects the unknown major, and a migration
152
+ helper converts the data. `flattenDoc(json)` normalizes the legacy nested
153
+ shape into the flat page model at load.
154
+
155
+ ## Validation
156
+
157
+ `validateDoc(json, lenient?)` returns the list of validation errors (empty
158
+ is valid). Strict (the default) is the writer's contract — it checks the
159
+ known types and mark values. Lenient is the reader boundary — unknown types
160
+ and marks pass through (the reader preserves them), so a reader of `v1.0`
161
+ can safely load `v1.5` data.
162
+
163
+ ## Non-goals
164
+
165
+ This package is deliberately headless. It does not ship a UI (that is
166
+ `docblocks-react-ui`), a server (any Yjs-compatible server is the receiving
167
+ side), or blob resolution (the consumer provides a `BlobStore`). It holds no
168
+ persistence — snapshot and update storage is a consumer-side concern.