@mseep/affine-mcp-server 2.3.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.
Files changed (43) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +270 -0
  3. package/bin/affine-mcp +5 -0
  4. package/dist/auth.js +61 -0
  5. package/dist/cli.js +726 -0
  6. package/dist/config.js +178 -0
  7. package/dist/edgeless/layout.js +222 -0
  8. package/dist/graphqlClient.js +116 -0
  9. package/dist/httpAuth.js +147 -0
  10. package/dist/httpDiagnostics.js +38 -0
  11. package/dist/index.js +209 -0
  12. package/dist/markdown/parse.js +559 -0
  13. package/dist/markdown/render.js +227 -0
  14. package/dist/markdown/types.js +1 -0
  15. package/dist/oauth.js +154 -0
  16. package/dist/sse.js +261 -0
  17. package/dist/toolSurface.js +349 -0
  18. package/dist/tools/accessTokens.js +45 -0
  19. package/dist/tools/auth.js +18 -0
  20. package/dist/tools/blobStorage.js +136 -0
  21. package/dist/tools/comments.js +104 -0
  22. package/dist/tools/docs.js +7478 -0
  23. package/dist/tools/history.js +22 -0
  24. package/dist/tools/icons.js +125 -0
  25. package/dist/tools/notifications.js +79 -0
  26. package/dist/tools/organize.js +1145 -0
  27. package/dist/tools/properties.js +426 -0
  28. package/dist/tools/user.js +13 -0
  29. package/dist/tools/userCRUD.js +77 -0
  30. package/dist/tools/workspaces.js +322 -0
  31. package/dist/util/explorerIcon.js +95 -0
  32. package/dist/util/mcp.js +28 -0
  33. package/dist/ws.js +113 -0
  34. package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
  35. package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
  36. package/docs/client-setup.md +174 -0
  37. package/docs/configuration-and-deployment.md +265 -0
  38. package/docs/edgeless-canvas-cookbook.md +226 -0
  39. package/docs/getting-started.md +229 -0
  40. package/docs/tool-reference.md +200 -0
  41. package/docs/workflow-recipes.md +147 -0
  42. package/package.json +118 -0
  43. package/tool-manifest.json +99 -0
@@ -0,0 +1,322 @@
1
+ import { z } from "zod";
2
+ import * as Y from "yjs";
3
+ import FormData from "form-data";
4
+ import fetch from "node-fetch";
5
+ import { receipt, text } from "../util/mcp.js";
6
+ import { connectWorkspaceSocket, joinWorkspace, pushDocUpdate, wsUrlFromGraphQLEndpoint } from "../ws.js";
7
+ // Generate AFFiNE-style document ID
8
+ function generateDocId() {
9
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
10
+ let id = '';
11
+ for (let i = 0; i < 10; i++) {
12
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
13
+ }
14
+ return id;
15
+ }
16
+ // Create initial workspace data with a document
17
+ function createInitialWorkspaceData(workspaceName = 'New Workspace', avatar = '') {
18
+ // Create workspace root YDoc
19
+ const rootDoc = new Y.Doc();
20
+ // Set workspace metadata
21
+ const meta = rootDoc.getMap('meta');
22
+ meta.set('name', workspaceName);
23
+ meta.set('avatar', avatar);
24
+ // Create pages array with initial document
25
+ const pages = new Y.Array();
26
+ const firstDocId = generateDocId();
27
+ // Add first document metadata
28
+ const pageMetadata = new Y.Map();
29
+ pageMetadata.set('id', firstDocId);
30
+ pageMetadata.set('title', 'Welcome to ' + workspaceName);
31
+ pageMetadata.set('createDate', Date.now());
32
+ pageMetadata.set('tags', new Y.Array());
33
+ pages.push([pageMetadata]);
34
+ meta.set('pages', pages);
35
+ // Create settings
36
+ const setting = rootDoc.getMap('setting');
37
+ setting.set('collections', new Y.Array());
38
+ // Encode workspace update
39
+ const workspaceUpdate = Y.encodeStateAsUpdate(rootDoc);
40
+ // Create the actual document
41
+ const docYDoc = new Y.Doc();
42
+ const blocks = docYDoc.getMap('blocks');
43
+ // Create page block with proper structure
44
+ const pageId = generateDocId();
45
+ const pageBlock = new Y.Map();
46
+ pageBlock.set('sys:id', pageId);
47
+ pageBlock.set('sys:flavour', 'affine:page');
48
+ // Title as Y.Text
49
+ const titleText = new Y.Text();
50
+ titleText.insert(0, 'Welcome to ' + workspaceName);
51
+ pageBlock.set('prop:title', titleText);
52
+ // Children
53
+ const pageChildren = new Y.Array();
54
+ pageBlock.set('sys:children', pageChildren);
55
+ blocks.set(pageId, pageBlock);
56
+ // Add surface block (required)
57
+ const surfaceId = generateDocId();
58
+ const surfaceBlock = new Y.Map();
59
+ surfaceBlock.set('sys:id', surfaceId);
60
+ surfaceBlock.set('sys:flavour', 'affine:surface');
61
+ surfaceBlock.set('sys:parent', null);
62
+ surfaceBlock.set('sys:children', new Y.Array());
63
+ blocks.set(surfaceId, surfaceBlock);
64
+ pageChildren.push([surfaceId]);
65
+ // Add note block with xywh
66
+ const noteId = generateDocId();
67
+ const noteBlock = new Y.Map();
68
+ noteBlock.set('sys:id', noteId);
69
+ noteBlock.set('sys:flavour', 'affine:note');
70
+ noteBlock.set('sys:parent', null);
71
+ noteBlock.set('prop:displayMode', 'DocAndEdgeless');
72
+ noteBlock.set('prop:xywh', '[0,0,800,600]');
73
+ noteBlock.set('prop:index', 'a0');
74
+ noteBlock.set('prop:lockedBySelf', false);
75
+ const noteChildren = new Y.Array();
76
+ noteBlock.set('sys:children', noteChildren);
77
+ blocks.set(noteId, noteBlock);
78
+ pageChildren.push([noteId]);
79
+ // Add initial paragraph
80
+ const paragraphId = generateDocId();
81
+ const paragraphBlock = new Y.Map();
82
+ paragraphBlock.set('sys:id', paragraphId);
83
+ paragraphBlock.set('sys:flavour', 'affine:paragraph');
84
+ paragraphBlock.set('sys:parent', null);
85
+ paragraphBlock.set('sys:children', new Y.Array());
86
+ paragraphBlock.set('prop:type', 'text');
87
+ const paragraphText = new Y.Text();
88
+ paragraphText.insert(0, 'This workspace was created by AFFiNE MCP Server');
89
+ paragraphBlock.set('prop:text', paragraphText);
90
+ blocks.set(paragraphId, paragraphBlock);
91
+ noteChildren.push([paragraphId]);
92
+ // Set document metadata
93
+ const docMeta = docYDoc.getMap('meta');
94
+ docMeta.set('id', firstDocId);
95
+ docMeta.set('title', 'Welcome to ' + workspaceName);
96
+ docMeta.set('createDate', Date.now());
97
+ docMeta.set('tags', new Y.Array());
98
+ docMeta.set('version', 1);
99
+ // Encode document update
100
+ const docUpdate = Y.encodeStateAsUpdate(docYDoc);
101
+ return {
102
+ workspaceUpdate,
103
+ firstDocId,
104
+ docUpdate
105
+ };
106
+ }
107
+ export function registerWorkspaceTools(server, gql) {
108
+ // LIST WORKSPACES
109
+ const listWorkspacesHandler = async () => {
110
+ try {
111
+ const query = `query { workspaces { id public enableAi createdAt } }`;
112
+ const data = await gql.request(query);
113
+ return text(data.workspaces || []);
114
+ }
115
+ catch (error) {
116
+ return text({ error: error.message });
117
+ }
118
+ };
119
+ server.registerTool("list_workspaces", {
120
+ title: "List Workspaces",
121
+ description: "List all available AFFiNE workspaces"
122
+ }, listWorkspacesHandler);
123
+ // GET WORKSPACE
124
+ const getWorkspaceHandler = async ({ id }) => {
125
+ try {
126
+ const query = `query GetWorkspace($id: String!) {
127
+ workspace(id: $id) {
128
+ id
129
+ public
130
+ enableAi
131
+ createdAt
132
+ permissions {
133
+ Workspace_Read
134
+ Workspace_CreateDoc
135
+ }
136
+ }
137
+ }`;
138
+ const data = await gql.request(query, { id });
139
+ return text(data.workspace);
140
+ }
141
+ catch (error) {
142
+ return text({ error: error.message });
143
+ }
144
+ };
145
+ server.registerTool("get_workspace", {
146
+ title: "Get Workspace",
147
+ description: "Get details of a specific workspace",
148
+ inputSchema: {
149
+ id: z.string().describe("Workspace ID")
150
+ }
151
+ }, getWorkspaceHandler);
152
+ // CREATE WORKSPACE
153
+ const createWorkspaceHandler = async ({ name, avatar }) => {
154
+ try {
155
+ // Get endpoint and headers from GraphQL client
156
+ const endpoint = gql.endpoint;
157
+ const headers = gql.headers;
158
+ const cookie = gql.cookie;
159
+ const bearer = gql.bearer;
160
+ // Create initial workspace data
161
+ const { workspaceUpdate, firstDocId, docUpdate } = createInitialWorkspaceData(name, avatar || '');
162
+ // Only send workspace update - document will be created separately
163
+ const initData = Buffer.from(workspaceUpdate);
164
+ // Create multipart form
165
+ const form = new FormData();
166
+ // Add GraphQL operation
167
+ form.append('operations', JSON.stringify({
168
+ name: 'createWorkspace',
169
+ query: `mutation createWorkspace($init: Upload!) {
170
+ createWorkspace(init: $init) {
171
+ id
172
+ public
173
+ createdAt
174
+ enableAi
175
+ }
176
+ }`,
177
+ variables: { init: null }
178
+ }));
179
+ // Map file to variable
180
+ form.append('map', JSON.stringify({ '0': ['variables.init'] }));
181
+ // Add workspace init data
182
+ form.append('0', initData, {
183
+ filename: 'init.yjs',
184
+ contentType: 'application/octet-stream'
185
+ });
186
+ // Send request
187
+ const response = await fetch(endpoint, {
188
+ method: 'POST',
189
+ headers: {
190
+ ...headers,
191
+ 'Cookie': cookie,
192
+ ...form.getHeaders()
193
+ },
194
+ body: form
195
+ });
196
+ const result = await response.json();
197
+ if (result.errors) {
198
+ throw new Error(result.errors[0].message);
199
+ }
200
+ const workspace = result.data.createWorkspace;
201
+ const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
202
+ const baseUrl = process.env.AFFINE_BASE_URL || endpoint.replace(/\/graphql\/?$/, '');
203
+ try {
204
+ const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
205
+ try {
206
+ await joinWorkspace(socket, workspace.id);
207
+ const docUpdateBase64 = Buffer.from(docUpdate).toString('base64');
208
+ await pushDocUpdate(socket, workspace.id, firstDocId, docUpdateBase64);
209
+ }
210
+ finally {
211
+ socket.disconnect();
212
+ }
213
+ }
214
+ catch (_wsError) {
215
+ // Keep workspace creation successful even if initial websocket sync fails.
216
+ return receipt("workspace.create", {
217
+ workspaceId: workspace.id,
218
+ ...workspace,
219
+ name,
220
+ avatar,
221
+ firstDocId,
222
+ syncStatus: "partial",
223
+ status: "partial",
224
+ message: "Workspace created (document sync may be pending)",
225
+ url: `${baseUrl}/workspace/${workspace.id}`
226
+ });
227
+ }
228
+ return receipt("workspace.create", {
229
+ workspaceId: workspace.id,
230
+ ...workspace,
231
+ name,
232
+ avatar,
233
+ firstDocId,
234
+ syncStatus: "success",
235
+ status: "success",
236
+ message: "Workspace created successfully",
237
+ url: `${baseUrl}/workspace/${workspace.id}`
238
+ });
239
+ }
240
+ catch (error) {
241
+ return text({
242
+ kind: "workspace.create",
243
+ ok: false,
244
+ status: "failed",
245
+ error: error.message,
246
+ });
247
+ }
248
+ };
249
+ server.registerTool("create_workspace", {
250
+ title: "Create Workspace",
251
+ description: "Create a new workspace with initial document (accessible in UI)",
252
+ inputSchema: {
253
+ name: z.string().describe("Workspace name"),
254
+ avatar: z.string().optional().describe("Avatar emoji or URL")
255
+ }
256
+ }, createWorkspaceHandler);
257
+ // UPDATE WORKSPACE
258
+ const updateWorkspaceHandler = async ({ id, public: isPublic, enableAi }) => {
259
+ try {
260
+ const mutation = `
261
+ mutation UpdateWorkspace($input: UpdateWorkspaceInput!) {
262
+ updateWorkspace(input: $input) {
263
+ id
264
+ public
265
+ enableAi
266
+ }
267
+ }
268
+ `;
269
+ const input = { id };
270
+ if (isPublic !== undefined)
271
+ input.public = isPublic;
272
+ if (enableAi !== undefined)
273
+ input.enableAi = enableAi;
274
+ const data = await gql.request(mutation, { input });
275
+ return receipt("workspace.update", {
276
+ workspaceId: id,
277
+ id,
278
+ ...data.updateWorkspace,
279
+ });
280
+ }
281
+ catch (error) {
282
+ return text({ error: error.message });
283
+ }
284
+ };
285
+ server.registerTool("update_workspace", {
286
+ title: "Update Workspace",
287
+ description: "Update workspace settings",
288
+ inputSchema: {
289
+ id: z.string().describe("Workspace ID"),
290
+ public: z.boolean().optional().describe("Make workspace public"),
291
+ enableAi: z.boolean().optional().describe("Enable AI features")
292
+ }
293
+ }, updateWorkspaceHandler);
294
+ // DELETE WORKSPACE
295
+ const deleteWorkspaceHandler = async ({ id }) => {
296
+ try {
297
+ const mutation = `
298
+ mutation DeleteWorkspace($id: String!) {
299
+ deleteWorkspace(id: $id)
300
+ }
301
+ `;
302
+ const data = await gql.request(mutation, { id });
303
+ return receipt("workspace.delete", {
304
+ workspaceId: id,
305
+ id,
306
+ deleted: data.deleteWorkspace,
307
+ success: data.deleteWorkspace,
308
+ message: "Workspace deleted successfully",
309
+ });
310
+ }
311
+ catch (error) {
312
+ return text({ error: error.message });
313
+ }
314
+ };
315
+ server.registerTool("delete_workspace", {
316
+ title: "Delete Workspace",
317
+ description: "Delete a workspace permanently",
318
+ inputSchema: {
319
+ id: z.string().describe("Workspace ID")
320
+ }
321
+ }, deleteWorkspaceHandler);
322
+ }
@@ -0,0 +1,95 @@
1
+ import * as Y from "yjs";
2
+ import { loadDoc, pushDocUpdate } from "../ws.js";
3
+ /** Build the sub-document guid that holds a workspace's explorer icons. */
4
+ export function explorerIconDocId(workspaceId) {
5
+ return `db$${workspaceId}$explorerIcon`;
6
+ }
7
+ /** Build the per-entity map key for a document. */
8
+ export function docIconKey(docId) {
9
+ return `doc:${docId}`;
10
+ }
11
+ /** Build the per-entity map key for an organize folder. */
12
+ export function folderIconKey(folderId) {
13
+ return `folder:${folderId}`;
14
+ }
15
+ /**
16
+ * Coerce user input into the stored icon shape (or null to clear).
17
+ *
18
+ * - A bare string is treated as an emoji shorthand → `{ type: "emoji", unicode }`.
19
+ * - A `{ type: "emoji", unicode }` or `{ type: "icon", name }` object is passed
20
+ * through after validation. Named-icon `name` values are not validated against
21
+ * AFFiNE's fixed icon set — they are written as-is.
22
+ * - `null` clears the icon.
23
+ */
24
+ export function normalizeIconInput(input) {
25
+ if (input === null)
26
+ return null;
27
+ if (typeof input === "string") {
28
+ const unicode = input.trim();
29
+ if (!unicode)
30
+ throw new Error("icon emoji string must not be empty.");
31
+ return { type: "emoji", unicode };
32
+ }
33
+ if (input.type === "emoji") {
34
+ const unicode = input.unicode?.trim();
35
+ if (!unicode)
36
+ throw new Error("emoji icon requires a non-empty `unicode`.");
37
+ return { type: "emoji", unicode };
38
+ }
39
+ if (input.type === "icon") {
40
+ const name = input.name?.trim();
41
+ if (!name)
42
+ throw new Error("named icon requires a non-empty `name`.");
43
+ return { type: "icon", name };
44
+ }
45
+ throw new Error(`Unsupported icon type: ${JSON.stringify(input.type)}.`);
46
+ }
47
+ async function loadExplorerIconDoc(socket, workspaceId) {
48
+ const snap = await loadDoc(socket, workspaceId, explorerIconDocId(workspaceId));
49
+ const doc = new Y.Doc();
50
+ if (snap.missing)
51
+ Y.applyUpdate(doc, Buffer.from(snap.missing, "base64"));
52
+ return doc;
53
+ }
54
+ function readIcon(doc, key) {
55
+ const map = doc.share.has(key) ? doc.getMap(key) : null;
56
+ if (!map || !map.has("icon"))
57
+ return null;
58
+ const raw = map.get("icon");
59
+ if (raw && typeof raw.toJSON === "function") {
60
+ return raw.toJSON();
61
+ }
62
+ return raw ?? null;
63
+ }
64
+ /**
65
+ * Set or clear the sidebar icon for a single explorer entity.
66
+ *
67
+ * Loads the workspace's `explorerIcon` sub-doc, mutates only the target entry,
68
+ * and pushes the minimal delta. Passing `icon = null` removes the `icon` field
69
+ * while preserving the entry's `id`.
70
+ */
71
+ export async function setExplorerIcon(socket, workspaceId, key, icon) {
72
+ const doc = await loadExplorerIconDoc(socket, workspaceId);
73
+ const prevSV = Y.encodeStateVector(doc);
74
+ const map = doc.getMap(key);
75
+ if (!map.has("id"))
76
+ map.set("id", key);
77
+ if (icon === null) {
78
+ if (map.has("icon"))
79
+ map.delete("icon");
80
+ }
81
+ else {
82
+ map.set("icon", icon);
83
+ }
84
+ const delta = Y.encodeStateAsUpdate(doc, prevSV);
85
+ await pushDocUpdate(socket, workspaceId, explorerIconDocId(workspaceId), Buffer.from(delta).toString("base64"));
86
+ return { key, icon };
87
+ }
88
+ /**
89
+ * Read the current sidebar icon for a single explorer entity.
90
+ * Returns `null` when no icon is set (or the entry does not exist).
91
+ */
92
+ export async function getExplorerIcon(socket, workspaceId, key) {
93
+ const doc = await loadExplorerIconDoc(socket, workspaceId);
94
+ return { key, icon: readIcon(doc, key) };
95
+ }
@@ -0,0 +1,28 @@
1
+ function cloneJsonValue(data) {
2
+ if (data === undefined) {
3
+ return data;
4
+ }
5
+ return JSON.parse(JSON.stringify(data));
6
+ }
7
+ export function text(data) {
8
+ if (typeof data === "string") {
9
+ return { content: [{ type: "text", text: data }] };
10
+ }
11
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
12
+ const structuredContent = cloneJsonValue(data);
13
+ return {
14
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
15
+ structuredContent,
16
+ };
17
+ }
18
+ return {
19
+ content: [{ type: "text", text: JSON.stringify(data) }],
20
+ };
21
+ }
22
+ export function receipt(kind, data) {
23
+ return text({
24
+ kind,
25
+ ok: true,
26
+ ...data,
27
+ });
28
+ }
package/dist/ws.js ADDED
@@ -0,0 +1,113 @@
1
+ import { io } from "socket.io-client";
2
+ const DEFAULT_WS_CLIENT_VERSION = process.env.AFFINE_WS_CLIENT_VERSION || '0.26.0';
3
+ const WS_CONNECT_TIMEOUT_MS = Number(process.env.AFFINE_WS_CONNECT_TIMEOUT_MS || 10000);
4
+ const WS_ACK_TIMEOUT_MS = Number(process.env.AFFINE_WS_ACK_TIMEOUT_MS || 10000);
5
+ function ackErrorMessage(ack, fallback) {
6
+ const message = ack?.error?.message;
7
+ if (typeof message === "string" && message.trim())
8
+ return message;
9
+ return ack?.error ? fallback : null;
10
+ }
11
+ function emitWithAck(socket, event, payload, onAck) {
12
+ return new Promise((resolve, reject) => {
13
+ let settled = false;
14
+ const timeout = setTimeout(() => {
15
+ if (settled)
16
+ return;
17
+ settled = true;
18
+ reject(new Error(`${event} timeout after ${WS_ACK_TIMEOUT_MS}ms`));
19
+ }, WS_ACK_TIMEOUT_MS);
20
+ socket.emit(event, payload, (ack) => {
21
+ if (settled)
22
+ return;
23
+ settled = true;
24
+ clearTimeout(timeout);
25
+ try {
26
+ resolve(onAck(ack));
27
+ }
28
+ catch (err) {
29
+ reject(err);
30
+ }
31
+ });
32
+ });
33
+ }
34
+ export function wsUrlFromGraphQLEndpoint(endpoint) {
35
+ return endpoint
36
+ .replace('https://', 'wss://')
37
+ .replace('http://', 'ws://')
38
+ .replace(/\/graphql\/?$/, '');
39
+ }
40
+ export async function connectWorkspaceSocket(wsUrl, cookie, bearer) {
41
+ return new Promise((resolve, reject) => {
42
+ let settled = false;
43
+ const extraHeaders = {};
44
+ if (cookie)
45
+ extraHeaders['Cookie'] = cookie;
46
+ if (bearer)
47
+ extraHeaders['Authorization'] = `Bearer ${bearer}`;
48
+ const socket = io(wsUrl, {
49
+ transports: ['websocket'],
50
+ path: '/socket.io/',
51
+ extraHeaders: Object.keys(extraHeaders).length ? extraHeaders : undefined,
52
+ autoConnect: true
53
+ });
54
+ const timeout = setTimeout(() => {
55
+ if (settled)
56
+ return;
57
+ settled = true;
58
+ cleanup();
59
+ socket.disconnect();
60
+ reject(new Error(`socket connect timeout after ${WS_CONNECT_TIMEOUT_MS}ms`));
61
+ }, WS_CONNECT_TIMEOUT_MS);
62
+ const onError = (err) => {
63
+ if (settled)
64
+ return;
65
+ settled = true;
66
+ cleanup();
67
+ socket.disconnect();
68
+ reject(err);
69
+ };
70
+ const onConnect = () => {
71
+ if (settled)
72
+ return;
73
+ settled = true;
74
+ cleanup();
75
+ resolve(socket);
76
+ };
77
+ const cleanup = () => {
78
+ clearTimeout(timeout);
79
+ socket.off('connect', onConnect);
80
+ socket.off('connect_error', onError);
81
+ };
82
+ socket.on('connect', onConnect);
83
+ socket.on('connect_error', onError);
84
+ });
85
+ }
86
+ export async function joinWorkspace(socket, workspaceId, clientVersion = DEFAULT_WS_CLIENT_VERSION) {
87
+ return emitWithAck(socket, 'space:join', { spaceType: 'workspace', spaceId: workspaceId, clientVersion }, (ack) => {
88
+ const message = ackErrorMessage(ack, "join failed");
89
+ if (message)
90
+ throw new Error(message);
91
+ });
92
+ }
93
+ export async function loadDoc(socket, workspaceId, docId) {
94
+ return emitWithAck(socket, 'space:load-doc', { spaceType: 'workspace', spaceId: workspaceId, docId }, (ack) => {
95
+ if (ack?.error) {
96
+ if (ack.error.name === 'DOC_NOT_FOUND')
97
+ return {};
98
+ throw new Error(ackErrorMessage(ack, "load-doc failed") || "load-doc failed");
99
+ }
100
+ return ack?.data || {};
101
+ });
102
+ }
103
+ export async function pushDocUpdate(socket, workspaceId, docId, updateBase64) {
104
+ return emitWithAck(socket, 'space:push-doc-update', { spaceType: 'workspace', spaceId: workspaceId, docId, update: updateBase64 }, (ack) => {
105
+ const message = ackErrorMessage(ack, "push-doc-update failed");
106
+ if (message)
107
+ throw new Error(message);
108
+ return ack?.data?.timestamp || Date.now();
109
+ });
110
+ }
111
+ export function deleteDoc(socket, workspaceId, docId) {
112
+ socket.emit('space:delete-doc', { spaceType: 'workspace', spaceId: workspaceId, docId });
113
+ }