@kerebron/extension-server-hono 0.3.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Permission is hereby granted, free of charge, to any
2
+ person obtaining a copy of this software and associated
3
+ documentation files (the "Software"), to deal in the
4
+ Software without restriction, including without
5
+ limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software
8
+ is furnished to do so, subject to the following
9
+ conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ shall be included in all copies or substantial portions
13
+ of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23
+ DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Kerebron - Prosemirror based online editor kit
2
+
3
+ ## Watch a Demo
4
+
5
+ <a href="https://youtube.com/shorts/OdJjhAPj-wA?feature=share" target="_blank">
6
+ <img src="https://github.com/user-attachments/assets/b63ec84a-0ed2-4f98-920c-76f6d3215168" alt="Alt Text" width="200">
7
+ </a>
8
+
9
+ ## Playground Demo
10
+
11
+ [playground](https://editor-test.gitgis.com) - be nice.
12
+
13
+ ## Overview
14
+
15
+ Using vanilla Prosemirror modules is often impossible because of
16
+ incompatibilities.
17
+
18
+ Kerebron forks several prosemirror projects into one monorepo in order to keep
19
+ them in sync.
20
+
21
+ Project is inspired on https://tiptap.dev/, but instead of building wrapper
22
+ around a wrapper it borrows concept of extension and command manager.
23
+
24
+ It has simplified tooling (deno), fewer dependencies and resulting in lower
25
+ number of output npm modules.
26
+
27
+ **Work in progress**
28
+
29
+ ## Development
30
+
31
+ To start example server:
32
+
33
+ ```
34
+ deno task -f example-server-hono start
35
+ ```
36
+
37
+ ## Build
38
+
39
+ ### Build static examples
40
+
41
+ ```shell
42
+ deno task -r build
43
+ ```
44
+
45
+ ### NPM packages are generated using DNT
46
+
47
+ - https://deno.com/blog/publish-esm-cjs-module-dnt - the easiest way to publish
48
+ a hybrid npm module for ESM and CommonJS
49
+ - https://github.com/denoland/dnt
50
+ - https://gaubee.com/article/Publishing-Your-Deno-Project-as-a-Monorepo-using-dnt/
51
+
52
+ To generate npm packages
53
+
54
+ ```shell
55
+ deno -A ./build/build_npm.ts
56
+ ```
57
+
58
+ ## Run through docker
59
+
60
+ ```
61
+ docker build . -t editor-test
62
+ docker run -it -p 8000:8000 -v $PWD:/usr/src/app editor-test
63
+ ```
@@ -0,0 +1,41 @@
1
+ import { WSEvents } from 'hono/ws';
2
+ import * as Y from 'yjs';
3
+ import * as awarenessProtocol from 'y-protocols/awareness';
4
+ import { HonoWsAdapter } from './mod.js';
5
+ export declare const messageType: {
6
+ sync: number;
7
+ awareness: number;
8
+ };
9
+ export declare const docs: Map<string, Y.Doc>;
10
+ /**
11
+ * Gets a Y.Doc by name, whether in memory or on disk
12
+ *
13
+ * @param {string} docname - the name of the Y.Doc to find or create
14
+ * @param {boolean} gc - whether to allow gc on the doc (applies only when created)
15
+ * @return {WSSharedDoc}
16
+ */
17
+ export declare const getYDoc: (docname: string, gc?: boolean) => Y.Doc;
18
+ export declare class SocketContext {
19
+ readonly room: Room;
20
+ readonly socket: WebSocket;
21
+ readonly controlledIds: Set<number>;
22
+ constructor(room: Room, socket: WebSocket);
23
+ }
24
+ export declare class Room {
25
+ readonly roomName: string;
26
+ readonly doc: Y.Doc;
27
+ readonly socketContexts: Map<WebSocket, SocketContext>;
28
+ awareness: awarenessProtocol.Awareness;
29
+ constructor(roomName: string, doc: Y.Doc);
30
+ }
31
+ export declare class HonoYjsMemAdapter implements HonoWsAdapter {
32
+ #private;
33
+ readonly sockets: Map<WebSocket, SocketContext>;
34
+ readonly rooms: Map<string, Room>;
35
+ constructor();
36
+ getRoomNames(): Promise<string[]>;
37
+ upgradeWebSocket(roomName: string): WSEvents<WebSocket>;
38
+ send(conn: WebSocket, m: Uint8Array): void;
39
+ receiveMessage(messageBytes: Uint8Array, conn: WebSocket): void;
40
+ }
41
+ //# sourceMappingURL=HonoYjsMemAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HonoYjsMemAdapter.d.ts","sourceRoot":"","sources":["../src/HonoYjsMemAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAI3D,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,eAAO,MAAM,WAAW;;;CAGvB,CAAC;AAEF,eAAO,MAAM,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAa,CAAC;AAIlD;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,MAAM,EAAE,YAAS,KAAG,CAAC,CAAC,GAWtD,CAAC;AAEF,qBAAa,aAAa;aAGI,IAAI,EAAE,IAAI;aAAkB,MAAM,EAAE,SAAS;IAFzE,SAAgB,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;gBAE3B,IAAI,EAAE,IAAI,EAAkB,MAAM,EAAE,SAAS;CAE1E;AAED,qBAAa,IAAI;aAIa,QAAQ,EAAE,MAAM;aAAkB,GAAG,EAAE,CAAC,CAAC,GAAG;IAHxE,SAAgB,cAAc,EAAE,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAa;IAC1E,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;gBAEX,QAAQ,EAAE,MAAM,EAAkB,GAAG,EAAE,CAAC,CAAC,GAAG;CAuDzE;AAID,qBAAa,iBAAkB,YAAW,aAAa;;IACrD,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAa;IAC5D,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;IAM5B,YAAY;IAIlB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC;IA0DvD,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU;IAUnC,cAAc,CAAC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS;CA+DzD"}
@@ -0,0 +1,241 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _HonoYjsMemAdapter_instances, _HonoYjsMemAdapter_terminate, _HonoYjsMemAdapter_removeSocket;
7
+ import * as Y from 'yjs';
8
+ import * as syncProtocol from 'y-protocols/sync';
9
+ import * as awarenessProtocol from 'y-protocols/awareness';
10
+ import * as encoding from 'lib0/encoding';
11
+ import * as decoding from 'lib0/decoding';
12
+ export const messageType = {
13
+ sync: 0,
14
+ awareness: 1,
15
+ };
16
+ export const docs = new Map();
17
+ const gcEnabled = false;
18
+ /**
19
+ * Gets a Y.Doc by name, whether in memory or on disk
20
+ *
21
+ * @param {string} docname - the name of the Y.Doc to find or create
22
+ * @param {boolean} gc - whether to allow gc on the doc (applies only when created)
23
+ * @return {WSSharedDoc}
24
+ */
25
+ export const getYDoc = (docname, gc = true) => {
26
+ if (docs.has(docname)) {
27
+ return docs.get(docname);
28
+ }
29
+ const doc = new Y.Doc({ gc: gcEnabled });
30
+ doc.gc = gc;
31
+ // if (persistence !== null) {
32
+ // persistence.bindState(docname, doc)
33
+ // }
34
+ docs.set(docname, doc);
35
+ return doc;
36
+ };
37
+ export class SocketContext {
38
+ constructor(room, socket) {
39
+ Object.defineProperty(this, "room", {
40
+ enumerable: true,
41
+ configurable: true,
42
+ writable: true,
43
+ value: room
44
+ });
45
+ Object.defineProperty(this, "socket", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: socket
50
+ });
51
+ Object.defineProperty(this, "controlledIds", {
52
+ enumerable: true,
53
+ configurable: true,
54
+ writable: true,
55
+ value: new Set()
56
+ });
57
+ }
58
+ }
59
+ export class Room {
60
+ constructor(roomName, doc) {
61
+ Object.defineProperty(this, "roomName", {
62
+ enumerable: true,
63
+ configurable: true,
64
+ writable: true,
65
+ value: roomName
66
+ });
67
+ Object.defineProperty(this, "doc", {
68
+ enumerable: true,
69
+ configurable: true,
70
+ writable: true,
71
+ value: doc
72
+ });
73
+ Object.defineProperty(this, "socketContexts", {
74
+ enumerable: true,
75
+ configurable: true,
76
+ writable: true,
77
+ value: new Map()
78
+ });
79
+ Object.defineProperty(this, "awareness", {
80
+ enumerable: true,
81
+ configurable: true,
82
+ writable: true,
83
+ value: void 0
84
+ });
85
+ this.awareness = new awarenessProtocol.Awareness(doc);
86
+ this.awareness.setLocalState(null);
87
+ this.awareness.on('update', ({ added, updated, removed }, conn) => {
88
+ const changedClients = added.concat(updated, removed);
89
+ if (conn !== null) {
90
+ const socketContext = this.socketContexts.get(conn);
91
+ if (socketContext) {
92
+ added.forEach((clientID) => {
93
+ socketContext.controlledIds.add(clientID);
94
+ });
95
+ removed.forEach((clientID) => {
96
+ socketContext.controlledIds.delete(clientID);
97
+ });
98
+ }
99
+ }
100
+ // broadcast awareness update
101
+ const encoder = encoding.createEncoder();
102
+ encoding.writeVarUint(encoder, messageType.awareness);
103
+ encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
104
+ const buff = encoding.toUint8Array(encoder);
105
+ for (const socket of this.socketContexts.keys()) {
106
+ socket.send(buff);
107
+ }
108
+ });
109
+ doc.on('update', (update) => {
110
+ const encoder = encoding.createEncoder();
111
+ encoding.writeVarUint(encoder, messageType.sync);
112
+ syncProtocol.writeUpdate(encoder, update);
113
+ const message = encoding.toUint8Array(encoder);
114
+ for (const socket of this.socketContexts.keys()) {
115
+ socket.send(message);
116
+ }
117
+ });
118
+ }
119
+ }
120
+ const rooms = new Map();
121
+ export class HonoYjsMemAdapter {
122
+ constructor() {
123
+ _HonoYjsMemAdapter_instances.add(this);
124
+ Object.defineProperty(this, "sockets", {
125
+ enumerable: true,
126
+ configurable: true,
127
+ writable: true,
128
+ value: new Map()
129
+ });
130
+ Object.defineProperty(this, "rooms", {
131
+ enumerable: true,
132
+ configurable: true,
133
+ writable: true,
134
+ value: void 0
135
+ });
136
+ this.rooms = rooms;
137
+ }
138
+ async getRoomNames() {
139
+ return Array.from(rooms.keys());
140
+ }
141
+ upgradeWebSocket(roomName) {
142
+ const doc = getYDoc(roomName, gcEnabled);
143
+ return {
144
+ onOpen: (evt, wsContext) => {
145
+ if (!wsContext.raw) {
146
+ return;
147
+ }
148
+ if (!this.rooms.has(roomName)) {
149
+ this.rooms.set(roomName, new Room(roomName, doc));
150
+ }
151
+ const room = this.rooms.get(roomName);
152
+ const socketContext = new SocketContext(room, wsContext.raw);
153
+ this.sockets.set(wsContext.raw, socketContext);
154
+ room.socketContexts.set(wsContext.raw, socketContext);
155
+ const encoder = encoding.createEncoder();
156
+ encoding.writeVarUint(encoder, messageType.sync);
157
+ syncProtocol.writeSyncStep1(encoder, doc);
158
+ this.send(wsContext.raw, encoding.toUint8Array(encoder));
159
+ const awarenessStates = room.awareness.getStates();
160
+ if (awarenessStates.size > 0) {
161
+ const encoder = encoding.createEncoder();
162
+ encoding.writeVarUint(encoder, messageType.awareness);
163
+ encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(room.awareness, Array.from(awarenessStates.keys())));
164
+ this.send(wsContext.raw, encoding.toUint8Array(encoder));
165
+ }
166
+ // this.#forceReady();
167
+ },
168
+ onError: (error, wsContext) => {
169
+ console.error('onError', error);
170
+ },
171
+ onMessage: (message, wsContext) => {
172
+ if (!wsContext.raw) {
173
+ return;
174
+ }
175
+ this.receiveMessage(new Uint8Array(message.data), wsContext.raw);
176
+ },
177
+ onClose: (event, wsContext) => {
178
+ if (!wsContext.raw) {
179
+ return;
180
+ }
181
+ __classPrivateFieldGet(this, _HonoYjsMemAdapter_instances, "m", _HonoYjsMemAdapter_removeSocket).call(this, wsContext.raw);
182
+ },
183
+ };
184
+ }
185
+ send(conn, m) {
186
+ if (conn.readyState !== WebSocket.CONNECTING &&
187
+ conn.readyState !== WebSocket.OPEN) {
188
+ __classPrivateFieldGet(this, _HonoYjsMemAdapter_instances, "m", _HonoYjsMemAdapter_terminate).call(this, conn);
189
+ }
190
+ conn.send(m);
191
+ }
192
+ receiveMessage(messageBytes, conn) {
193
+ try {
194
+ const socketContext = this.sockets.get(conn);
195
+ if (!socketContext) {
196
+ return;
197
+ }
198
+ const room = socketContext.room;
199
+ const encoder = encoding.createEncoder();
200
+ const decoder = decoding.createDecoder(messageBytes);
201
+ const type = decoding.readVarUint(decoder);
202
+ switch (type) {
203
+ case messageType.sync:
204
+ encoding.writeVarUint(encoder, messageType.sync);
205
+ syncProtocol.readSyncMessage(decoder, encoder, room.doc, conn);
206
+ // If the `encoder` only contains the type of reply message and no
207
+ // message, there is no need to send the message. When `encoder` only
208
+ // contains the type of reply, its length is 1.
209
+ if (encoding.length(encoder) > 1) {
210
+ this.send(conn, encoding.toUint8Array(encoder));
211
+ }
212
+ break;
213
+ case messageType.awareness: {
214
+ awarenessProtocol.applyAwarenessUpdate(room.awareness, decoding.readVarUint8Array(decoder), conn);
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ catch (err) {
220
+ console.error(err);
221
+ // @ts-ignore
222
+ doc.emit('error', [err]);
223
+ }
224
+ }
225
+ }
226
+ _HonoYjsMemAdapter_instances = new WeakSet(), _HonoYjsMemAdapter_terminate = function _HonoYjsMemAdapter_terminate(socket) {
227
+ __classPrivateFieldGet(this, _HonoYjsMemAdapter_instances, "m", _HonoYjsMemAdapter_removeSocket).call(this, socket);
228
+ socket.close();
229
+ }, _HonoYjsMemAdapter_removeSocket = function _HonoYjsMemAdapter_removeSocket(conn) {
230
+ const socketContext = this.sockets.get(conn);
231
+ if (socketContext) {
232
+ const room = socketContext.room;
233
+ room.socketContexts.delete(socketContext.socket);
234
+ awarenessProtocol.removeAwarenessStates(room.awareness, Array.from(socketContext.controlledIds), null);
235
+ if (room.socketContexts.size === 0) {
236
+ // TODO save to persistence
237
+ // TODO Destroy room
238
+ }
239
+ this.sockets.delete(conn);
240
+ }
241
+ };
package/esm/mod.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { WSEvents } from 'hono/ws';
2
+ export interface HonoWsAdapter {
3
+ upgradeWebSocket(roomName: string): WSEvents<WebSocket>;
4
+ }
5
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,MAAM,WAAW,aAAa;IAC5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;CACzD"}
package/esm/mod.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@kerebron/extension-server-hono",
3
+ "version": "0.3.1",
4
+ "license": "MIT",
5
+ "module": "./esm/mod.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./esm/mod.js"
9
+ },
10
+ "./HonoYjsMemAdapter": {
11
+ "import": "./esm/HonoYjsMemAdapter.js"
12
+ }
13
+ },
14
+ "scripts": {},
15
+ "dependencies": {
16
+ "yjs": "13.6.24"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.9.0"
20
+ },
21
+ "_generatedBy": "dnt@dev"
22
+ }