@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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.makePath('partykit-worker', 'party.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
/**
|
|
5
|
+
* Worker PartyKit do @adonis-agora/collaboration (edge sync).
|
|
6
|
+
*
|
|
7
|
+
* Um Durable Object por room (= docName). Os browsers conectam AQUI, direto
|
|
8
|
+
* — the WebSocket traffic does not pass through the Adonis server. This
|
|
9
|
+
* worker speaks the binary y-protocols and exposes an HTTP endpoint
|
|
10
|
+
* (`onRequest`) that
|
|
11
|
+
* o PartyKitDriver no Adonis consome pra estado/texto/restore.
|
|
12
|
+
*
|
|
13
|
+
* Deploy: npx partykit deploy (config em partykit.toml)
|
|
14
|
+
*/
|
|
15
|
+
import { onConnect } from 'y-partykit';
|
|
16
|
+
import type { PartyConnection, PartyConnectionContext, PartyRequest, PartyServer } from 'partykit/server';
|
|
17
|
+
import * as Y from 'yjs';
|
|
18
|
+
|
|
19
|
+
/** JWT payload issued by Adonis (GET /collaboration/token route). */
|
|
20
|
+
interface CollabToken {
|
|
21
|
+
userId: string;
|
|
22
|
+
docName: string;
|
|
23
|
+
exp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function verifyToken(token: string | null, secret: string): Promise<CollabToken | null> {
|
|
27
|
+
if (!token) return null;
|
|
28
|
+
const parts = token.split('.');
|
|
29
|
+
if (parts.length !== 3) return null;
|
|
30
|
+
const [header, body, signature] = parts as [string, string, string];
|
|
31
|
+
const key = await crypto.subtle.importKey(
|
|
32
|
+
'raw',
|
|
33
|
+
new TextEncoder().encode(secret),
|
|
34
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
35
|
+
false,
|
|
36
|
+
['verify'],
|
|
37
|
+
);
|
|
38
|
+
const valid = await crypto.subtle.verify(
|
|
39
|
+
'HMAC',
|
|
40
|
+
key,
|
|
41
|
+
Buffer.from(signature, 'base64url'),
|
|
42
|
+
new TextEncoder().encode(`${header}.${body}`),
|
|
43
|
+
);
|
|
44
|
+
if (!valid) return null;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(Buffer.from(body, 'base64url').toString()) as CollabToken;
|
|
47
|
+
if (!parsed.userId || !parsed.docName || parsed.exp * 1000 < Date.now()) return null;
|
|
48
|
+
return parsed;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default class CollabParty implements PartyServer {
|
|
55
|
+
/** Doc vivo do room — populado pelo primeiro ciclo load/callback. */
|
|
56
|
+
private liveDoc?: Y.Doc;
|
|
57
|
+
|
|
58
|
+
constructor(public party: import('partykit/server').Party) {}
|
|
59
|
+
|
|
60
|
+
static async onBeforeConnect(request: PartyRequest, lobby: { context: PartyConnectionContext }) {
|
|
61
|
+
const secret = lobby.context.party.env.COLLAB_JWT_SECRET as string | undefined;
|
|
62
|
+
const token = new URL(request.url).searchParams.get('token');
|
|
63
|
+
const claims = await verifyToken(token, secret ?? '');
|
|
64
|
+
if (!claims) {
|
|
65
|
+
return new Response('invalid or expired token', { status: 401 });
|
|
66
|
+
}
|
|
67
|
+
// Forward claims to onConnect via synthetic headers.
|
|
68
|
+
request.headers.set('x-collab-user', claims.userId);
|
|
69
|
+
request.headers.set('x-collab-doc', claims.docName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async onConnect(connection: PartyConnection, ctx: PartyConnectionContext) {
|
|
73
|
+
await onConnect(connection, this.party, {
|
|
74
|
+
// Debounced persistence into DO storage; large docs should use the
|
|
75
|
+
// history mode (10 MB cap) — tune for your workload.
|
|
76
|
+
persist: { mode: 'snapshot' },
|
|
77
|
+
load: async () => {
|
|
78
|
+
// Initial state: fetch the authoritative snapshot from Adonis
|
|
79
|
+
// (source of truth for versions). Falls back to empty.
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.fetchFromAdonis(`/collaboration/state`);
|
|
82
|
+
if (!response.ok) return Y.encodeStateAsUpdate(new Y.Doc());
|
|
83
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
84
|
+
} catch {
|
|
85
|
+
return Y.encodeStateAsUpdate(new Y.Doc());
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
callback: {
|
|
89
|
+
handler: async (doc: Y.Doc) => {
|
|
90
|
+
// Keep the reference so onRequest serves real state/text.
|
|
91
|
+
this.liveDoc = doc;
|
|
92
|
+
// Post-debounce persistence back to Adonis.
|
|
93
|
+
try {
|
|
94
|
+
await this.fetchFromAdonis('/collaboration/state', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: Y.encodeStateAsUpdate(doc),
|
|
97
|
+
});
|
|
98
|
+
} catch {
|
|
99
|
+
// Transient failure: local DO storage already ensured durability.
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
debounceWait: 2000,
|
|
103
|
+
debounceMaxWait: 10000,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** HTTP API consumed by the PartyKitDriver (Adonis side). */
|
|
109
|
+
async onRequest(request: PartyRequest): Promise<Response> {
|
|
110
|
+
const secret = this.party.env.COLLAB_JWT_SECRET as string | undefined;
|
|
111
|
+
const url = new URL(request.url);
|
|
112
|
+
const claims =
|
|
113
|
+
(await verifyToken(url.searchParams.get('token'), secret ?? '')) ??
|
|
114
|
+
(await verifyToken(request.headers.get('authorization')?.replace(/^Bearer /, '') ?? '', secret ?? ''));
|
|
115
|
+
if (!claims) {
|
|
116
|
+
return new Response('unauthenticated', { status: 401 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The room name is the docName (clients connect at /parties/main/<docName>).
|
|
120
|
+
const doc = this.liveDoc ?? new Y.Doc();
|
|
121
|
+
|
|
122
|
+
if (request.method === 'GET') {
|
|
123
|
+
const mode = request.headers.get('x-collab-return') ?? 'update';
|
|
124
|
+
if (mode === 'text') {
|
|
125
|
+
return new Response(this.extractText(doc), {
|
|
126
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return new Response(Y.encodeStateAsUpdate(doc), {
|
|
130
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (request.method === 'POST') {
|
|
135
|
+
// Restore: apply the update/snapshot posted by Adonis to the live doc.
|
|
136
|
+
const update = new Uint8Array(await request.arrayBuffer());
|
|
137
|
+
Y.applyUpdate(doc, update);
|
|
138
|
+
return new Response(null, { status: 204 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return new Response('method not allowed', { status: 405 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private extractText(doc: Y.Doc): string {
|
|
145
|
+
let text = '';
|
|
146
|
+
const fragment = doc.getXmlFragment('default');
|
|
147
|
+
for (const block of fragment.toArray()) {
|
|
148
|
+
text += `${block.toString()}\n\n`;
|
|
149
|
+
}
|
|
150
|
+
return text.trimEnd();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The room name identifies the document — same value used in the ?doc=
|
|
155
|
+
* query of REST routes and by the client hooks.
|
|
156
|
+
*/
|
|
157
|
+
private get docName(): string {
|
|
158
|
+
return this.party.name;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fetchFromAdonis(path: string, init?: { method: 'POST'; body: Uint8Array }): Promise<Response> {
|
|
162
|
+
const base = this.party.env.ADONIS_BASE_URL as string;
|
|
163
|
+
const url = `${base}${path}?doc=${encodeURIComponent(this.docName)}`;
|
|
164
|
+
const headers: Record<string, string> = {
|
|
165
|
+
// Shared secret configured via `partykit secret put COLLAB_WORKER_SECRET`
|
|
166
|
+
'x-collab-worker-secret': (this.party.env.COLLAB_WORKER_SECRET as string | undefined) ?? '',
|
|
167
|
+
...(init?.body ? { 'content-type': 'application/octet-stream' } : {}),
|
|
168
|
+
};
|
|
169
|
+
return fetch(url, {
|
|
170
|
+
method: init?.method ?? 'GET',
|
|
171
|
+
headers,
|
|
172
|
+
body: init?.body ? (init.body as unknown as ArrayBufferView) : undefined,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.makePath('partykit-worker', 'partykit.toml') })
|
|
3
|
+
}}}
|
|
4
|
+
# Config do worker PartyKit — docs: https://docs.partykit.io/reference/partykit-configuration/
|
|
5
|
+
name = "{{ partykitWorkerName }}"
|
|
6
|
+
main = "party.ts"
|
|
7
|
+
compatibility_date = "2025-01-01"
|
|
8
|
+
|
|
9
|
+
# Segredo do JWT compartilhado com o Adonis (config/collaboration.ts).
|
|
10
|
+
[vars]
|
|
11
|
+
ADONIS_BASE_URL = "{{ adonisBaseUrl }}"
|
|
12
|
+
|
|
13
|
+
# COLLAB_JWT_SECRET goes as a secret, not a var:
|
|
14
|
+
# npx partykit secret put COLLAB_JWT_SECRET
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/collaboration",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Collaborative editing (CRDT) for AdonisJS 7 — Yjs (Hocuspocus) and Automerge drivers, Redis-backed presence, unified version control. Part of the Agora ecosystem.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./dist/src/drivers/automerge/automerge_driver.js",
|
|
34
34
|
"default": "./dist/src/drivers/automerge/automerge_driver.js"
|
|
35
35
|
},
|
|
36
|
+
"./drivers/partykit": {
|
|
37
|
+
"types": "./dist/src/drivers/partykit/partykit_driver.d.ts",
|
|
38
|
+
"import": "./dist/src/drivers/partykit/partykit_driver.js",
|
|
39
|
+
"default": "./dist/src/drivers/partykit/partykit_driver.js"
|
|
40
|
+
},
|
|
36
41
|
"./storage/file_system": {
|
|
37
42
|
"types": "./dist/src/storage/file_system_storage.d.ts",
|
|
38
43
|
"import": "./dist/src/storage/file_system_storage.js",
|
|
@@ -58,6 +63,16 @@
|
|
|
58
63
|
"import": "./dist/providers/collaboration_provider.js",
|
|
59
64
|
"default": "./dist/providers/collaboration_provider.js"
|
|
60
65
|
},
|
|
66
|
+
"./commands": {
|
|
67
|
+
"types": "./dist/commands/main.d.ts",
|
|
68
|
+
"import": "./dist/commands/main.js",
|
|
69
|
+
"default": "./dist/commands/main.js"
|
|
70
|
+
},
|
|
71
|
+
"./codegen_init": {
|
|
72
|
+
"types": "./dist/src/codegen_init.d.ts",
|
|
73
|
+
"import": "./dist/src/codegen_init.js",
|
|
74
|
+
"default": "./dist/src/codegen_init.js"
|
|
75
|
+
},
|
|
61
76
|
"./configure": {
|
|
62
77
|
"types": "./dist/configure.d.ts",
|
|
63
78
|
"import": "./dist/configure.js",
|
|
@@ -71,10 +86,11 @@
|
|
|
71
86
|
],
|
|
72
87
|
"sideEffects": false,
|
|
73
88
|
"scripts": {
|
|
74
|
-
"build": "pnpm run clean && tsc -p tsconfig.build.json && pnpm run copy:stubs",
|
|
89
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json && pnpm run build:commands && pnpm run copy:stubs",
|
|
75
90
|
"check:dist": "node ../../scripts/assert-build-output.mjs dist src/index.js",
|
|
76
91
|
"clean": "rm -rf dist",
|
|
77
|
-
"
|
|
92
|
+
"build:commands": "node ./scripts/build-commands.mjs",
|
|
93
|
+
"copy:stubs": "mkdir -p dist/stubs/config dist/stubs/partykit/worker dist/stubs/collaboration/migrations && cp stubs/main.ts dist/stubs/main.js && cp stubs/config/collaboration.stub dist/stubs/config/collaboration.stub && cp stubs/partykit/worker/*.stub dist/stubs/partykit/worker/ && cp stubs/collaboration/*.stub dist/stubs/collaboration/ && cp stubs/collaboration/migrations/*.stub dist/stubs/collaboration/migrations/",
|
|
78
94
|
"prepack": "pnpm run check:dist",
|
|
79
95
|
"test": "vitest run --passWithNoTests",
|
|
80
96
|
"test:watch": "vitest",
|
|
@@ -84,7 +100,6 @@
|
|
|
84
100
|
"@automerge/automerge": "^3.4.1",
|
|
85
101
|
"@hocuspocus/server": "^4.6.0",
|
|
86
102
|
"@hocuspocus/transformer": "^4.6.0",
|
|
87
|
-
"ioredis": "^5.11.1",
|
|
88
103
|
"ws": "^8.18.0",
|
|
89
104
|
"y-protocols": "^1.0.7",
|
|
90
105
|
"yjs": "^13.6.27"
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Presença cross-instance da colaboração.
|
|
3
|
-
*
|
|
4
|
-
* Mesma filosofia do @adonisjs/transmit: cada instância conhece as conexões
|
|
5
|
-
* LOCAIS (em memória) e propaga eventos via Redis pub/sub pra que qualquer
|
|
6
|
-
* instância enxergue quem está online no documento inteiro — não só na
|
|
7
|
-
* instância que recebeu a conexão.
|
|
8
|
-
*
|
|
9
|
-
* Canal: `collab:presence` (payload identifica o docName).
|
|
10
|
-
* - heartbeat local → publish de join/leave;
|
|
11
|
-
* - subscriber atualiza o mapa agregado e notifica os callbacks.
|
|
12
|
-
*/
|
|
13
|
-
export interface PresenceMember {
|
|
14
|
-
userId: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
avatarUrl?: string | null;
|
|
17
|
-
/** Instância que segura a conexão (id do processo). */
|
|
18
|
-
instanceId: string;
|
|
19
|
-
}
|
|
20
|
-
type PresenceListener = (docName: string, members: PresenceMember[]) => void;
|
|
21
|
-
export declare class PresenceService {
|
|
22
|
-
private publisher?;
|
|
23
|
-
private subscriber?;
|
|
24
|
-
private localMembers;
|
|
25
|
-
private remoteMembers;
|
|
26
|
-
private listeners;
|
|
27
|
-
private instanceId;
|
|
28
|
-
private heartbeatTimer?;
|
|
29
|
-
constructor(redisUrl?: string);
|
|
30
|
-
/** Registra uma conexão local num documento. */
|
|
31
|
-
join(docName: string, member: Omit<PresenceMember, 'instanceId'>): Promise<void>;
|
|
32
|
-
leave(docName: string, userId: string): Promise<void>;
|
|
33
|
-
/** Membros conhecidos de um documento (locais + remotos via pub/sub). */
|
|
34
|
-
list(docName: string): PresenceMember[];
|
|
35
|
-
onPresenceChange(listener: PresenceListener): () => void;
|
|
36
|
-
close(): Promise<void>;
|
|
37
|
-
private localMap;
|
|
38
|
-
private publish;
|
|
39
|
-
private publishLocalHeartbeats;
|
|
40
|
-
private handleRemoteMessage;
|
|
41
|
-
private notify;
|
|
42
|
-
}
|
|
43
|
-
export {};
|
|
44
|
-
//# sourceMappingURL=presence_service.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence_service.d.ts","sourceRoot":"","sources":["../../../src/services/presence_service.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,gBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;AAU7E,qBAAa,eAAe;IAC1B,OAAO,CAAC,SAAS,CAAC,CAAQ;IAC1B,OAAO,CAAC,UAAU,CAAC,CAAQ;IAC3B,OAAO,CAAC,YAAY,CAAkD;IACtE,OAAO,CAAC,aAAa,CAAkD;IACvE,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,cAAc,CAAC,CAAiC;gBAE5C,QAAQ,CAAC,EAAE,MAAM;IAY7B,gDAAgD;IAC1C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3D,yEAAyE;IACzE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE;IAYvC,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAKlD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B,OAAO,CAAC,QAAQ;YAIF,OAAO;YAKP,sBAAsB;IAQpC,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,CAAC,MAAM;CAKf"}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { Redis } from 'ioredis';
|
|
2
|
-
export class PresenceService {
|
|
3
|
-
publisher;
|
|
4
|
-
subscriber;
|
|
5
|
-
localMembers = new Map();
|
|
6
|
-
remoteMembers = new Map();
|
|
7
|
-
listeners = new Set();
|
|
8
|
-
instanceId = `${process.pid}-${randomHex(4)}`;
|
|
9
|
-
heartbeatTimer;
|
|
10
|
-
constructor(redisUrl) {
|
|
11
|
-
if (redisUrl) {
|
|
12
|
-
this.publisher = new Redis(redisUrl);
|
|
13
|
-
this.subscriber = new Redis(redisUrl);
|
|
14
|
-
void this.subscriber.subscribe('collab:presence');
|
|
15
|
-
this.subscriber.on('message', (_channel, message) => {
|
|
16
|
-
this.handleRemoteMessage(JSON.parse(message));
|
|
17
|
-
});
|
|
18
|
-
this.heartbeatTimer = setInterval(() => this.publishLocalHeartbeats(), 30_000);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/** Registra uma conexão local num documento. */
|
|
22
|
-
async join(docName, member) {
|
|
23
|
-
if (!this.localMembers.has(docName)) {
|
|
24
|
-
this.localMembers.set(docName, new Map());
|
|
25
|
-
}
|
|
26
|
-
const full = { ...member, instanceId: this.instanceId };
|
|
27
|
-
this.localMembers.get(docName).set(member.userId, full);
|
|
28
|
-
await this.publish({ kind: 'join', docName, member: full });
|
|
29
|
-
this.notify(docName);
|
|
30
|
-
}
|
|
31
|
-
async leave(docName, userId) {
|
|
32
|
-
const members = this.localMembers.get(docName);
|
|
33
|
-
members?.delete(userId);
|
|
34
|
-
await this.publish({ kind: 'leave', docName, userId, instanceId: this.instanceId });
|
|
35
|
-
this.notify(docName);
|
|
36
|
-
}
|
|
37
|
-
/** Membros conhecidos de um documento (locais + remotos via pub/sub). */
|
|
38
|
-
list(docName) {
|
|
39
|
-
const deduped = new Map();
|
|
40
|
-
for (const member of this.remoteMembers.get(docName)?.values() ?? []) {
|
|
41
|
-
deduped.set(member.userId, member);
|
|
42
|
-
}
|
|
43
|
-
// Local ganha do remoto (é a fonte mais fresca).
|
|
44
|
-
for (const member of this.localMap(docName).values()) {
|
|
45
|
-
deduped.set(member.userId, member);
|
|
46
|
-
}
|
|
47
|
-
return [...deduped.values()];
|
|
48
|
-
}
|
|
49
|
-
onPresenceChange(listener) {
|
|
50
|
-
this.listeners.add(listener);
|
|
51
|
-
return () => this.listeners.delete(listener);
|
|
52
|
-
}
|
|
53
|
-
async close() {
|
|
54
|
-
if (this.heartbeatTimer)
|
|
55
|
-
clearInterval(this.heartbeatTimer);
|
|
56
|
-
if (this.publisher)
|
|
57
|
-
await this.publisher.quit();
|
|
58
|
-
if (this.subscriber)
|
|
59
|
-
await this.subscriber.quit();
|
|
60
|
-
}
|
|
61
|
-
/* ───────────────────────── internals ───────────────────────── */
|
|
62
|
-
localMap(docName) {
|
|
63
|
-
return this.localMembers.get(docName) ?? new Map();
|
|
64
|
-
}
|
|
65
|
-
async publish(message) {
|
|
66
|
-
if (!this.publisher)
|
|
67
|
-
return;
|
|
68
|
-
await this.publisher.publish('collab:presence', JSON.stringify(message));
|
|
69
|
-
}
|
|
70
|
-
async publishLocalHeartbeats() {
|
|
71
|
-
for (const [docName, members] of this.localMembers) {
|
|
72
|
-
for (const member of members.values()) {
|
|
73
|
-
await this.publish({ kind: 'join', docName, member });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
handleRemoteMessage(message) {
|
|
78
|
-
if (!this.remoteMembers.has(message.docName)) {
|
|
79
|
-
this.remoteMembers.set(message.docName, new Map());
|
|
80
|
-
}
|
|
81
|
-
const byUser = this.remoteMembers.get(message.docName);
|
|
82
|
-
if (message.kind === 'join' && message.member) {
|
|
83
|
-
byUser.set(message.member.userId, message.member);
|
|
84
|
-
}
|
|
85
|
-
else if (message.kind === 'leave' && message.userId) {
|
|
86
|
-
// Só remove se veio da mesma instância que registrou.
|
|
87
|
-
const current = byUser.get(message.userId);
|
|
88
|
-
if (!current || current.instanceId === message.instanceId) {
|
|
89
|
-
byUser.delete(message.userId);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
this.notify(message.docName);
|
|
93
|
-
}
|
|
94
|
-
notify(docName) {
|
|
95
|
-
for (const listener of this.listeners) {
|
|
96
|
-
listener(docName, this.list(docName));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function randomHex(bytes) {
|
|
101
|
-
return Array.from({ length: bytes }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
102
|
-
}
|
|
103
|
-
//# sourceMappingURL=presence_service.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence_service.js","sourceRoot":"","sources":["../../../src/services/presence_service.ts"],"names":[],"mappings":"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"}
|