@adonis-agora/collaboration 0.1.4 → 0.2.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 +24 -0
- package/README.md +172 -0
- package/dist/commands/collaboration_init.d.ts +26 -0
- package/dist/commands/collaboration_init.d.ts.map +1 -0
- package/dist/commands/collaboration_init.js +83 -0
- package/dist/commands/collaboration_init.js.map +1 -0
- package/dist/commands/commands.json +1 -0
- package/dist/commands/main.d.ts +16 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +20 -0
- package/dist/commands/main.js.map +1 -0
- package/dist/commands/make_collab_document.d.ts +23 -0
- package/dist/commands/make_collab_document.d.ts.map +1 -0
- package/dist/commands/make_collab_document.js +75 -0
- package/dist/commands/make_collab_document.js.map +1 -0
- package/dist/configure.d.ts +3 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +6 -1
- package/dist/configure.js.map +1 -1
- package/dist/providers/collaboration_provider.d.ts +6 -2
- package/dist/providers/collaboration_provider.d.ts.map +1 -1
- package/dist/providers/collaboration_provider.js +36 -17
- package/dist/providers/collaboration_provider.js.map +1 -1
- package/dist/src/auth/token.d.ts +59 -0
- package/dist/src/auth/token.d.ts.map +1 -0
- package/dist/src/auth/token.js +91 -0
- package/dist/src/auth/token.js.map +1 -0
- package/dist/src/codegen/documents_registry.d.ts +28 -0
- package/dist/src/codegen/documents_registry.d.ts.map +1 -0
- package/dist/src/codegen/documents_registry.js +50 -0
- package/dist/src/codegen/documents_registry.js.map +1 -0
- package/dist/src/codegen_init.d.ts +3 -0
- package/dist/src/codegen_init.d.ts.map +1 -0
- package/dist/src/codegen_init.js +45 -0
- package/dist/src/codegen_init.js.map +1 -0
- package/dist/src/collaboration_manager.d.ts +20 -11
- package/dist/src/collaboration_manager.d.ts.map +1 -1
- package/dist/src/collaboration_manager.js +46 -17
- package/dist/src/collaboration_manager.js.map +1 -1
- package/dist/src/define_config.d.ts +25 -6
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +2 -2
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/driver.d.ts +26 -32
- package/dist/src/driver.d.ts.map +1 -1
- package/dist/src/drivers/automerge/automerge_driver.d.ts +7 -12
- package/dist/src/drivers/automerge/automerge_driver.d.ts.map +1 -1
- package/dist/src/drivers/automerge/automerge_driver.js +37 -75
- package/dist/src/drivers/automerge/automerge_driver.js.map +1 -1
- package/dist/src/drivers/partykit/partykit_driver.d.ts +54 -0
- package/dist/src/drivers/partykit/partykit_driver.d.ts.map +1 -0
- package/dist/src/drivers/partykit/partykit_driver.js +162 -0
- package/dist/src/drivers/partykit/partykit_driver.js.map +1 -0
- package/dist/src/drivers/shared.d.ts +17 -0
- package/dist/src/drivers/shared.d.ts.map +1 -0
- package/dist/src/drivers/shared.js +46 -0
- package/dist/src/drivers/shared.js.map +1 -0
- package/dist/src/drivers/yjs/yjs_driver.d.ts +18 -12
- package/dist/src/drivers/yjs/yjs_driver.d.ts.map +1 -1
- package/dist/src/drivers/yjs/yjs_driver.js +50 -94
- package/dist/src/drivers/yjs/yjs_driver.js.map +1 -1
- package/dist/src/http/types.d.ts +79 -0
- package/dist/src/http/types.d.ts.map +1 -0
- package/dist/src/http/types.js +8 -0
- package/dist/src/http/types.js.map +1 -0
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes.d.ts +29 -0
- package/dist/src/routes.d.ts.map +1 -0
- package/dist/src/routes.js +189 -0
- package/dist/src/routes.js.map +1 -0
- package/dist/src/services/booted_app.js +1 -1
- package/dist/src/services/booted_app.js.map +1 -1
- package/dist/src/services/comment_service.d.ts +5 -4
- package/dist/src/services/comment_service.d.ts.map +1 -1
- package/dist/src/services/comment_service.js +5 -4
- package/dist/src/services/comment_service.js.map +1 -1
- package/dist/src/services/main.js +2 -2
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/storage/file_system_storage.d.ts +2 -2
- package/dist/src/storage/file_system_storage.js +2 -2
- package/dist/src/storage/in_memory_storage.d.ts +3 -3
- package/dist/src/storage/in_memory_storage.js +3 -3
- package/dist/src/storage/lucid_storage.d.ts +9 -8
- package/dist/src/storage/lucid_storage.d.ts.map +1 -1
- package/dist/src/storage/lucid_storage.js +13 -12
- package/dist/src/storage/lucid_storage.js.map +1 -1
- package/dist/src/types.d.ts +62 -35
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +5 -6
- package/dist/src/types.js.map +1 -1
- package/dist/src/versioning.d.ts +3 -3
- package/dist/src/versioning.d.ts.map +1 -1
- package/dist/src/versioning.js +3 -3
- package/dist/src/versioning.js.map +1 -1
- package/dist/stubs/collaboration/document_types.stub +41 -0
- package/dist/stubs/collaboration/documents_dir.stub +3 -0
- package/dist/stubs/collaboration/migrations/collab_comments.stub +29 -0
- package/dist/stubs/collaboration/migrations/collab_documents.stub +23 -0
- package/dist/stubs/collaboration/migrations/collab_versions.stub +26 -0
- package/dist/stubs/partykit/worker/package.json.stub +21 -0
- package/dist/stubs/partykit/worker/party.ts.stub +175 -0
- package/dist/stubs/partykit/worker/partykit.toml.stub +14 -0
- package/package.json +19 -4
- package/dist/src/services/presence_service.d.ts +0 -44
- package/dist/src/services/presence_service.d.ts.map +0 -1
- package/dist/src/services/presence_service.js +0 -103
- package/dist/src/services/presence_service.js.map +0 -1
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { CollaborationManager } from '../src/collaboration_manager.js';
|
|
2
|
+
import { collaborationRoutes } from '../src/routes.js';
|
|
2
3
|
import { setBootedApp } from '../src/services/booted_app.js';
|
|
3
4
|
/**
|
|
4
5
|
* Wires `@adonis-agora/collaboration` into the AdonisJS application:
|
|
5
6
|
*
|
|
6
7
|
* - `register()` hands the booted app to `services/main` and binds the
|
|
7
8
|
* `CollaborationManager` singleton from `config/collaboration.ts`;
|
|
8
|
-
* - `boot()` attaches the collaboration WebSocket upgrade to the
|
|
9
|
-
* server (path from config, default `/collaboration`);
|
|
9
|
+
* - `boot()`/`ready()` attaches the collaboration WebSocket upgrade to the
|
|
10
|
+
* Adonis HTTP server (path from config, default `/collaboration`);
|
|
11
|
+
* - `ready()` registers the built-in REST endpoints automatically unless
|
|
12
|
+
* `routes.enabled: false` — then call `collaborationRoutes(router)` in
|
|
13
|
+
* `start/routes.ts` with your own middleware;
|
|
10
14
|
* - `shutdown()` flushes pending stores and closes all connections.
|
|
11
15
|
*/
|
|
12
16
|
export default class CollaborationServiceProvider {
|
|
13
17
|
app;
|
|
18
|
+
config;
|
|
14
19
|
constructor(app) {
|
|
15
20
|
this.app = app;
|
|
16
21
|
}
|
|
@@ -19,32 +24,28 @@ export default class CollaborationServiceProvider {
|
|
|
19
24
|
// copy bin/server booted, never a pnpm-split non-booted one (see services/booted_app.ts).
|
|
20
25
|
setBootedApp(this.app);
|
|
21
26
|
const config = this.app.config.get('collaboration');
|
|
27
|
+
this.config = config;
|
|
22
28
|
this.app.container.singleton(CollaborationManager, () => {
|
|
23
29
|
if (!config?.authorize) {
|
|
24
|
-
throw new Error('Configure authorize()
|
|
30
|
+
throw new Error('Configure authorize() in config/collaboration.ts — the library never authorizes by default');
|
|
25
31
|
}
|
|
26
32
|
const managerConfig = {
|
|
27
33
|
engine: config.engine ?? 'yjs',
|
|
28
|
-
authorize:
|
|
34
|
+
authorize: config.authorize,
|
|
29
35
|
path: config.path ?? '/collaboration',
|
|
30
36
|
debounce: config.debounce ?? 2000,
|
|
37
|
+
...(config.storage ? { storage: config.storage } : {}),
|
|
38
|
+
...(config.redisUrl ? { redis: { url: config.redisUrl } } : {}),
|
|
39
|
+
...(config.partykit ? { partykit: config.partykit } : {}),
|
|
31
40
|
};
|
|
32
|
-
if (config.storage) {
|
|
33
|
-
managerConfig.storage = config.storage;
|
|
34
|
-
}
|
|
35
|
-
if (config.redisUrl) {
|
|
36
|
-
managerConfig.redis = { url: config.redisUrl };
|
|
37
|
-
}
|
|
38
41
|
return new CollaborationManager(managerConfig);
|
|
39
42
|
});
|
|
40
43
|
}
|
|
41
44
|
async ready() {
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// Sem HTTP server fora do processo web (ace/console, test, repl) — a
|
|
47
|
-
// colaboração não sobe; o manager segue disponível pra uso programático.
|
|
45
|
+
// Attach happens in `ready()` (lifecycle phase 4): Ignitor created the
|
|
46
|
+
// node HTTP server during `start()` (phase 3), so getNodeServer() exists.
|
|
47
|
+
// Outside the web process (ace/console/test/repl) there is no HTTP
|
|
48
|
+
// server — collaboration does not start; the manager stays available.
|
|
48
49
|
const env = this.app.getEnvironment();
|
|
49
50
|
if (env !== 'web') {
|
|
50
51
|
return;
|
|
@@ -53,9 +54,27 @@ export default class CollaborationServiceProvider {
|
|
|
53
54
|
const adonisServer = (await this.app.container.make('server'));
|
|
54
55
|
const nodeServer = adonisServer.getNodeServer?.();
|
|
55
56
|
if (!nodeServer) {
|
|
56
|
-
throw new Error('[@adonis-agora/collaboration] getNodeServer()
|
|
57
|
+
throw new Error('[@adonis-agora/collaboration] getNodeServer() unavailable in ready() — check @adonisjs/http-server version');
|
|
57
58
|
}
|
|
58
59
|
await manager.attach(nodeServer);
|
|
60
|
+
// Auto-register built-in REST endpoints unless explicitly disabled.
|
|
61
|
+
const routes = this.config?.routes;
|
|
62
|
+
if (routes?.enabled === false) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const router = (await this.app.container.make('router'));
|
|
66
|
+
const runtimeOptions = {
|
|
67
|
+
managerFallback: () => this.app.container.make(CollaborationManager),
|
|
68
|
+
};
|
|
69
|
+
if (routes?.prefix !== undefined)
|
|
70
|
+
runtimeOptions.prefix = routes.prefix;
|
|
71
|
+
if (routes?.middleware !== undefined)
|
|
72
|
+
runtimeOptions.middleware = routes.middleware;
|
|
73
|
+
if (routes?.resolveUser !== undefined)
|
|
74
|
+
runtimeOptions.resolveUser = routes.resolveUser;
|
|
75
|
+
if (this.config?.authorize !== undefined)
|
|
76
|
+
runtimeOptions.authorize = this.config.authorize;
|
|
77
|
+
await collaborationRoutes(router, runtimeOptions);
|
|
59
78
|
}
|
|
60
79
|
async shutdown() {
|
|
61
80
|
const manager = await this.app.container.make(CollaborationManager);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collaboration_provider.js","sourceRoot":"","sources":["../../providers/collaboration_provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"collaboration_provider.js","sourceRoot":"","sources":["../../providers/collaboration_provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CollabConnectionContext, CollabPermission, PartyKitConfig } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The token/auth module — the single seam for issuing and verifying
|
|
4
|
+
* collaboration credentials.
|
|
5
|
+
*
|
|
6
|
+
* Two wire formats, chosen by engine:
|
|
7
|
+
* - `partykit` → HS256 JWT (verified by the edge worker via WebCrypto)
|
|
8
|
+
* - self-hosted (`yjs`/`automerge`) → legacy base64 JSON payload accepted
|
|
9
|
+
* by the driver handshake
|
|
10
|
+
*
|
|
11
|
+
* `issueToken` also enforces `config.authorize` on the REST path — the
|
|
12
|
+
* permission seam must hold outside the WebSocket handshake too.
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenIssuerConfig {
|
|
15
|
+
engine: string;
|
|
16
|
+
/** WS path of the embedded Hocuspocus server (self-hosted engines). */
|
|
17
|
+
path?: string;
|
|
18
|
+
partykit?: Partial<Pick<PartyKitConfig, 'roomHost' | 'jwtSecret' | 'party'>>;
|
|
19
|
+
}
|
|
20
|
+
/** Minimal authorize contract — mirrors CollaborationConfig.authorize. */
|
|
21
|
+
export type AuthorizeFn = (ctx: CollabConnectionContext, docName: string) => Promise<CollabPermission>;
|
|
22
|
+
/** Signs a minimal HS256 JWT — no external dependency. */
|
|
23
|
+
export declare function signPartyKitToken(payload: {
|
|
24
|
+
userId: string;
|
|
25
|
+
docName: string;
|
|
26
|
+
user?: {
|
|
27
|
+
name?: string;
|
|
28
|
+
avatarUrl?: string | null;
|
|
29
|
+
};
|
|
30
|
+
}, secret: string, expiresInSeconds?: number): string;
|
|
31
|
+
/** Verifies signature + expiry of a token issued by {@link signPartyKitToken}. */
|
|
32
|
+
export declare function verifyPartyKitToken(token: string, secret: string): {
|
|
33
|
+
userId: string;
|
|
34
|
+
docName: string;
|
|
35
|
+
exp: number;
|
|
36
|
+
} | null;
|
|
37
|
+
/** Parses the legacy base64 payload used by self-hosted driver handshakes. */
|
|
38
|
+
export declare function parseLegacyToken(token: string): CollabConnectionContext | null;
|
|
39
|
+
/** Builds the browser-facing WebSocket URL for an engine. */
|
|
40
|
+
export declare function buildWsUrl(config: TokenIssuerConfig, docName: string): string;
|
|
41
|
+
/** Result of a successful token issue. */
|
|
42
|
+
export interface IssuedToken {
|
|
43
|
+
token: string;
|
|
44
|
+
wsUrl: string;
|
|
45
|
+
engine: string;
|
|
46
|
+
}
|
|
47
|
+
/** Thrown when authorize() denies read access for the document. */
|
|
48
|
+
export declare class CollabForbiddenError extends Error {
|
|
49
|
+
constructor(docName: string);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Issues an ephemeral credential for `docName`, enforcing the permission
|
|
53
|
+
* seam via `authorize` when one is provided.
|
|
54
|
+
*/
|
|
55
|
+
export declare function issueToken(config: TokenIssuerConfig, user: {
|
|
56
|
+
id: string;
|
|
57
|
+
name?: string | null;
|
|
58
|
+
}, docName: string, authorize?: AuthorizeFn): Promise<IssuedToken>;
|
|
59
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/auth/token.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7F;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAC9E;AAED,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,uBAAuB,EAC5B,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/B,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EACjG,MAAM,EAAE,MAAM,EACd,gBAAgB,SAAM,GACrB,MAAM,CAQR;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoBzD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAO9E;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAS7E;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mEAAmE;AACnE,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC1C,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,WAAW,GACtB,OAAO,CAAC,WAAW,CAAC,CA4BtB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/** Signs a minimal HS256 JWT — no external dependency. */
|
|
3
|
+
export function signPartyKitToken(payload, secret, expiresInSeconds = 300) {
|
|
4
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
5
|
+
const now = Math.floor(Date.now() / 1000);
|
|
6
|
+
const body = { ...payload, iat: now, exp: now + expiresInSeconds };
|
|
7
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
8
|
+
const unsigned = `${encode(header)}.${encode(body)}`;
|
|
9
|
+
const signature = createHmac('sha256', secret).update(unsigned).digest('base64url');
|
|
10
|
+
return `${unsigned}.${signature}`;
|
|
11
|
+
}
|
|
12
|
+
/** Verifies signature + expiry of a token issued by {@link signPartyKitToken}. */
|
|
13
|
+
export function verifyPartyKitToken(token, secret) {
|
|
14
|
+
const parts = token.split('.');
|
|
15
|
+
if (parts.length !== 3)
|
|
16
|
+
return null;
|
|
17
|
+
const [header, body, signature] = parts;
|
|
18
|
+
const expected = createHmac('sha256', secret).update(`${header}.${body}`).digest();
|
|
19
|
+
const received = Buffer.from(signature, 'base64url');
|
|
20
|
+
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(Buffer.from(body, 'base64url').toString());
|
|
25
|
+
if (!parsed.userId || !parsed.docName || parsed.exp * 1000 < Date.now())
|
|
26
|
+
return null;
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Parses the legacy base64 payload used by self-hosted driver handshakes. */
|
|
34
|
+
export function parseLegacyToken(token) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(Buffer.from(token, 'base64').toString());
|
|
37
|
+
return parsed.userId ? parsed : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Builds the browser-facing WebSocket URL for an engine. */
|
|
44
|
+
export function buildWsUrl(config, docName) {
|
|
45
|
+
if (config.engine === 'partykit') {
|
|
46
|
+
const { roomHost, party } = config.partykit ?? {};
|
|
47
|
+
const scheme = roomHost?.startsWith('localhost') || roomHost?.startsWith('127.0.0.1') ? 'ws' : 'wss';
|
|
48
|
+
const host = roomHost?.replace(/^https?:\/\//, '') ?? '';
|
|
49
|
+
return `${scheme}://${host}/parties/${party ?? 'main'}/${encodeURIComponent(docName)}`;
|
|
50
|
+
}
|
|
51
|
+
return config.path ?? '/collaboration';
|
|
52
|
+
}
|
|
53
|
+
/** Thrown when authorize() denies read access for the document. */
|
|
54
|
+
export class CollabForbiddenError extends Error {
|
|
55
|
+
constructor(docName) {
|
|
56
|
+
super(`no access to document "${docName}"`);
|
|
57
|
+
this.name = 'CollabForbiddenError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Issues an ephemeral credential for `docName`, enforcing the permission
|
|
62
|
+
* seam via `authorize` when one is provided.
|
|
63
|
+
*/
|
|
64
|
+
export async function issueToken(config, user, docName, authorize) {
|
|
65
|
+
const ctx = {
|
|
66
|
+
userId: user.id,
|
|
67
|
+
...(user.name ? { user: { name: user.name } } : {}),
|
|
68
|
+
};
|
|
69
|
+
let permission;
|
|
70
|
+
if (authorize) {
|
|
71
|
+
permission = await authorize(ctx, docName);
|
|
72
|
+
if (!permission.canRead)
|
|
73
|
+
throw new CollabForbiddenError(docName);
|
|
74
|
+
}
|
|
75
|
+
if (config.engine === 'partykit') {
|
|
76
|
+
if (!config.partykit?.jwtSecret) {
|
|
77
|
+
throw new Error('config.partykit.jwtSecret is required when engine=partykit');
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
token: signPartyKitToken({ userId: user.id, docName }, config.partykit.jwtSecret),
|
|
81
|
+
wsUrl: buildWsUrl(config, docName),
|
|
82
|
+
engine: config.engine,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
token: Buffer.from(JSON.stringify(ctx)).toString('base64'),
|
|
87
|
+
wsUrl: buildWsUrl(config, docName),
|
|
88
|
+
engine: config.engine,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../../src/auth/token.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer for the collaborative-documents registry generated at
|
|
3
|
+
* `.adonisjs/collaboration/documents.ts` by the Assembler `init` hook.
|
|
4
|
+
*
|
|
5
|
+
* Pure function so it is trivially testable — the hook only adapts
|
|
6
|
+
* vfs/buffer and delegates here.
|
|
7
|
+
*/
|
|
8
|
+
/** File discovered by the IndexGenerator. */
|
|
9
|
+
export interface RegistryFile {
|
|
10
|
+
/** Extension-less relative key from the vfs (e.g. 'researches/writing_types'). */
|
|
11
|
+
vfsKey: string;
|
|
12
|
+
/** Import path already resolved via helpers.toImportPath. */
|
|
13
|
+
importPath: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Converts a vfs key into a document name — same convention as
|
|
17
|
+
* `make:collab-document`: the '_types' suffix is stripped.
|
|
18
|
+
*
|
|
19
|
+
* 'researches/writing_types' -> 'researches/writing'
|
|
20
|
+
*/
|
|
21
|
+
export declare function documentKeyFromVfsKey(vfsKey: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Renders the typed registry lines. The map is type-only (`typeof import`),
|
|
24
|
+
* so the file produces no runtime code — consumed by tsc and re-exportable
|
|
25
|
+
* to the frontend through manifest/config types.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderDocumentsRegistry(files: RegistryFile[]): string[];
|
|
28
|
+
//# sourceMappingURL=documents_registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents_registry.d.ts","sourceRoot":"","sources":["../../../src/codegen/documents_registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,CAiDvE"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer for the collaborative-documents registry generated at
|
|
3
|
+
* `.adonisjs/collaboration/documents.ts` by the Assembler `init` hook.
|
|
4
|
+
*
|
|
5
|
+
* Pure function so it is trivially testable — the hook only adapts
|
|
6
|
+
* vfs/buffer and delegates here.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Converts a vfs key into a document name — same convention as
|
|
10
|
+
* `make:collab-document`: the '_types' suffix is stripped.
|
|
11
|
+
*
|
|
12
|
+
* 'researches/writing_types' -> 'researches/writing'
|
|
13
|
+
*/
|
|
14
|
+
export function documentKeyFromVfsKey(vfsKey) {
|
|
15
|
+
return vfsKey.replace(/_types$/, '');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders the typed registry lines. The map is type-only (`typeof import`),
|
|
19
|
+
* so the file produces no runtime code — consumed by tsc and re-exportable
|
|
20
|
+
* to the frontend through manifest/config types.
|
|
21
|
+
*/
|
|
22
|
+
export function renderDocumentsRegistry(files) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const unique = files.filter((file) => {
|
|
25
|
+
const key = documentKeyFromVfsKey(file.vfsKey);
|
|
26
|
+
if (seen.has(key))
|
|
27
|
+
return false;
|
|
28
|
+
seen.add(key);
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
const lines = [
|
|
32
|
+
'/**',
|
|
33
|
+
' * Generated by @adonis-agora/collaboration — do not edit.',
|
|
34
|
+
' * Refresh with `node ace codegen`.',
|
|
35
|
+
' */',
|
|
36
|
+
];
|
|
37
|
+
if (unique.length === 0) {
|
|
38
|
+
lines.push('', '/** No collaborative documents declared yet. Use `node ace make:collab-document`. */', 'export type CollabDocumentName = never;', '', 'export type CollabDocumentDefinition<Name extends CollabDocumentName> = Name extends never', ' ? never', ' : unknown;');
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
lines.push('', 'type CollabDocumentModules = {');
|
|
42
|
+
for (const file of unique) {
|
|
43
|
+
const key = documentKeyFromVfsKey(file.vfsKey);
|
|
44
|
+
// Always quote — doc names may contain slashes/hyphens.
|
|
45
|
+
lines.push(` '${key}': typeof import('${file.importPath}');`);
|
|
46
|
+
}
|
|
47
|
+
lines.push('};', '', '/** Valid collaborative document names in this app. */', 'export type CollabDocumentName = keyof CollabDocumentModules;', '', '/** Document definition module (the *_types default export). */', 'export type CollabDocumentDefinition<Name extends CollabDocumentName> =', ' CollabDocumentModules[Name][', "'default'", '];');
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=documents_registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents_registry.js","sourceRoot":"","sources":["../../../src/codegen/documents_registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,OAAO,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAqB;IAC3D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QACnC,MAAM,GAAG,GAAG,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/C,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAa;QACtB,KAAK;QACL,4DAA4D;QAC5D,qCAAqC;QACrC,KAAK;KACN,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CACR,EAAE,EACF,sFAAsF,EACtF,yCAAyC,EACzC,EAAE,EACF,4FAA4F,EAC5F,WAAW,EACX,cAAc,CACf,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,gCAAgC,CAAC,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/C,wDAAwD;QACxD,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,qBAAqB,IAAI,CAAC,UAAU,KAAK,CAAC,CAAC;IACjE,CAAC;IACD,KAAK,CAAC,IAAI,CACR,IAAI,EACJ,EAAE,EACF,wDAAwD,EACxD,+DAA+D,EAC/D,EAAE,EACF,iEAAiE,EACjE,yEAAyE,EACzE,gCAAgC,EAChC,WAAW,EACX,IAAI,CACL,CAAC;IAEF,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen_init.d.ts","sourceRoot":"","sources":["../../src/codegen_init.ts"],"names":[],"mappings":"wBA6D2B,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO;AAA1D,wBAA2D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { hooks } from '@adonisjs/core/app';
|
|
3
|
+
import { renderDocumentsRegistry } from './codegen/documents_registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Assembler `init` hook (AdonisJS v7 codegen).
|
|
6
|
+
*
|
|
7
|
+
* Registers an IndexGenerator source that scans `app/collaboration/documents`
|
|
8
|
+
* and generates `.adonisjs/collaboration/documents.ts` — the typed registry
|
|
9
|
+
* of collaborative documents (`CollabDocumentName`,
|
|
10
|
+
* `CollabDocumentDefinition`), refreshed by `node ace codegen`, the dev
|
|
11
|
+
* server, test runner and production build.
|
|
12
|
+
*
|
|
13
|
+
* Registered in adonisrc.ts by `node ace configure`:
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* hooks: {
|
|
17
|
+
* init: [() => import('@adonis-agora/collaboration/codegen_init')]
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
// The wrapper's inferred return type references non-portable @poppinss
|
|
22
|
+
// internals — annotate explicitly so the declaration emit stays portable.
|
|
23
|
+
const initHook = hooks.init((_parent, _hooksManager, indexGenerator) => {
|
|
24
|
+
const source = './app/collaboration/documents';
|
|
25
|
+
// Directory not created yet: nothing to index (make:collab-document creates it).
|
|
26
|
+
if (!existsSync(source)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
indexGenerator.add('collabDocuments', {
|
|
30
|
+
source,
|
|
31
|
+
glob: ['**/*.ts'],
|
|
32
|
+
output: './.adonisjs/collaboration/documents.ts',
|
|
33
|
+
as: (vfs, buffer, _config, helpers) => {
|
|
34
|
+
const files = Object.entries(vfs.asList()).map(([vfsKey, absolutePath]) => ({
|
|
35
|
+
vfsKey,
|
|
36
|
+
importPath: helpers.toImportPath(absolutePath),
|
|
37
|
+
}));
|
|
38
|
+
for (const line of renderDocumentsRegistry(files)) {
|
|
39
|
+
buffer.writeLine(line);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
export default initHook;
|
|
45
|
+
//# sourceMappingURL=codegen_init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen_init.js","sourceRoot":"","sources":["../../src/codegen_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAyB,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAEjG;;;;;;;;;;;;;;;;GAgBG;AACH,uEAAuE;AACvE,0EAA0E;AAC1E,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CACzB,CACE,OAAgB,EAChB,aAAsB,EACtB,cAEC,EACD,EAAE;IACF,MAAM,MAAM,GAAG,+BAA+B,CAAC;IAE/C,iFAAiF;IACjF,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,OAAO;IACT,CAAC;IAED,cAAc,CAAC,GAAG,CAAC,iBAAiB,EAAE;QACpC,MAAM;QACN,IAAI,EAAE,CAAC,SAAS,CAAC;QACjB,MAAM,EAAE,wCAAwC;QAChD,EAAE,EAAE,CACF,GAAyC,EACzC,MAA4C,EAC5C,OAAgB,EAChB,OAAmD,EACnD,EAAE;YACF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC1E,MAAM;gBACN,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC;aAC/C,CAAC,CAAC,CAAC;YAEJ,KAAK,MAAM,IAAI,IAAI,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CACF,CAAC;AAEF,eAAe,QAA2C,CAAC"}
|
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
import type { CollaborationDriver } from './driver.js';
|
|
2
2
|
import { AutomergeDriver } from './drivers/automerge/automerge_driver.js';
|
|
3
|
+
import { PartyKitDriver } from './drivers/partykit/partykit_driver.js';
|
|
3
4
|
import { YjsDriver } from './drivers/yjs/yjs_driver.js';
|
|
4
5
|
import { CommentService } from './services/comment_service.js';
|
|
5
|
-
import { type PresenceMember, PresenceService } from './services/presence_service.js';
|
|
6
6
|
import type { CollabDiffSummary, CollabVersion, CollaborationConfig } from './types.js';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Library facade: picks the driver for the configured engine and exposes the
|
|
9
|
+
* unified API (documents + versions + comments) to the host app.
|
|
10
|
+
*
|
|
11
|
+
* Seams:
|
|
12
|
+
* - drivers connect only through the registry below;
|
|
13
|
+
* - comments live exclusively here (`manager.comments`) — drivers never
|
|
14
|
+
* touch them;
|
|
15
|
+
* - presence is owned by each engine at runtime (edge worker awareness or
|
|
16
|
+
* client-side), not by this process.
|
|
10
17
|
*/
|
|
11
18
|
export declare class CollaborationManager {
|
|
12
19
|
private config;
|
|
13
20
|
private driver;
|
|
14
|
-
presence: PresenceService;
|
|
15
21
|
comments: CommentService;
|
|
16
22
|
constructor(config: CollaborationConfig);
|
|
17
23
|
get engine(): string;
|
|
18
|
-
/**
|
|
24
|
+
/** Attaches the driver's WebSocket to the Adonis HTTP server. No-op on edge. */
|
|
19
25
|
attach(server: import('node:http').Server): Promise<void>;
|
|
20
|
-
/**
|
|
26
|
+
/** Current binary state (for external push — Google Drive, export...). */
|
|
21
27
|
getDocumentState(docName: string): Promise<Uint8Array>;
|
|
22
|
-
/**
|
|
28
|
+
/** Plain text (LanguageTool, RAG, comment indexes). */
|
|
23
29
|
getDocumentText(docName: string): Promise<string>;
|
|
24
30
|
createVersion(docName: string, createdBy: string | null, label?: string | null): Promise<CollabVersion>;
|
|
25
31
|
listVersions(docName: string): Promise<CollabVersion[]>;
|
|
26
32
|
restoreVersion(docName: string, versionId: string, restoredBy: string | null): Promise<void>;
|
|
27
33
|
diffVersions(docName: string, aId: string, bId: string): Promise<CollabDiffSummary>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Persists an external snapshot (e.g. PartyKit worker post-debounce).
|
|
36
|
+
* Requires a configured storage backend.
|
|
37
|
+
*/
|
|
38
|
+
persistDocument(docName: string, state: Uint8Array): Promise<void>;
|
|
30
39
|
close(): Promise<void>;
|
|
31
40
|
}
|
|
32
|
-
export { YjsDriver, AutomergeDriver };
|
|
33
|
-
export type {
|
|
41
|
+
export { YjsDriver, AutomergeDriver, PartyKitDriver };
|
|
42
|
+
export type { CollaborationConfig, CollaborationDriver };
|
|
34
43
|
//# sourceMappingURL=collaboration_manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collaboration_manager.d.ts","sourceRoot":"","sources":["../../src/collaboration_manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"collaboration_manager.d.ts","sourceRoot":"","sources":["../../src/collaboration_manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAEvE,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExF;;;;;;;;;;GAUG;AACH,qBAAa,oBAAoB;IAInB,OAAO,CAAC,MAAM;IAH1B,OAAO,CAAC,MAAM,CAAsB;IACpC,QAAQ,EAAE,cAAc,CAAC;gBAEL,MAAM,EAAE,mBAAmB;IA0B/C,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,gFAAgF;IAC1E,MAAM,CAAC,MAAM,EAAE,OAAO,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D,0EAA0E;IAC1E,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAItD,uDAAuD;IACvD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjD,aAAa,CACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAAC,aAAa,CAAC;IAIzB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAIvD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5F,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAInF;;;OAGG;IACG,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IASlE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AACtD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -1,36 +1,60 @@
|
|
|
1
1
|
import { AutomergeDriver } from './drivers/automerge/automerge_driver.js';
|
|
2
|
+
import { PartyKitDriver } from './drivers/partykit/partykit_driver.js';
|
|
3
|
+
import { resolveStorage } from './drivers/shared.js';
|
|
2
4
|
import { YjsDriver } from './drivers/yjs/yjs_driver.js';
|
|
3
5
|
import { CommentService } from './services/comment_service.js';
|
|
4
|
-
import { PresenceService } from './services/presence_service.js';
|
|
5
|
-
import { InMemoryCollaborationStorage } from './storage/in_memory_storage.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Library facade: picks the driver for the configured engine and exposes the
|
|
8
|
+
* unified API (documents + versions + comments) to the host app.
|
|
9
|
+
*
|
|
10
|
+
* Seams:
|
|
11
|
+
* - drivers connect only through the registry below;
|
|
12
|
+
* - comments live exclusively here (`manager.comments`) — drivers never
|
|
13
|
+
* touch them;
|
|
14
|
+
* - presence is owned by each engine at runtime (edge worker awareness or
|
|
15
|
+
* client-side), not by this process.
|
|
9
16
|
*/
|
|
10
17
|
export class CollaborationManager {
|
|
11
18
|
config;
|
|
12
19
|
driver;
|
|
13
|
-
presence;
|
|
14
20
|
comments;
|
|
15
21
|
constructor(config) {
|
|
16
22
|
this.config = config;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const engine = config.engine ?? 'yjs';
|
|
24
|
+
const storage = config.storage;
|
|
25
|
+
if (engine === 'partykit') {
|
|
26
|
+
this.driver = new PartyKitDriver({
|
|
27
|
+
...(storage ? { storage } : {}),
|
|
28
|
+
partykit: config.partykit,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const shared = {
|
|
33
|
+
authorize: config.authorize,
|
|
34
|
+
};
|
|
35
|
+
const opts = {
|
|
36
|
+
...shared,
|
|
37
|
+
...(storage ? { storage } : {}),
|
|
38
|
+
...(config.path !== undefined ? { path: config.path } : {}),
|
|
39
|
+
...(config.debounce !== undefined ? { debounce: config.debounce } : {}),
|
|
40
|
+
};
|
|
41
|
+
this.driver = engine === 'automerge' ? new AutomergeDriver(opts) : new YjsDriver(opts);
|
|
42
|
+
}
|
|
43
|
+
// Single comments seam — one service, one storage instance.
|
|
44
|
+
this.comments = new CommentService(resolveStorage(storage));
|
|
21
45
|
}
|
|
22
46
|
get engine() {
|
|
23
47
|
return this.driver.engine;
|
|
24
48
|
}
|
|
25
|
-
/**
|
|
49
|
+
/** Attaches the driver's WebSocket to the Adonis HTTP server. No-op on edge. */
|
|
26
50
|
async attach(server) {
|
|
27
51
|
await this.driver.attach(server);
|
|
28
52
|
}
|
|
29
|
-
/**
|
|
53
|
+
/** Current binary state (for external push — Google Drive, export...). */
|
|
30
54
|
getDocumentState(docName) {
|
|
31
55
|
return this.driver.getDocumentState(docName);
|
|
32
56
|
}
|
|
33
|
-
/**
|
|
57
|
+
/** Plain text (LanguageTool, RAG, comment indexes). */
|
|
34
58
|
getDocumentText(docName) {
|
|
35
59
|
return this.driver.getDocumentText(docName);
|
|
36
60
|
}
|
|
@@ -46,14 +70,19 @@ export class CollaborationManager {
|
|
|
46
70
|
diffVersions(docName, aId, bId) {
|
|
47
71
|
return this.driver.diffVersions(docName, aId, bId);
|
|
48
72
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Persists an external snapshot (e.g. PartyKit worker post-debounce).
|
|
75
|
+
* Requires a configured storage backend.
|
|
76
|
+
*/
|
|
77
|
+
async persistDocument(docName, state) {
|
|
78
|
+
if (!this.config.storage) {
|
|
79
|
+
throw new Error('persistDocument requires a storage backend — configure storage in config/collaboration.ts');
|
|
80
|
+
}
|
|
81
|
+
await this.config.storage.saveDocument(docName, state);
|
|
52
82
|
}
|
|
53
83
|
async close() {
|
|
54
84
|
await this.driver.close();
|
|
55
|
-
await this.presence.close();
|
|
56
85
|
}
|
|
57
86
|
}
|
|
58
|
-
export { YjsDriver, AutomergeDriver };
|
|
87
|
+
export { YjsDriver, AutomergeDriver, PartyKitDriver };
|
|
59
88
|
//# sourceMappingURL=collaboration_manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collaboration_manager.js","sourceRoot":"","sources":["../../src/collaboration_manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"collaboration_manager.js","sourceRoot":"","sources":["../../src/collaboration_manager.ts"],"names":[],"mappings":"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"}
|