agentic-documents 0.1.0 → 0.2.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 +36 -0
- package/README.md +57 -4
- package/dist/chunk-FZA5LQV3.js +150 -0
- package/dist/chunk-GYQFTF2E.js +389 -0
- package/dist/chunk-IU3YIQ4Y.js +232 -0
- package/dist/chunk-R5XUTBC3.js +177 -0
- package/dist/{chunk-BSXUNQDR.js → chunk-TFCRPEEM.js} +1 -1
- package/dist/chunk-YBBQNCMC.js +744 -0
- package/dist/cli.js +47 -11
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -8
- package/dist/live-worker.d.ts +20 -0
- package/dist/live-worker.js +27 -0
- package/dist/live.d.ts +73 -0
- package/dist/live.js +12 -0
- package/dist/{mcp-2VZIGHIX.js → mcp-P2C75IIG.js} +4 -2
- package/dist/{service-eYISo6-b.d.ts → service-BKkxEnaJ.d.ts} +24 -2
- package/dist/worker.d.ts +19 -6
- package/dist/worker.js +11 -158
- package/package.json +13 -2
- package/dist/chunk-4YVWD5PH.js +0 -1123
- package/dist/chunk-LRPLNXOF.js +0 -67
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocsService,
|
|
3
|
+
MAX_LIVE_BYTES
|
|
4
|
+
} from "./chunk-FZA5LQV3.js";
|
|
5
|
+
import {
|
|
6
|
+
commands
|
|
7
|
+
} from "./chunk-GYQFTF2E.js";
|
|
8
|
+
import {
|
|
9
|
+
DocError,
|
|
10
|
+
PRINCIPAL_KINDS,
|
|
11
|
+
parseAuthor
|
|
12
|
+
} from "./chunk-YBBQNCMC.js";
|
|
13
|
+
|
|
14
|
+
// src/worker.ts
|
|
15
|
+
import { Hono } from "hono";
|
|
16
|
+
import { assertRegion, parseRegion } from "agentic-identity";
|
|
17
|
+
import { D1_SCHEMA, createD1Store, createR2Blobs } from "agentic-identity/cloudflare";
|
|
18
|
+
var STATUS = {
|
|
19
|
+
NO_MATCH: 404,
|
|
20
|
+
NO_SECTION: 404,
|
|
21
|
+
NO_THREAD: 404,
|
|
22
|
+
NO_SUGGESTION: 404,
|
|
23
|
+
NO_DOCUMENT: 404,
|
|
24
|
+
AMBIGUOUS_MATCH: 409,
|
|
25
|
+
STALE_ANCHOR: 409,
|
|
26
|
+
ALREADY_DECIDED: 409,
|
|
27
|
+
DOCUMENT_EXISTS: 409,
|
|
28
|
+
CONFLICT: 409,
|
|
29
|
+
FORBIDDEN: 403,
|
|
30
|
+
LOCKED: 503,
|
|
31
|
+
MISCONFIGURED: 500
|
|
32
|
+
};
|
|
33
|
+
function same(a, b) {
|
|
34
|
+
let diff = a.length ^ b.length;
|
|
35
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
36
|
+
return diff === 0;
|
|
37
|
+
}
|
|
38
|
+
var MIN_TOKEN_BYTES = 32;
|
|
39
|
+
var warned = /* @__PURE__ */ new Set();
|
|
40
|
+
function refuse(reason) {
|
|
41
|
+
if (!warned.has(reason)) {
|
|
42
|
+
warned.add(reason);
|
|
43
|
+
console.error(`AGENTIC_DOCS_TOKENS is refused: ${reason}`);
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
function identityOf(entry) {
|
|
48
|
+
const object = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : void 0;
|
|
49
|
+
if (object?.role !== void 0 && object.role !== "editor" && object.role !== "viewer") return void 0;
|
|
50
|
+
const role = object?.role ?? "editor";
|
|
51
|
+
const principal = principalOf(entry, object);
|
|
52
|
+
return principal && { principal, role };
|
|
53
|
+
}
|
|
54
|
+
function principalOf(entry, object) {
|
|
55
|
+
const who = typeof entry === "string" ? entry : object?.as;
|
|
56
|
+
const author = typeof who === "string" ? parseAuthor(who) : void 0;
|
|
57
|
+
if (!author || !PRINCIPAL_KINDS.includes(author.kind) || !author.name) return void 0;
|
|
58
|
+
if (object?.scopes === void 0) return author;
|
|
59
|
+
const scopes = object.scopes;
|
|
60
|
+
return Array.isArray(scopes) && scopes.every((s) => typeof s === "string" && s !== "") ? { ...author, scopes } : void 0;
|
|
61
|
+
}
|
|
62
|
+
function readTokens(raw) {
|
|
63
|
+
let table;
|
|
64
|
+
try {
|
|
65
|
+
table = JSON.parse(raw ?? "{}");
|
|
66
|
+
} catch {
|
|
67
|
+
return refuse("not valid JSON");
|
|
68
|
+
}
|
|
69
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) return refuse("not a JSON object");
|
|
70
|
+
const found = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const [token, entry] of Object.entries(table)) {
|
|
72
|
+
if (new TextEncoder().encode(token).length < MIN_TOKEN_BYTES) return refuse(`a token is shorter than ${MIN_TOKEN_BYTES} bytes`);
|
|
73
|
+
const identity = identityOf(entry);
|
|
74
|
+
if (!identity) return refuse("an entry is not a valid kind:name or { as, scopes, role }");
|
|
75
|
+
found.set(token, identity);
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
function identityFor(tokens, bearer) {
|
|
80
|
+
let found;
|
|
81
|
+
for (const [token, identity] of readTokens(tokens) ?? []) if (same(token, bearer)) found = identity;
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
var fail = (code, message, status, matches = []) => Response.json({ error: { code, message, matches } }, { status });
|
|
85
|
+
async function readCapped(request, cap) {
|
|
86
|
+
const tooBig = () => new DocError("INVALID_INPUT", `the update is over ${cap} bytes`);
|
|
87
|
+
if (Number(request.headers.get("content-length") ?? 0) > cap) throw tooBig();
|
|
88
|
+
const chunks = [];
|
|
89
|
+
let size = 0;
|
|
90
|
+
const reader = request.body?.getReader();
|
|
91
|
+
for (let part = await reader?.read(); part && !part.done; part = await reader?.read()) {
|
|
92
|
+
size += part.value.length;
|
|
93
|
+
if (size > cap) {
|
|
94
|
+
await reader?.cancel();
|
|
95
|
+
throw tooBig();
|
|
96
|
+
}
|
|
97
|
+
chunks.push(part.value);
|
|
98
|
+
}
|
|
99
|
+
const whole = new Uint8Array(size);
|
|
100
|
+
let at = 0;
|
|
101
|
+
for (const chunk of chunks) {
|
|
102
|
+
whole.set(chunk, at);
|
|
103
|
+
at += chunk.length;
|
|
104
|
+
}
|
|
105
|
+
return whole;
|
|
106
|
+
}
|
|
107
|
+
var DOC_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
108
|
+
function createApp(source, options = {}) {
|
|
109
|
+
const app2 = new Hono();
|
|
110
|
+
app2.use("*", async (c, next) => {
|
|
111
|
+
await next();
|
|
112
|
+
c.header("x-content-type-options", "nosniff");
|
|
113
|
+
});
|
|
114
|
+
app2.get("/health", (c) => c.json({ ok: true, pack: "agentic-documents" }));
|
|
115
|
+
app2.use("/api/v1/*", async (c, next) => {
|
|
116
|
+
const bearer = /^Bearer (.+)$/.exec(c.req.header("authorization") ?? "")?.[1];
|
|
117
|
+
const identity = bearer === void 0 ? void 0 : identityFor(c.env?.AGENTIC_DOCS_TOKENS, bearer);
|
|
118
|
+
if (!identity) return fail("UNAUTHENTICATED", "send a valid bearer token", 401);
|
|
119
|
+
c.set("author", identity.principal);
|
|
120
|
+
c.set("role", identity.role);
|
|
121
|
+
await next();
|
|
122
|
+
});
|
|
123
|
+
app2.get("/api/v1/me", (c) => {
|
|
124
|
+
const { kind, name, scopes } = c.get("author");
|
|
125
|
+
return c.json({ kind, name, scopes: scopes ?? [], role: c.get("role") });
|
|
126
|
+
});
|
|
127
|
+
app2.get("/api/v1/docs/:id/room", async (c) => {
|
|
128
|
+
if (!options.live) return fail("NOT_FOUND", "this server hosts no live rooms", 404);
|
|
129
|
+
if (!DOC_ID.test(c.req.param("id"))) return fail("INVALID_INPUT", `invalid document id "${c.req.param("id")}"`, 400);
|
|
130
|
+
const { contributors } = await options.live(c.env ?? {}).meta(c.req.param("id"));
|
|
131
|
+
return c.json({ contributors: contributors.map(({ id, kind, machine }) => ({ name: id, kind, machine })) });
|
|
132
|
+
});
|
|
133
|
+
app2.get("/api/v1/docs/:id/ws", (c) => {
|
|
134
|
+
const id = c.req.param("id");
|
|
135
|
+
if (!options.live) return fail("NOT_FOUND", "this server hosts no live rooms", 404);
|
|
136
|
+
if (c.req.header("upgrade") !== "websocket") return fail("UPGRADE_REQUIRED", "open this route as a WebSocket", 426);
|
|
137
|
+
if (!DOC_ID.test(id)) return fail("INVALID_INPUT", `invalid document id "${id}"`, 400);
|
|
138
|
+
const { kind, name } = c.get("author");
|
|
139
|
+
return options.live(c.env ?? {}).connect(id, c.req.raw, { principal: { id: name, kind }, role: c.get("role"), name });
|
|
140
|
+
});
|
|
141
|
+
async function run(c, command, input) {
|
|
142
|
+
try {
|
|
143
|
+
if (!command.readOnly && c.get("role") === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
144
|
+
const result = await command.run(await (typeof source === "function" ? source(c.env ?? {}) : source), input, c.get("author"));
|
|
145
|
+
if (command.name === "export") {
|
|
146
|
+
const { data, contentType, encoding, format } = result;
|
|
147
|
+
const images = input.remoteImages === true || input.remoteImages === "true" ? "data: https:" : "data:";
|
|
148
|
+
const disposition = format === "odt" ? { "content-disposition": `attachment; filename="${input.id}.odt"` } : {};
|
|
149
|
+
const headers = { ...disposition, "content-type": contentType, "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; img-src ${images}; sandbox` };
|
|
150
|
+
return encoding === "base64" ? c.body(Uint8Array.from(atob(data), (ch) => ch.charCodeAt(0)), 200, headers) : c.body(data, 200, headers);
|
|
151
|
+
}
|
|
152
|
+
if (typeof result === "string") return c.body(result, 200, { "content-type": "text/markdown; charset=utf-8" });
|
|
153
|
+
return c.json(result);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!(error instanceof DocError)) throw error;
|
|
156
|
+
return fail(error.code, error.message, STATUS[error.code] ?? 400, error.matches);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function body(c) {
|
|
160
|
+
const parsed = await c.req.json().catch(() => void 0);
|
|
161
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
162
|
+
throw new DocError("INVALID_INPUT", "the body must be a JSON object");
|
|
163
|
+
}
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
const serviceOf = (c) => typeof source === "function" ? source(c.env ?? {}) : Promise.resolve(source);
|
|
167
|
+
app2.get("/api/v1/docs/:id/state", async (c) => {
|
|
168
|
+
const state = await (await serviceOf(c)).state(c.req.param("id"));
|
|
169
|
+
return c.body(new Uint8Array(state), 200, { "content-type": "application/octet-stream" });
|
|
170
|
+
});
|
|
171
|
+
app2.post("/api/v1/docs/:id/checkpoint", async (c) => {
|
|
172
|
+
if (c.get("role") === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
173
|
+
const update = await readCapped(c.req.raw, MAX_LIVE_BYTES);
|
|
174
|
+
await (await serviceOf(c)).checkpoint(c.req.param("id"), update, c.get("author"));
|
|
175
|
+
return c.json({ ok: true });
|
|
176
|
+
});
|
|
177
|
+
const create = commands.find((c) => c.name === "new");
|
|
178
|
+
app2.post("/api/v1/docs", async (c) => run(c, create, await body(c)));
|
|
179
|
+
for (const command of commands.filter((c) => c !== create)) {
|
|
180
|
+
const path = `/api/v1/docs/:id/${command.name}`;
|
|
181
|
+
if (command.readOnly) app2.get(path, (c) => run(c, command, { ...c.req.query(), id: c.req.param("id") }));
|
|
182
|
+
else app2.post(path, async (c) => run(c, command, { ...await body(c), id: c.req.param("id") }));
|
|
183
|
+
}
|
|
184
|
+
app2.notFound(() => fail("NOT_FOUND", "no such route", 404));
|
|
185
|
+
app2.onError((error) => {
|
|
186
|
+
if (error instanceof DocError) return fail(error.code, error.message, STATUS[error.code] ?? 400, error.matches);
|
|
187
|
+
console.error(error instanceof Error ? error.name : "error");
|
|
188
|
+
return fail("INTERNAL", "the server could not finish the request", 500);
|
|
189
|
+
});
|
|
190
|
+
return app2;
|
|
191
|
+
}
|
|
192
|
+
var schemas = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
var urlSecret;
|
|
194
|
+
function regionOf(env) {
|
|
195
|
+
let region;
|
|
196
|
+
try {
|
|
197
|
+
region = parseRegion(env.AGENTIC_DOCS_REGION);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new DocError("MISCONFIGURED", 'set AGENTIC_DOCS_REGION to "eu" or "us" ("unknown" only for local development)');
|
|
200
|
+
}
|
|
201
|
+
if (region === "unknown" && env.AGENTIC_DOCS_UNSAFE_REGION !== "1") {
|
|
202
|
+
throw new DocError("MISCONFIGURED", 'the region "unknown" turns the residency check off; set AGENTIC_DOCS_UNSAFE_REGION=1 to accept that for local development');
|
|
203
|
+
}
|
|
204
|
+
return region;
|
|
205
|
+
}
|
|
206
|
+
async function serviceFor(env) {
|
|
207
|
+
if (!env.DB || !env.DOCS) throw new DocError("MISCONFIGURED", "bind a D1 database as DB and an R2 bucket as DOCS");
|
|
208
|
+
const region = regionOf(env);
|
|
209
|
+
const db = env.DB;
|
|
210
|
+
let ready = schemas.get(db);
|
|
211
|
+
if (!ready) {
|
|
212
|
+
ready = db.prepare(D1_SCHEMA).run().catch((error) => {
|
|
213
|
+
schemas.delete(db);
|
|
214
|
+
throw error;
|
|
215
|
+
});
|
|
216
|
+
schemas.set(db, ready);
|
|
217
|
+
}
|
|
218
|
+
await ready;
|
|
219
|
+
const store = createD1Store(db, { region });
|
|
220
|
+
const blobs = createR2Blobs(env.DOCS, { region, urlBase: "https://invalid.example/unused", urlSecret: urlSecret ??= `${crypto.randomUUID()}${crypto.randomUUID()}` });
|
|
221
|
+
assertRegion(region, store, blobs);
|
|
222
|
+
return new DocsService({ store, blobs });
|
|
223
|
+
}
|
|
224
|
+
var app = createApp(serviceFor);
|
|
225
|
+
var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
|
|
226
|
+
|
|
227
|
+
export {
|
|
228
|
+
createApp,
|
|
229
|
+
regionOf,
|
|
230
|
+
serviceFor,
|
|
231
|
+
worker_default
|
|
232
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_LIVE_BYTES
|
|
3
|
+
} from "./chunk-FZA5LQV3.js";
|
|
4
|
+
import {
|
|
5
|
+
AgenticDoc,
|
|
6
|
+
DOC_ERROR_CODES,
|
|
7
|
+
DocError,
|
|
8
|
+
PRINCIPAL_KINDS
|
|
9
|
+
} from "./chunk-YBBQNCMC.js";
|
|
10
|
+
|
|
11
|
+
// src/live.ts
|
|
12
|
+
import * as Y from "yjs";
|
|
13
|
+
import { SyncClient } from "agentic-realtime/client";
|
|
14
|
+
async function refusal(response, fallback) {
|
|
15
|
+
const { error } = await response.json().catch(() => ({}));
|
|
16
|
+
const code = DOC_ERROR_CODES.find((known) => known === error?.code) ?? (response.status >= 500 ? "MISCONFIGURED" : "INVALID_INPUT");
|
|
17
|
+
return new DocError(code, typeof error?.message === "string" ? error.message : `${fallback} (${response.status})`);
|
|
18
|
+
}
|
|
19
|
+
var base = (server) => server.replace(/\/$/, "");
|
|
20
|
+
async function whoAmI(server, token) {
|
|
21
|
+
const response = await fetch(`${base(server)}/api/v1/me`, { headers: { authorization: `Bearer ${token}` } }).catch((error) => {
|
|
22
|
+
throw unreachable(error);
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
if (response.status === 401 || response.status === 403) throw new DocError("FORBIDDEN", `the server refused the token (${response.status})`);
|
|
26
|
+
throw await refusal(response, "the server could not say who the token is");
|
|
27
|
+
}
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
const { kind, name, scopes, role } = body;
|
|
30
|
+
if (!PRINCIPAL_KINDS.includes(kind) || typeof name !== "string" || role !== "editor" && role !== "viewer") {
|
|
31
|
+
throw new DocError("INVALID_INPUT", "the server sent an identity this client does not know");
|
|
32
|
+
}
|
|
33
|
+
const granted = Array.isArray(scopes) ? scopes.filter((s) => typeof s === "string") : [];
|
|
34
|
+
return { author: { kind, name, ...granted.length ? { scopes: granted } : {} }, role };
|
|
35
|
+
}
|
|
36
|
+
var LiveDoc = class _LiveDoc {
|
|
37
|
+
constructor(doc, me, client, api, refusedAtStart) {
|
|
38
|
+
this.doc = doc;
|
|
39
|
+
this.me = me;
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.api = api;
|
|
42
|
+
this.refusedAtStart = refusedAtStart;
|
|
43
|
+
}
|
|
44
|
+
doc;
|
|
45
|
+
me;
|
|
46
|
+
client;
|
|
47
|
+
api;
|
|
48
|
+
refusedAtStart;
|
|
49
|
+
static async join(options) {
|
|
50
|
+
const me = await whoAmI(options.server, options.token);
|
|
51
|
+
const url = new URL(`${base(options.server)}/api/v1/docs/${encodeURIComponent(options.id)}/ws`);
|
|
52
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
53
|
+
const client = await SyncClient.connect({
|
|
54
|
+
url: url.toString(),
|
|
55
|
+
headers: { authorization: `Bearer ${options.token}` },
|
|
56
|
+
presence: { name: options.name ?? me.author.name },
|
|
57
|
+
reconnect: options.reconnect
|
|
58
|
+
});
|
|
59
|
+
const api = (path, init = {}) => fetch(`${base(options.server)}/api/v1/docs/${encodeURIComponent(options.id)}${path}`, {
|
|
60
|
+
...init,
|
|
61
|
+
headers: { ...init.headers, authorization: `Bearer ${options.token}` }
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
if (options.seed !== false) {
|
|
65
|
+
const stored = await api("/state");
|
|
66
|
+
if (stored.ok) {
|
|
67
|
+
const state = new Uint8Array(await stored.arrayBuffer());
|
|
68
|
+
if (state.length > MAX_LIVE_BYTES) throw new DocError("INVALID_INPUT", `the stored document is over ${MAX_LIVE_BYTES} bytes, the most a live room takes`);
|
|
69
|
+
Y.applyUpdate(client.doc, state);
|
|
70
|
+
} else if (stored.status !== 404) throw await refusal(stored, "could not read the stored document");
|
|
71
|
+
await client.flush();
|
|
72
|
+
if (me.role === "editor" && client.denied.length > 0) throw new DocError("FORBIDDEN", `the room refused the stored document: ${client.denied[0]}`);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await client.disconnect();
|
|
76
|
+
throw error instanceof DocError ? error : new DocError("INVALID_INPUT", `could not open the room: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
return new _LiveDoc(new AgenticDoc(client.doc, options.doc), me, client, api, client.denied.length);
|
|
79
|
+
}
|
|
80
|
+
/** Saves the room's state to the stored document, through the Blobs port. A viewer cannot save. */
|
|
81
|
+
async save() {
|
|
82
|
+
if (this.me.role === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
83
|
+
await this.client.flush();
|
|
84
|
+
const saved = await this.api("/checkpoint", { method: "POST", headers: { "content-type": "application/octet-stream" }, body: new Uint8Array(this.doc.encode()) });
|
|
85
|
+
if (!saved.ok) throw await refusal(saved, "the server did not save the document");
|
|
86
|
+
}
|
|
87
|
+
/** The other people and agents in the room. `machine` is true for an agent. */
|
|
88
|
+
get peers() {
|
|
89
|
+
return this.client.peers;
|
|
90
|
+
}
|
|
91
|
+
/** Why the room dropped an update, in order. */
|
|
92
|
+
get denied() {
|
|
93
|
+
return this.client.denied.slice(this.refusedAtStart);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Runs one change as the token's author and waits until the room has it. A viewer fails before the change.
|
|
97
|
+
* After a refusal from the room the local copy still holds the change. Leave and join again.
|
|
98
|
+
*/
|
|
99
|
+
async edit(run) {
|
|
100
|
+
if (this.me.role === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
101
|
+
const seen = this.denied.length;
|
|
102
|
+
const result = run(this.doc, this.me.author);
|
|
103
|
+
await this.client.flush();
|
|
104
|
+
if (this.denied.length > seen) throw new DocError("FORBIDDEN", `the room refused the edit: ${this.denied[seen]}`);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/** Waits until the room has every change made so far. */
|
|
108
|
+
flush() {
|
|
109
|
+
return this.client.flush();
|
|
110
|
+
}
|
|
111
|
+
async leave() {
|
|
112
|
+
await this.client.disconnect();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function unreachable(error) {
|
|
116
|
+
if (error instanceof DocError) return error;
|
|
117
|
+
return new DocError("MISCONFIGURED", `could not reach the live server: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
var LiveService = class {
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.options = options;
|
|
122
|
+
}
|
|
123
|
+
options;
|
|
124
|
+
async join(id) {
|
|
125
|
+
try {
|
|
126
|
+
return await LiveDoc.join({ ...this.options, id });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw unreachable(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** The room is only a copy, so a new document is made in the store. A room seeds from it on the next join. */
|
|
132
|
+
async create(id, markdown) {
|
|
133
|
+
const response = await fetch(`${base(this.options.server)}/api/v1/docs`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { authorization: `Bearer ${this.options.token}`, "content-type": "application/json" },
|
|
136
|
+
body: JSON.stringify({ id, markdown })
|
|
137
|
+
}).catch((error) => {
|
|
138
|
+
throw unreachable(error);
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) throw await refusal(response, "the server refused the request");
|
|
141
|
+
}
|
|
142
|
+
async read(id, run) {
|
|
143
|
+
const live = await this.join(id);
|
|
144
|
+
try {
|
|
145
|
+
return run(this.exists(live, id));
|
|
146
|
+
} finally {
|
|
147
|
+
await live.leave();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async write(id, run) {
|
|
151
|
+
const live = await this.join(id);
|
|
152
|
+
try {
|
|
153
|
+
this.exists(live, id);
|
|
154
|
+
const result = await live.edit((doc) => run(doc));
|
|
155
|
+
try {
|
|
156
|
+
await live.save();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
159
|
+
throw new DocError(error instanceof DocError ? error.code : "MISCONFIGURED", `the room has the change, but it is not saved: ${why}. Do not repeat the change.`);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
} finally {
|
|
163
|
+
await live.leave();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// A document that nobody made has an empty room and an empty log.
|
|
167
|
+
exists(live, id) {
|
|
168
|
+
if (live.doc.edits().length === 0) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
169
|
+
return live.doc;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
whoAmI,
|
|
175
|
+
LiveDoc,
|
|
176
|
+
LiveService
|
|
177
|
+
};
|