@mengine/medeo-client 1.0.1-alpha.2 → 1.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/dist/{document-DgffKwRw.js → document-C98vSu7J.js} +186 -149
- package/dist/{index-DRUGsbm2.d.ts → index-BUKA3L7o.d.ts} +766 -112
- package/dist/index.d.ts +543 -76
- package/dist/index.js +1310 -145
- package/dist/loro-relay-doc-ssdYpuef.js +58 -0
- package/dist/relay.d.ts +37 -0
- package/dist/relay.js +2 -0
- package/dist/testing.d.ts +16 -1
- package/dist/testing.js +58 -6
- package/package.json +5 -7
- package/dist/index-6e5cbdM3.d.ts +0 -475
- package/dist/schemas.d.ts +0 -2
- package/dist/schemas.js +0 -378
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { LoroDoc } from "loro-crdt";
|
|
2
|
+
//#region src/relay/loro-relay-doc.ts
|
|
3
|
+
/**
|
|
4
|
+
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
5
|
+
* *host* needs, as opposed to an editing client.
|
|
6
|
+
*
|
|
7
|
+
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
8
|
+
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
9
|
+
* and exports diffs by version vector.
|
|
10
|
+
*
|
|
11
|
+
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
12
|
+
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
13
|
+
* both storage implementations) and the in-process test double
|
|
14
|
+
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
15
|
+
* double with its own verdict logic drifts from the server it stands in for, and
|
|
16
|
+
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
17
|
+
* the classification is shared code, not duplicated code.
|
|
18
|
+
*/
|
|
19
|
+
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
20
|
+
function newRelayDoc() {
|
|
21
|
+
const doc = new LoroDoc();
|
|
22
|
+
doc.detach();
|
|
23
|
+
return doc;
|
|
24
|
+
}
|
|
25
|
+
/** Load a detached relay doc from a stored snapshot. */
|
|
26
|
+
function relayDocFromSnapshot(snapshot) {
|
|
27
|
+
const doc = newRelayDoc();
|
|
28
|
+
doc.import(snapshot);
|
|
29
|
+
return doc;
|
|
30
|
+
}
|
|
31
|
+
/** A full-oplog clone (via snapshot round-trip) for non-destructive probing. */
|
|
32
|
+
function cloneRelayDoc(doc) {
|
|
33
|
+
return relayDocFromSnapshot(doc.export({ mode: "snapshot" }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Classify an incoming update against `doc` without mutating it.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
39
|
+
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
40
|
+
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
41
|
+
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
42
|
+
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
43
|
+
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
44
|
+
*/
|
|
45
|
+
function classifyUpdate(doc, update) {
|
|
46
|
+
const probe = cloneRelayDoc(doc);
|
|
47
|
+
let status;
|
|
48
|
+
try {
|
|
49
|
+
status = probe.import(update);
|
|
50
|
+
} catch {
|
|
51
|
+
return "corrupt_update";
|
|
52
|
+
}
|
|
53
|
+
if (status?.pending != null) return "missing_dependency";
|
|
54
|
+
if (probe.cmpWithFrontiers(doc.oplogFrontiers()) !== 1) return "duplicate";
|
|
55
|
+
return "accepted";
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { newRelayDoc as n, relayDocFromSnapshot as r, classifyUpdate as t };
|
package/dist/relay.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LoroDoc } from "loro-crdt";
|
|
2
|
+
|
|
3
|
+
//#region src/relay/loro-relay-doc.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
6
|
+
* *host* needs, as opposed to an editing client.
|
|
7
|
+
*
|
|
8
|
+
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
9
|
+
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
10
|
+
* and exports diffs by version vector.
|
|
11
|
+
*
|
|
12
|
+
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
13
|
+
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
14
|
+
* both storage implementations) and the in-process test double
|
|
15
|
+
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
16
|
+
* double with its own verdict logic drifts from the server it stands in for, and
|
|
17
|
+
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
18
|
+
* the classification is shared code, not duplicated code.
|
|
19
|
+
*/
|
|
20
|
+
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
21
|
+
declare function newRelayDoc(): LoroDoc;
|
|
22
|
+
/** Load a detached relay doc from a stored snapshot. */
|
|
23
|
+
declare function relayDocFromSnapshot(snapshot: Uint8Array): LoroDoc;
|
|
24
|
+
type ImportVerdict = 'accepted' | 'duplicate' | 'corrupt_update' | 'missing_dependency';
|
|
25
|
+
/**
|
|
26
|
+
* Classify an incoming update against `doc` without mutating it.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
29
|
+
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
30
|
+
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
31
|
+
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
32
|
+
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
33
|
+
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
34
|
+
*/
|
|
35
|
+
declare function classifyUpdate(doc: LoroDoc, update: Uint8Array): ImportVerdict;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type ImportVerdict, classifyUpdate, newRelayDoc, relayDocFromSnapshot };
|
package/dist/relay.js
ADDED
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as MirrorVideoDocumentAdapter, tt as VideoDraft } from "./index-BUKA3L7o.js";
|
|
2
2
|
|
|
3
3
|
//#region src/testing/in-memory-mengine-server.d.ts
|
|
4
4
|
/**
|
|
@@ -29,6 +29,21 @@ declare class InMemoryMengineServer {
|
|
|
29
29
|
private handleSnapshot;
|
|
30
30
|
private handleBootstrap;
|
|
31
31
|
private handleSync;
|
|
32
|
+
/**
|
|
33
|
+
* Push one update, classifying it exactly as the real server does.
|
|
34
|
+
*
|
|
35
|
+
* This shares `classifyUpdate` with `apps/mengine-server` rather than assuming
|
|
36
|
+
* success, because the four push verdicts are the contract clients are written
|
|
37
|
+
* against: a double that always acks makes `duplicate` / `rejected` untestable
|
|
38
|
+
* above the storage layer, so an ack-waiter or a doc handle could mishandle
|
|
39
|
+
* them and still show green. It also mirrors the real server's two subtler
|
|
40
|
+
* behaviors — a non-accepted update is NOT imported (orphan bytes stay out of
|
|
41
|
+
* the log), and `duplicate` / `rejected` report the version from *before* the
|
|
42
|
+
* push, since nothing was appended.
|
|
43
|
+
*
|
|
44
|
+
* Status codes match the wire contract: `missing_dependency` is 409 (retryable
|
|
45
|
+
* after catch-up), `corrupt_update` is 422 (never retryable).
|
|
46
|
+
*/
|
|
32
47
|
private handlePush;
|
|
33
48
|
private handleEvents;
|
|
34
49
|
}
|
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { L as base64ToBytes, M as VIDEO_DOCUMENT_SCHEMA_VERSION, R as bytesToBase64, r as createMirrorVideoDocumentAdapter } from "./document-C98vSu7J.js";
|
|
2
|
+
import { t as classifyUpdate } from "./loro-relay-doc-ssdYpuef.js";
|
|
2
3
|
import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
|
|
3
4
|
//#region src/testing/in-memory-mengine-server.ts
|
|
4
5
|
/**
|
|
@@ -35,10 +36,6 @@ var InMemoryMengineServer = class {
|
|
|
35
36
|
if (path.startsWith("sync") && method === "GET") return this.handleSync(url);
|
|
36
37
|
if (path === "updates" && method === "POST") return this.handlePush(init);
|
|
37
38
|
if (path === "events" && method === "GET") return this.handleEvents(init);
|
|
38
|
-
if (path === "audit" && method === "GET") return Response.json({ entries: this.updates.map((u) => ({
|
|
39
|
-
...u.meta,
|
|
40
|
-
update_seq: u.updateSeq
|
|
41
|
-
})) });
|
|
42
39
|
return new Response("not found", { status: 404 });
|
|
43
40
|
};
|
|
44
41
|
parsePath(url) {
|
|
@@ -85,6 +82,21 @@ var InMemoryMengineServer = class {
|
|
|
85
82
|
server_vv: bytesToBase64(this.doc.oplogVersion().encode())
|
|
86
83
|
});
|
|
87
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Push one update, classifying it exactly as the real server does.
|
|
87
|
+
*
|
|
88
|
+
* This shares `classifyUpdate` with `apps/mengine-server` rather than assuming
|
|
89
|
+
* success, because the four push verdicts are the contract clients are written
|
|
90
|
+
* against: a double that always acks makes `duplicate` / `rejected` untestable
|
|
91
|
+
* above the storage layer, so an ack-waiter or a doc handle could mishandle
|
|
92
|
+
* them and still show green. It also mirrors the real server's two subtler
|
|
93
|
+
* behaviors — a non-accepted update is NOT imported (orphan bytes stay out of
|
|
94
|
+
* the log), and `duplicate` / `rejected` report the version from *before* the
|
|
95
|
+
* push, since nothing was appended.
|
|
96
|
+
*
|
|
97
|
+
* Status codes match the wire contract: `missing_dependency` is 409 (retryable
|
|
98
|
+
* after catch-up), `corrupt_update` is 422 (never retryable).
|
|
99
|
+
*/
|
|
88
100
|
async handlePush(init) {
|
|
89
101
|
const body = JSON.parse(typeof init?.body === "string" ? init.body : "{}");
|
|
90
102
|
if (body.update == null) return Response.json({
|
|
@@ -94,6 +106,25 @@ var InMemoryMengineServer = class {
|
|
|
94
106
|
}, { status: 400 });
|
|
95
107
|
const data = base64ToBytes(body.update);
|
|
96
108
|
const baseVV = this.doc.oplogVersion();
|
|
109
|
+
const serverVersion = this.version();
|
|
110
|
+
const verdict = classifyUpdate(this.doc, data);
|
|
111
|
+
if (verdict === "corrupt_update") return Response.json({
|
|
112
|
+
kind: "rejected",
|
|
113
|
+
code: "corrupt_update",
|
|
114
|
+
message: "update failed to decode",
|
|
115
|
+
server_version: serverVersion
|
|
116
|
+
}, { status: 422 });
|
|
117
|
+
if (verdict === "missing_dependency") return Response.json({
|
|
118
|
+
kind: "rejected",
|
|
119
|
+
code: "missing_dependency",
|
|
120
|
+
message: "update depends on operations that are not in the server log",
|
|
121
|
+
server_version: serverVersion
|
|
122
|
+
}, { status: 409 });
|
|
123
|
+
if (verdict === "duplicate") return Response.json({
|
|
124
|
+
kind: "duplicate",
|
|
125
|
+
update_seq: null,
|
|
126
|
+
version: serverVersion
|
|
127
|
+
});
|
|
97
128
|
this.doc.import(data);
|
|
98
129
|
this.hasSnapshot = true;
|
|
99
130
|
this.seq += 1;
|
|
@@ -161,6 +192,7 @@ function extractMeta(doc, baseVV) {
|
|
|
161
192
|
semantic_op: null,
|
|
162
193
|
payload: null,
|
|
163
194
|
intent: null,
|
|
195
|
+
actor: null,
|
|
164
196
|
message: null,
|
|
165
197
|
parse_error: false,
|
|
166
198
|
peer: "0",
|
|
@@ -171,11 +203,12 @@ function extractMeta(doc, baseVV) {
|
|
|
171
203
|
};
|
|
172
204
|
const [counterPart, peerPart] = change.id.split("@");
|
|
173
205
|
const message = change.msg ?? change.message ?? null;
|
|
174
|
-
const { semantic_op, payload, intent, parse_error } = parseMessage(message);
|
|
206
|
+
const { semantic_op, payload, intent, actor, parse_error } = parseMessage(message);
|
|
175
207
|
return {
|
|
176
208
|
semantic_op,
|
|
177
209
|
payload,
|
|
178
210
|
intent,
|
|
211
|
+
actor,
|
|
179
212
|
message,
|
|
180
213
|
parse_error,
|
|
181
214
|
peer: peers[Number(peerPart)] ?? peerPart ?? "0",
|
|
@@ -185,11 +218,17 @@ function extractMeta(doc, baseVV) {
|
|
|
185
218
|
frontiers
|
|
186
219
|
};
|
|
187
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Mirrors `apps/mengine-server`'s `parseMessage` / `parseActor`. Kept in step
|
|
223
|
+
* deliberately: this double is what harness tests push against, so a divergence
|
|
224
|
+
* here would let them assert metadata the real server never produces.
|
|
225
|
+
*/
|
|
188
226
|
function parseMessage(message) {
|
|
189
227
|
if (message == null || message === "") return {
|
|
190
228
|
semantic_op: null,
|
|
191
229
|
payload: null,
|
|
192
230
|
intent: null,
|
|
231
|
+
actor: null,
|
|
193
232
|
parse_error: false
|
|
194
233
|
};
|
|
195
234
|
try {
|
|
@@ -198,12 +237,14 @@ function parseMessage(message) {
|
|
|
198
237
|
semantic_op: null,
|
|
199
238
|
payload: null,
|
|
200
239
|
intent: null,
|
|
240
|
+
actor: null,
|
|
201
241
|
parse_error: true
|
|
202
242
|
};
|
|
203
243
|
return {
|
|
204
244
|
semantic_op: typeof parsed.semantic_op === "string" ? parsed.semantic_op : null,
|
|
205
245
|
payload: parsed.payload ?? null,
|
|
206
246
|
intent: typeof parsed.intent === "string" ? parsed.intent : null,
|
|
247
|
+
actor: parseActor(parsed.actor),
|
|
207
248
|
parse_error: false
|
|
208
249
|
};
|
|
209
250
|
} catch {
|
|
@@ -211,10 +252,21 @@ function parseMessage(message) {
|
|
|
211
252
|
semantic_op: null,
|
|
212
253
|
payload: null,
|
|
213
254
|
intent: null,
|
|
255
|
+
actor: null,
|
|
214
256
|
parse_error: true
|
|
215
257
|
};
|
|
216
258
|
}
|
|
217
259
|
}
|
|
260
|
+
/** A malformed `actor` yields null for the author alone, never `parse_error`. */
|
|
261
|
+
function parseActor(actor) {
|
|
262
|
+
if (actor == null || typeof actor !== "object") return null;
|
|
263
|
+
const { user_id: userId, role } = actor;
|
|
264
|
+
if (typeof userId !== "string" || userId === "" || typeof role !== "string" || role === "") return null;
|
|
265
|
+
return {
|
|
266
|
+
user_id: userId,
|
|
267
|
+
role
|
|
268
|
+
};
|
|
269
|
+
}
|
|
218
270
|
//#endregion
|
|
219
271
|
//#region src/testing/index.ts
|
|
220
272
|
const ACTOR_TEST = "1001";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mengine/medeo-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./dist/index.js",
|
|
17
|
-
"./
|
|
17
|
+
"./relay": "./dist/relay.js",
|
|
18
18
|
"./testing": "./dist/testing.js",
|
|
19
19
|
"./package.json": "./package.json"
|
|
20
20
|
},
|
|
@@ -23,18 +23,16 @@
|
|
|
23
23
|
"registry": "https://registry.npmjs.org/"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"immer": "^10.2.0",
|
|
27
26
|
"loro-mirror": "^2.2.0",
|
|
28
27
|
"zod": "^4.4.3",
|
|
29
|
-
"@mengine/storage": "1.
|
|
30
|
-
"@mengine/
|
|
31
|
-
"@mengine/
|
|
28
|
+
"@mengine/storage": "1.1.0",
|
|
29
|
+
"@mengine/sync": "1.1.0",
|
|
30
|
+
"@mengine/utils": "1.1.0"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
33
|
"@types/node": "^25.9.1",
|
|
35
34
|
"@typescript/native-preview": "7.0.0-dev.20260521.1",
|
|
36
35
|
"loro-crdt": "^1.13.6",
|
|
37
|
-
"tsx": "^4.22.3",
|
|
38
36
|
"typescript": "^6.0.3",
|
|
39
37
|
"vite-plugin-wasm": "^3.6.0",
|
|
40
38
|
"vite-plus": "^0.1.23",
|