docblocks-core 0.0.0-stage → 0.1.1

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,74 @@
1
+ # docblocks-core
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - The README's `createDoc`'s contract stands the actual initial state (only
8
+ the root block — no auto first paragraph).
9
+
10
+ ## 0.1.0
11
+
12
+ ### Added
13
+
14
+ - The initial release of the headless CRDT document engine
15
+ (dependencies: `yjs`, `y-protocols`; no UI dependencies — the server,
16
+ the blob resolution, and the persistence are consumer-side concerns).
17
+ - The document data format (JSON `v: 1`): the document meta (`id`,
18
+ `title`, the optional `avatar`, `createdAt`) + the flat block map
19
+ (`id → block`) with the explicit order via `children: [blockId]`; the
20
+ JSON ↔ Yjs layout is isomorphic (every JSON field has exactly one Yjs
21
+ image — the root's `children` live in the registered `rootOrder` array,
22
+ and each non-root block carries a Yjs-layout-only `parent` field that
23
+ never reaches the JSON).
24
+ - The block and mark registry as the single source of truth, with the
25
+ generated discriminated-union types (an invalid attribute is a type
26
+ error at the call site) and the typed command errors (the
27
+ `UnknownBlockError`, the `InvalidRangeError`, the `DetachedBlockError`, ...).
28
+ - The v1 block types: `root`, `paragraph`, `quote`, `heading` (levels
29
+ 1–6), `list-item` (`bullet` / `number` / `task`, the nesting over the
30
+ `indent` attribute), `callout` (the `variant` + the additive
31
+ `color?` / `icon?` overrides), `code`, `divider`, `image` (the opaque
32
+ `src` key), `embed`, the `table` / `table-row` / `table-cell` triple
33
+ (the cells are the mini-docs; the column set is the shared
34
+ `columnOrder` id array + the cell's `columnId` binding), the
35
+ `database` block (the inline `columns` + `rows` form and the external
36
+ `tableId` form), and the five canvas types (`surface`, `shape`,
37
+ `connector`, `frame`, `canvas-text`) with the optional `geometry`.
38
+ - The text model: a single `Y.Text` per text block with the
39
+ character-attribute marks (`bold`, `italic`, `strike`, `underline`,
40
+ `code`, `link`, `color`, `highlight`, `mention`) and the JSON run
41
+ array (`content: [{ text, marks }]`); the formatting is a pure
42
+ attribute op, so the character identity survives the concurrent
43
+ formatting and deletion.
44
+ - The semantic command API (the only path for the normal editing): the
45
+ block commands (`insertBlock` / `updateBlock` / `moveBlock` /
46
+ `removeBlock` / `changeBlockType`), the text commands (`insertText` /
47
+ `deleteText` / `setMark` / `unsetMark`), the document meta
48
+ (`setTitle` / `setAvatar`), the geometry and the z-order
49
+ (`setGeometry`, `setZOrder`), the database row commands, and the table
50
+ row / column commands; `transact` batches the commands into one undo
51
+ step and one wire update.
52
+ - The selection view (points and ranges, the rebase rules over the
53
+ concurrent edits) and the per-document undo / redo history; the
54
+ selection is the client / view state, never document data (the other
55
+ clients' selections travel only in the ephemeral awareness).
56
+ - The CRDT-level power-user API: `encode` / `encodeUpdate` /
57
+ `applyUpdate` / `toJSON` / `fromJSON`, and the explicit `doc.raw:
58
+ Y.Doc` escape hatch (not guaranteed across versions).
59
+ - The transport layer: the `SyncProvider` interface + the
60
+ `WsSyncProvider` (the standard y-protocols over WebSocket,
61
+ wire-compatible with `y-websocket` / `pycrdt`) and the `attach(doc,
62
+ provider)` glue (the origin guard + the full-state bootstrap; the
63
+ send buffers over the not-open socket); the presence over the
64
+ y-protocols awareness (the selection + the `updatedAt` per peer, the
65
+ ~30 s staleness TTL).
66
+ - The frame as the viewport window into its own surface (no children,
67
+ no content, no `zIndex`) + the page frame block (the `frameId?` weak
68
+ reference); the canvas z-order stands the derived dense 1..N mirror
69
+ (`derivedZIndex`, re-derived on load).
70
+ - The complementary reader / validator postures: the strict validator
71
+ (only the known types and marks) and the lenient reader (the unknown
72
+ type → the placeholder fallback, the unknown mark → ignored, the
73
+ unknown attribute → ignored) — the forward-compatibility across the
74
+ 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,169 @@
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 only the root block (an empty
60
+ page — no auto first paragraph; the react-ui editor guarantees the trailing
61
+ editable paragraph). The other entry points:
62
+
63
+ - `fromJSON(json)` — build a `Doc` from a JSON snapshot.
64
+ - `loadDoc(bytes)` — build a `Doc` from an encoded full state (a base
65
+ snapshot; apply diffs with `applyUpdate`).
66
+ - `doc.toJSON()` — the JSON snapshot (`{ v, meta, blocks, children }`).
67
+ - `doc.encode()` / `doc.encodeUpdate(stateVector?)` — the encoded full
68
+ state / the incremental update.
69
+ - `doc.applyUpdate(bytes)` — apply an encoded update (the remote-apply path).
70
+
71
+ `doc.raw` exposes the underlying `Y.Doc`; it is an explicit escape hatch
72
+ and its shape is not guaranteed.
73
+
74
+ ## Commands
75
+
76
+ Editing goes through the semantic API (the only path for normal editing).
77
+ The commands translate into `Y.Doc` mutations inside a transaction:
78
+
79
+ - **Blocks** — `insertBlock`, `updateBlock`, `moveBlock`, `removeBlock`,
80
+ `changeBlockType`.
81
+ - **Text** — `insertText`, `deleteText`, `setMark`, `unsetMark`, and the
82
+ range forms (`deleteTextRange`, `setMarkRange`).
83
+ - **Document meta** — `setTitle`, `setAvatar`, `setGeometry`, `setZOrder`.
84
+ - **Database rows** — the row commands (`insertRow`, `setCell`,
85
+ `removeRow`, `moveRow`, …).
86
+ - **Table rows / columns** — `insertTableRow`, `removeTableRow`,
87
+ `insertTableColumn`, `removeTableColumn`, the move / duplicate / color /
88
+ width commands, and `setTableRowColor`.
89
+
90
+ Reads: `getBlock(id)`, `getChildren(id)`, `getRuns(id)`, `getText(id)`,
91
+ `getTextLength(id)`. History: `undo()` / `redo()` (with `canUndo()` /
92
+ `canRedo()`) — one `transact` is one undo step, and a command batched
93
+ into a `transact` is one step across the batch.
94
+
95
+ ## Sync
96
+
97
+ `attach(doc, provider)` is the glue between a `Doc` and a
98
+ `SyncProvider`: it bootstraps the session (sending the full local state
99
+ first), then routes local transactions out and remote updates in. `detach()`
100
+ on the returned session tears the session down.
101
+
102
+ `WsSyncProvider` speaks the standard y-protocols wire over a WebSocket
103
+ (wire-compatible with a standard y-websocket / pycrdt server):
104
+
105
+ ```ts
106
+ import { WsSyncProvider, attach } from "docblocks-core";
107
+
108
+ const provider = new WsSyncProvider("wss://my-server/ws/room-1", {
109
+ token: "…",
110
+ });
111
+ await provider.connect();
112
+ const session = attach(doc, provider);
113
+ // … on teardown:
114
+ session.detach();
115
+ ```
116
+
117
+ Reactivity: `doc.onChange(handler)` fires for every transaction;
118
+ `doc.onRemoteChange(handler)` fires for non-local-origin transactions only
119
+ (the feed a sync consumer's write-back guards hook into).
120
+
121
+ ## Selection
122
+
123
+ Selection is client / view state, not document data — it is not in the JSON
124
+ and not in document updates. `createSelection(doc)` builds a `SelectionView`
125
+ over the doc; other clients' selections travel only in the ephemeral
126
+ awareness channel (the presence payload). `presenceSelectionOf(selection)`
127
+ projects a selection into the presence payload shape.
128
+
129
+ ## Data format
130
+
131
+ The JSON format is `v: 1`: `meta` (the doc `id`, `title`, the optional
132
+ `avatar`) plus a **flat block map** (`id → block`) with explicit order via
133
+ `children: [blockId]`; nesting (lists, table cells) is `children`. The text
134
+ of a text block is `content: [{ text, marks }]` runs. JSON ↔ Yjs is
135
+ isomorphic — every JSON field has exactly one Yjs image.
136
+
137
+ The v1 registry covers the page types (`paragraph`, `quote`, `heading`,
138
+ `list-item`, `callout`, `code`, `divider`, `image`, `embed`, `table` /
139
+ `table-row` / `table-cell`, `database`), the five canvas types (`surface`,
140
+ `shape`, `connector`, `frame`, `canvas-text`), and `root`; the v1 marks
141
+ are `bold`, `italic`, `strike`, `underline`, `code` (exclusive), `link`,
142
+ `color`, `highlight`, and `mention`. The type lists stand exported
143
+ (`PAGE_BLOCK_TYPES`, `LEAF_BLOCK_TYPES`, `BLOCK_REGISTRY`,
144
+ `MARK_REGISTRY`, `CALLOUT_ICONS`).
145
+
146
+ The block and mark registries are the extension point. Adding a block type
147
+ or mark is additive (a minor): the registry entry plus a renderer; the
148
+ validator and the commands stay registry-driven. The reader tolerates the
149
+ unknown (an unknown block type renders a placeholder, an unknown mark is
150
+ ignored and its text stays); the validator is strict (`validateDoc` reports
151
+ only known types and marks — it protects the data you own). A breaking
152
+ change is a major: the reader rejects the unknown major, and a migration
153
+ helper converts the data. `flattenDoc(json)` normalizes the legacy nested
154
+ shape into the flat page model at load.
155
+
156
+ ## Validation
157
+
158
+ `validateDoc(json, lenient?)` returns the list of validation errors (empty
159
+ is valid). Strict (the default) is the writer's contract — it checks the
160
+ known types and mark values. Lenient is the reader boundary — unknown types
161
+ and marks pass through (the reader preserves them), so a reader of `v1.0`
162
+ can safely load `v1.5` data.
163
+
164
+ ## Non-goals
165
+
166
+ This package is deliberately headless. It does not ship a UI (that is
167
+ `docblocks-react-ui`), a server (any Yjs-compatible server is the receiving
168
+ side), or blob resolution (the consumer provides a `BlobStore`). It holds no
169
+ persistence — snapshot and update storage is a consumer-side concern.