@kyneta/yjs-schema 1.2.0 → 1.3.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/README.md +3 -3
- package/dist/index.d.ts +82 -170
- package/dist/index.js +176 -260
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/bind-constraints.test.ts +5 -13
- package/src/__tests__/bind-yjs.test.ts +57 -46
- package/src/__tests__/create.test.ts +80 -56
- package/src/__tests__/reader.test.ts +3 -14
- package/src/__tests__/record-text-spike.test.ts +38 -36
- package/src/__tests__/substrate.test.ts +47 -40
- package/src/bind-yjs.ts +9 -40
- package/src/change-mapping.ts +7 -2
- package/src/index.ts +24 -26
- package/src/native-map.ts +37 -0
- package/src/populate.ts +1 -1
- package/src/substrate.ts +19 -4
- package/src/version.ts +14 -67
- package/src/yjs-resolve.ts +1 -1
- package/src/create.ts +0 -177
- package/src/sync.ts +0 -107
package/src/index.ts
CHANGED
|
@@ -3,51 +3,49 @@
|
|
|
3
3
|
// Provides a Substrate<YjsVersion> implementation that wraps a Y.Doc
|
|
4
4
|
// with schema-aware typed reads, writes, versioning, and export/import.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Low-level primitives (power users):
|
|
11
|
-
// createYjsSubstrate, yjsSubstrateFactory, yjsReader,
|
|
12
|
-
// resolveYjsType, stepIntoYjs, applyChangeToYjs, eventsToOps, YjsVersion
|
|
6
|
+
// The single entry point is `createDoc(yjs.bind(schema))`. For the
|
|
7
|
+
// batteries-included API, import from this package. For the composable
|
|
8
|
+
// toolkit, import from `@kyneta/schema` directly.
|
|
13
9
|
|
|
14
10
|
// ---------------------------------------------------------------------------
|
|
15
|
-
//
|
|
11
|
+
// Generic API (re-exported from @kyneta/schema for convenience)
|
|
16
12
|
// ---------------------------------------------------------------------------
|
|
17
13
|
|
|
14
|
+
// Types (re-exported for convenience)
|
|
15
|
+
export type { Changeset } from "@kyneta/changefeed"
|
|
16
|
+
export type { DocRef, Op, Ref, SubstratePayload } from "@kyneta/schema"
|
|
17
|
+
// Construction
|
|
18
18
|
// Mutation & observation (re-exported from @kyneta/schema for convenience)
|
|
19
19
|
// Schema definition (re-exported for convenience)
|
|
20
|
+
// Native escape hatch
|
|
21
|
+
// Sync primitives (generic — work for any substrate)
|
|
20
22
|
export {
|
|
21
23
|
applyChanges,
|
|
22
24
|
change,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
subscribeNode,
|
|
26
|
-
} from "@kyneta/schema"
|
|
27
|
-
// Construction
|
|
28
|
-
export { createYjsDoc, createYjsDocFromEntirety } from "./create.js"
|
|
29
|
-
// Sync primitives (Yjs-specific)
|
|
30
|
-
export {
|
|
25
|
+
createDoc,
|
|
26
|
+
createRef,
|
|
31
27
|
exportEntirety,
|
|
32
28
|
exportSince,
|
|
33
29
|
merge,
|
|
30
|
+
NATIVE,
|
|
31
|
+
Schema,
|
|
32
|
+
subscribe,
|
|
33
|
+
subscribeNode,
|
|
34
|
+
unwrap,
|
|
34
35
|
version,
|
|
35
|
-
} from "
|
|
36
|
-
|
|
37
|
-
// Types (re-exported for convenience)
|
|
38
|
-
export type { Changeset } from "@kyneta/changefeed"
|
|
39
|
-
export type { Op, Ref, SubstratePayload } from "@kyneta/schema"
|
|
36
|
+
} from "@kyneta/schema"
|
|
40
37
|
|
|
41
38
|
// ---------------------------------------------------------------------------
|
|
42
|
-
//
|
|
39
|
+
// Yjs-specific exports
|
|
43
40
|
// ---------------------------------------------------------------------------
|
|
44
41
|
|
|
45
|
-
// Namespace — the yjs substrate namespace (replaces standalone escape hatch;
|
|
46
|
-
// the old `yjs(ref)` call is now `yjs.unwrap(ref)`)
|
|
47
|
-
export { yjs } from "./bind-yjs.js"
|
|
48
42
|
export type { YjsCaps } from "./bind-yjs.js"
|
|
43
|
+
// Namespace
|
|
44
|
+
export { yjs } from "./bind-yjs.js"
|
|
49
45
|
// Change mapping
|
|
50
46
|
export { applyChangeToYjs, eventsToOps } from "./change-mapping.js"
|
|
47
|
+
// NativeMap — the Yjs functor
|
|
48
|
+
export type { YjsNativeMap } from "./native-map.js"
|
|
51
49
|
// Container creation
|
|
52
50
|
export { ensureContainers } from "./populate.js"
|
|
53
51
|
// Reader
|
|
@@ -61,4 +59,4 @@ export {
|
|
|
61
59
|
// Version
|
|
62
60
|
export { YjsVersion } from "./version.js"
|
|
63
61
|
// Container resolution
|
|
64
|
-
export { resolveYjsType, stepIntoYjs } from "./yjs-resolve.js"
|
|
62
|
+
export { resolveYjsType, stepIntoYjs } from "./yjs-resolve.js"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// native-map — Yjs NativeMap functor.
|
|
2
|
+
//
|
|
3
|
+
// Maps schema kinds to Yjs shared types. Used as the `N`
|
|
4
|
+
// type parameter in `SchemaRef<S, M, N>` for Yjs-backed documents.
|
|
5
|
+
|
|
6
|
+
import type { NativeMap } from "@kyneta/schema"
|
|
7
|
+
import type * as Y from "yjs"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NativeMap for the Yjs CRDT substrate.
|
|
11
|
+
*
|
|
12
|
+
* Maps each schema kind to the corresponding Yjs shared type:
|
|
13
|
+
* - `root → Y.Doc` (the document itself)
|
|
14
|
+
* - `text → Y.Text`
|
|
15
|
+
* - `counter → undefined` (Yjs has no counter type)
|
|
16
|
+
* - `list → Y.Array<unknown>`
|
|
17
|
+
* - `movableList → undefined` (Yjs has no movable list)
|
|
18
|
+
* - `struct → Y.Map<unknown>` (Yjs uses maps for struct fields)
|
|
19
|
+
* - `map → Y.Map<unknown>`
|
|
20
|
+
* - `tree → undefined` (Yjs has no tree type)
|
|
21
|
+
* - `set → undefined` (not yet supported)
|
|
22
|
+
* - `scalar → undefined` (no container; stored in parent map)
|
|
23
|
+
* - `sum → undefined` (no container; stored in parent map)
|
|
24
|
+
*/
|
|
25
|
+
export interface YjsNativeMap extends NativeMap {
|
|
26
|
+
readonly root: Y.Doc
|
|
27
|
+
readonly text: Y.Text
|
|
28
|
+
readonly counter: undefined
|
|
29
|
+
readonly list: Y.Array<unknown>
|
|
30
|
+
readonly movableList: undefined
|
|
31
|
+
readonly struct: Y.Map<unknown>
|
|
32
|
+
readonly map: Y.Map<unknown>
|
|
33
|
+
readonly tree: undefined
|
|
34
|
+
readonly set: undefined
|
|
35
|
+
readonly scalar: undefined
|
|
36
|
+
readonly sum: undefined
|
|
37
|
+
}
|
package/src/populate.ts
CHANGED
package/src/substrate.ts
CHANGED
|
@@ -22,12 +22,18 @@ import type {
|
|
|
22
22
|
SubstratePayload,
|
|
23
23
|
WritableContext,
|
|
24
24
|
} from "@kyneta/schema"
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
BACKING_DOC,
|
|
27
|
+
buildWritableContext,
|
|
28
|
+
executeBatch,
|
|
29
|
+
KIND,
|
|
30
|
+
} from "@kyneta/schema"
|
|
26
31
|
import * as Y from "yjs"
|
|
27
32
|
import { applyChangeToYjs, eventsToOps } from "./change-mapping.js"
|
|
28
33
|
import { ensureContainers } from "./populate.js"
|
|
29
34
|
import { yjsReader } from "./reader.js"
|
|
30
35
|
import { YjsVersion } from "./version.js"
|
|
36
|
+
import { resolveYjsType } from "./yjs-resolve.js"
|
|
31
37
|
|
|
32
38
|
// ---------------------------------------------------------------------------
|
|
33
39
|
// Origin tag — used to suppress echo from our own transactions
|
|
@@ -124,6 +130,17 @@ export function createYjsSubstrate(
|
|
|
124
130
|
context(): WritableContext {
|
|
125
131
|
if (!cachedCtx) {
|
|
126
132
|
cachedCtx = buildWritableContext(substrate)
|
|
133
|
+
// Attach nativeResolver — used by interpretImpl to set [NATIVE]
|
|
134
|
+
// on every ref. The resolver maps schema positions to Yjs shared types.
|
|
135
|
+
;(cachedCtx as any).nativeResolver = (
|
|
136
|
+
nodeSchema: SchemaNode,
|
|
137
|
+
path: { segments: readonly unknown[] },
|
|
138
|
+
) => {
|
|
139
|
+
if (path.segments.length === 0) return doc
|
|
140
|
+
if (nodeSchema[KIND] === "scalar" || nodeSchema[KIND] === "sum")
|
|
141
|
+
return undefined
|
|
142
|
+
return resolveYjsType(rootMap, schema, path as any)
|
|
143
|
+
}
|
|
127
144
|
}
|
|
128
145
|
return cachedCtx
|
|
129
146
|
},
|
|
@@ -252,9 +269,7 @@ export function createYjsSubstrate(
|
|
|
252
269
|
*/
|
|
253
270
|
export function createYjsReplica(doc: Y.Doc): Replica<YjsVersion> {
|
|
254
271
|
let currentDoc = doc
|
|
255
|
-
let currentBase: YjsVersion = new YjsVersion(
|
|
256
|
-
Y.encodeStateVector(new Y.Doc()),
|
|
257
|
-
)
|
|
272
|
+
let currentBase: YjsVersion = new YjsVersion(Y.encodeStateVector(new Y.Doc()))
|
|
258
273
|
|
|
259
274
|
return {
|
|
260
275
|
get [BACKING_DOC]() {
|
package/src/version.ts
CHANGED
|
@@ -12,30 +12,14 @@
|
|
|
12
12
|
// decoded `Map<number, number>` (clientID → clock) maps ourselves.
|
|
13
13
|
|
|
14
14
|
import type { Version } from "@kyneta/schema"
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
base64ToUint8Array,
|
|
17
|
+
uint8ArrayToBase64,
|
|
18
|
+
versionVectorCompare,
|
|
19
|
+
versionVectorMeet,
|
|
20
|
+
} from "@kyneta/schema"
|
|
16
21
|
import { decodeStateVector } from "yjs"
|
|
17
22
|
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Base64 helpers (platform-agnostic, no Node.js Buffer dependency)
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
23
|
-
let binary = ""
|
|
24
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
25
|
-
binary += String.fromCharCode(bytes[i]!)
|
|
26
|
-
}
|
|
27
|
-
return btoa(binary)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function base64ToUint8Array(base64: string): Uint8Array {
|
|
31
|
-
const binary = atob(base64)
|
|
32
|
-
const bytes = new Uint8Array(binary.length)
|
|
33
|
-
for (let i = 0; i < binary.length; i++) {
|
|
34
|
-
bytes[i] = binary.charCodeAt(i)
|
|
35
|
-
}
|
|
36
|
-
return bytes
|
|
37
|
-
}
|
|
38
|
-
|
|
39
23
|
// ---------------------------------------------------------------------------
|
|
40
24
|
// State vector encoding — manual varint (unsigned LEB128)
|
|
41
25
|
// ---------------------------------------------------------------------------
|
|
@@ -105,54 +89,19 @@ export class YjsVersion implements Version {
|
|
|
105
89
|
/**
|
|
106
90
|
* Compare with another version using version-vector partial order.
|
|
107
91
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
92
|
+
* Delegates to the shared `versionVectorCompare` utility after decoding
|
|
93
|
+
* both state vectors via `Y.decodeStateVector()`.
|
|
110
94
|
*
|
|
111
|
-
*
|
|
112
|
-
* - For each client, compare clocks (missing client = clock 0).
|
|
113
|
-
* - If all clocks in `this` ≤ `other` and at least one strictly less → `"behind"`
|
|
114
|
-
* - If all clocks in `this` ≥ `other` and at least one strictly greater → `"ahead"`
|
|
115
|
-
* - If all clocks equal → `"equal"`
|
|
116
|
-
* - Otherwise → `"concurrent"`
|
|
117
|
-
*
|
|
118
|
-
* Throws if `other` is not a `YjsVersion`.
|
|
95
|
+
* @throws If `other` is not a `YjsVersion`.
|
|
119
96
|
*/
|
|
120
97
|
compare(other: Version): "behind" | "equal" | "ahead" | "concurrent" {
|
|
121
98
|
if (!(other instanceof YjsVersion)) {
|
|
122
99
|
throw new Error("YjsVersion can only be compared with another YjsVersion")
|
|
123
100
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Collect the union of all client IDs
|
|
129
|
-
const allClients = new Set<number>()
|
|
130
|
-
for (const id of thisMap.keys()) allClients.add(id)
|
|
131
|
-
for (const id of otherMap.keys()) allClients.add(id)
|
|
132
|
-
|
|
133
|
-
let hasLess = false
|
|
134
|
-
let hasGreater = false
|
|
135
|
-
|
|
136
|
-
for (const clientId of allClients) {
|
|
137
|
-
const thisClock = thisMap.get(clientId) ?? 0
|
|
138
|
-
const otherClock = otherMap.get(clientId) ?? 0
|
|
139
|
-
|
|
140
|
-
if (thisClock < otherClock) {
|
|
141
|
-
hasLess = true
|
|
142
|
-
}
|
|
143
|
-
if (thisClock > otherClock) {
|
|
144
|
-
hasGreater = true
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Early exit: if we've seen both less and greater, it's concurrent
|
|
148
|
-
if (hasLess && hasGreater) {
|
|
149
|
-
return "concurrent"
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (hasLess && !hasGreater) return "behind"
|
|
154
|
-
if (hasGreater && !hasLess) return "ahead"
|
|
155
|
-
return "equal"
|
|
101
|
+
return versionVectorCompare(
|
|
102
|
+
decodeStateVector(this.sv),
|
|
103
|
+
decodeStateVector(other.sv),
|
|
104
|
+
)
|
|
156
105
|
}
|
|
157
106
|
|
|
158
107
|
/**
|
|
@@ -166,9 +115,7 @@ export class YjsVersion implements Version {
|
|
|
166
115
|
*/
|
|
167
116
|
meet(other: Version): YjsVersion {
|
|
168
117
|
if (!(other instanceof YjsVersion)) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
"YjsVersion can only be meet'd with another YjsVersion",
|
|
171
|
-
)
|
|
118
|
+
throw new Error("YjsVersion can only be meet'd with another YjsVersion")
|
|
172
119
|
}
|
|
173
120
|
const thisMap = decodeStateVector(this.sv)
|
|
174
121
|
const otherMap = decodeStateVector(other.sv)
|
package/src/yjs-resolve.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// captures all mutations with correct relative paths.
|
|
16
16
|
|
|
17
17
|
import type { Path, Schema as SchemaNode, Segment } from "@kyneta/schema"
|
|
18
|
-
import { advanceSchema
|
|
18
|
+
import { advanceSchema } from "@kyneta/schema"
|
|
19
19
|
import * as Y from "yjs"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
package/src/create.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// create — batteries-included document construction backed by YjsSubstrate.
|
|
2
|
-
//
|
|
3
|
-
// Provides `createYjsDoc` and `createYjsDocFromEntirety` functions that
|
|
4
|
-
// hide the interpret pipeline and layer composition behind a single call.
|
|
5
|
-
//
|
|
6
|
-
// Internally tracks substrates via a module-scoped WeakMap so that sync
|
|
7
|
-
// primitives (`version`, `exportEntirety`, `merge` in sync.ts)
|
|
8
|
-
// can retrieve the substrate from just a doc ref.
|
|
9
|
-
//
|
|
10
|
-
// `getSubstrate` is exported for use by `sync.ts` but is NOT re-exported
|
|
11
|
-
// from the barrel (`index.ts`). It is an internal cross-module helper.
|
|
12
|
-
//
|
|
13
|
-
// Two forms for `createYjsDoc`:
|
|
14
|
-
// createYjsDoc(schema, yjsDoc) — "bring your own doc" (wrap existing)
|
|
15
|
-
// createYjsDoc(schema) — create a fresh empty Y.Doc
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
Ref,
|
|
19
|
-
Schema as SchemaType,
|
|
20
|
-
Substrate,
|
|
21
|
-
SubstratePayload,
|
|
22
|
-
} from "@kyneta/schema"
|
|
23
|
-
import {
|
|
24
|
-
interpret,
|
|
25
|
-
observation,
|
|
26
|
-
readable,
|
|
27
|
-
registerSubstrate,
|
|
28
|
-
writable,
|
|
29
|
-
} from "@kyneta/schema"
|
|
30
|
-
import type * as Y from "yjs"
|
|
31
|
-
import { createYjsSubstrate, yjsSubstrateFactory } from "./substrate.js"
|
|
32
|
-
import type { YjsVersion } from "./version.js"
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Substrate tracking (module-scoped)
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
const substrates = new WeakMap<object, Substrate<YjsVersion>>()
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Retrieve the substrate associated with a doc created by `createYjsDoc`
|
|
42
|
-
* or `createYjsDocFromEntirety`.
|
|
43
|
-
*
|
|
44
|
-
* Exported for `sync.ts` — NOT re-exported from the barrel.
|
|
45
|
-
*
|
|
46
|
-
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
47
|
-
*/
|
|
48
|
-
export function getSubstrate(doc: object): Substrate<YjsVersion> {
|
|
49
|
-
const s = substrates.get(doc)
|
|
50
|
-
if (!s) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
"version/exportEntirety/merge called on an object without a YjsSubstrate. " +
|
|
53
|
-
"Use a doc created by createYjsDoc() or createYjsDocFromEntirety().",
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
return s
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// registerDoc — internal helper (interpret + WeakMap registration)
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
function registerDoc(
|
|
64
|
-
schema: SchemaType,
|
|
65
|
-
substrate: Substrate<YjsVersion>,
|
|
66
|
-
): any {
|
|
67
|
-
// The `as any` on the builder avoids TS2589 — interpret's fluent API
|
|
68
|
-
// produces deeply recursive types when S is the abstract SchemaType.
|
|
69
|
-
// The public createYjsDoc/createYjsDocFromEntirety signatures provide
|
|
70
|
-
// the correct Ref<S> return type via interface call signature patterns.
|
|
71
|
-
const doc: any = (interpret as any)(schema, substrate.context())
|
|
72
|
-
.with(readable)
|
|
73
|
-
.with(writable)
|
|
74
|
-
.with(observation)
|
|
75
|
-
.done()
|
|
76
|
-
substrates.set(doc, substrate)
|
|
77
|
-
// Also register in the general unwrap() registry so that the
|
|
78
|
-
// yjs() escape hatch can discover the substrate from the ref.
|
|
79
|
-
registerSubstrate(doc, substrate)
|
|
80
|
-
return doc
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// isYDoc — runtime check for Y.Doc
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
function isYDoc(value: unknown): value is Y.Doc {
|
|
88
|
-
return (
|
|
89
|
-
value !== null &&
|
|
90
|
-
value !== undefined &&
|
|
91
|
-
typeof value === "object" &&
|
|
92
|
-
"getMap" in value &&
|
|
93
|
-
"getText" in value &&
|
|
94
|
-
"getArray" in value &&
|
|
95
|
-
"transact" in value &&
|
|
96
|
-
typeof (value as any).transact === "function" &&
|
|
97
|
-
// Y.Doc has clientID; distinguish from other objects
|
|
98
|
-
"clientID" in value &&
|
|
99
|
-
typeof (value as any).clientID === "number"
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// createYjsDoc
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
// Interface call signature avoids TS2589 on Ref<S> when S is generic.
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Create a live Yjs-backed document.
|
|
111
|
-
*
|
|
112
|
-
* **Form 1 — bring your own doc:**
|
|
113
|
-
* ```ts
|
|
114
|
-
* const yjsDoc = new Y.Doc()
|
|
115
|
-
* const doc = createYjsDoc(mySchema, yjsDoc)
|
|
116
|
-
* ```
|
|
117
|
-
*
|
|
118
|
-
* **Form 2 — fresh empty doc:**
|
|
119
|
-
* ```ts
|
|
120
|
-
* const doc = createYjsDoc(mySchema)
|
|
121
|
-
*
|
|
122
|
-
* // Apply initial content via change():
|
|
123
|
-
* change(doc, d => {
|
|
124
|
-
* d.title.insert(0, "Hello")
|
|
125
|
-
* d.items.push({ name: "First item" })
|
|
126
|
-
* })
|
|
127
|
-
* ```
|
|
128
|
-
*
|
|
129
|
-
* Returns a full-stack `Ref<S>` — callable, navigable, writable,
|
|
130
|
-
* transactable, and observable. Backed by a `YjsSubstrate` with
|
|
131
|
-
* CRDT collaboration support.
|
|
132
|
-
*
|
|
133
|
-
* The returned ref observes **all** mutations to the underlying Y.Doc,
|
|
134
|
-
* regardless of source (local kyneta writes, merge, external
|
|
135
|
-
* `Y.applyUpdate()`, external raw Yjs API mutations).
|
|
136
|
-
*
|
|
137
|
-
* @param schema - The schema describing the document structure.
|
|
138
|
-
* @param doc - Optional `Y.Doc` instance to wrap. If omitted, a fresh
|
|
139
|
-
* empty Y.Doc is created with containers matching the schema.
|
|
140
|
-
*/
|
|
141
|
-
type CreateYjsDoc = <S extends SchemaType>(schema: S, doc?: Y.Doc) => Ref<S>
|
|
142
|
-
|
|
143
|
-
export const createYjsDoc: CreateYjsDoc = (schema, doc) => {
|
|
144
|
-
if (doc !== undefined && isYDoc(doc)) {
|
|
145
|
-
// Bring your own doc — wrap the existing Y.Doc
|
|
146
|
-
return registerDoc(schema, createYjsSubstrate(doc, schema))
|
|
147
|
-
}
|
|
148
|
-
// Fresh empty doc
|
|
149
|
-
return registerDoc(schema, yjsSubstrateFactory.create(schema))
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// createYjsDocFromEntirety
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
type CreateYjsDocFromEntirety = <S extends SchemaType>(
|
|
157
|
-
schema: S,
|
|
158
|
-
payload: SubstratePayload,
|
|
159
|
-
) => Ref<S>
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Reconstruct a live Yjs-backed document from a substrate entirety payload.
|
|
163
|
-
*
|
|
164
|
-
* The payload must have been produced by `exportEntirety()` on a
|
|
165
|
-
* compatible document. This is the entry point for SSR hydration
|
|
166
|
-
* and reconnection past log compaction.
|
|
167
|
-
*
|
|
168
|
-
* ```ts
|
|
169
|
-
* const payload = exportEntirety(docA)
|
|
170
|
-
* const docB = createYjsDocFromEntirety(MySchema, payload)
|
|
171
|
-
* // docB has the same state as docA at the time of export
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
export const createYjsDocFromEntirety: CreateYjsDocFromEntirety = (
|
|
175
|
-
schema,
|
|
176
|
-
payload,
|
|
177
|
-
) => registerDoc(schema, yjsSubstrateFactory.fromEntirety(payload, schema))
|
package/src/sync.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// sync — sync primitives for YjsSubstrate-backed documents.
|
|
2
|
-
//
|
|
3
|
-
// These functions provide version tracking, entirety export, and merge
|
|
4
|
-
// for documents created via `createYjsDoc` or
|
|
5
|
-
// `createYjsDocFromEntirety`. They discover the substrate via the
|
|
6
|
-
// module-scoped WeakMap in `create.ts`.
|
|
7
|
-
//
|
|
8
|
-
// Unlike PlainSubstrate's sync (which returns Op[] for deltas),
|
|
9
|
-
// YjsSubstrate's sync uses binary SubstratePayload for both entireties
|
|
10
|
-
// and deltas — these are Yjs's native state-as-update bytes.
|
|
11
|
-
|
|
12
|
-
import type { SubstratePayload } from "@kyneta/schema"
|
|
13
|
-
import { getSubstrate } from "./create.js"
|
|
14
|
-
import type { YjsVersion } from "./version.js"
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// version — current YjsVersion
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Current version as a `YjsVersion` (wrapping a Yjs state vector).
|
|
22
|
-
*
|
|
23
|
-
* Use `.serialize()` to get a text-safe string for embedding in HTML
|
|
24
|
-
* meta tags, URL parameters, etc.
|
|
25
|
-
*
|
|
26
|
-
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
27
|
-
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
28
|
-
*/
|
|
29
|
-
export function version(doc: object): YjsVersion {
|
|
30
|
-
return getSubstrate(doc).version()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// exportEntirety — full state for reconstruction
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Export the full substrate entirety — sufficient for a new peer to
|
|
39
|
-
* reconstruct an equivalent document via `createYjsDocFromEntirety()`.
|
|
40
|
-
*
|
|
41
|
-
* Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
|
|
42
|
-
*
|
|
43
|
-
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
44
|
-
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
45
|
-
*/
|
|
46
|
-
export function exportEntirety(doc: object): SubstratePayload {
|
|
47
|
-
return getSubstrate(doc).exportEntirety()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// exportSince — delta since a version
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Export a delta payload containing all changes since the given version.
|
|
56
|
-
*
|
|
57
|
-
* Returns a binary `SubstratePayload` (Yjs update bytes), or `null`
|
|
58
|
-
* if the delta cannot be computed.
|
|
59
|
-
*
|
|
60
|
-
* ```ts
|
|
61
|
-
* const v0 = version(docA)
|
|
62
|
-
* change(docA, d => d.title.insert(0, "Hi"))
|
|
63
|
-
* const delta = exportSince(docA, v0)
|
|
64
|
-
* merge(docB, delta!)
|
|
65
|
-
* ```
|
|
66
|
-
*
|
|
67
|
-
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
68
|
-
* @param since - The version to diff from.
|
|
69
|
-
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
70
|
-
*/
|
|
71
|
-
export function exportSince(
|
|
72
|
-
doc: object,
|
|
73
|
-
since: YjsVersion,
|
|
74
|
-
): SubstratePayload | null {
|
|
75
|
-
return getSubstrate(doc).exportSince(since)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// merge — apply a delta from another peer
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Import a delta payload into a live document.
|
|
84
|
-
*
|
|
85
|
-
* The payload must have been produced by `exportSince()` or
|
|
86
|
-
* `exportEntirety()` on a compatible document.
|
|
87
|
-
*
|
|
88
|
-
* After import, the changefeed fires for all subscribers — the event
|
|
89
|
-
* bridge handles this automatically.
|
|
90
|
-
*
|
|
91
|
-
* ```ts
|
|
92
|
-
* const delta = exportSince(docA, sinceVersion)
|
|
93
|
-
* merge(docB, delta!, "sync")
|
|
94
|
-
* ```
|
|
95
|
-
*
|
|
96
|
-
* @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
|
|
97
|
-
* @param payload - The delta or entirety payload to merge.
|
|
98
|
-
* @param origin - Optional provenance tag for the changeset.
|
|
99
|
-
* @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
|
|
100
|
-
*/
|
|
101
|
-
export function merge(
|
|
102
|
-
doc: object,
|
|
103
|
-
payload: SubstratePayload,
|
|
104
|
-
origin?: string,
|
|
105
|
-
): void {
|
|
106
|
-
getSubstrate(doc).merge(payload, origin)
|
|
107
|
-
}
|