Skip to content

Commit 2aeb83e

Browse files
authoredMay 28, 2024··
feat: switch to ystream persistence (#4)
* feat: add basic ystream setup * chore: update upstream * chore: bump ystream * refactor: bump api * feat: switch persistence
1 parent 8601bf9 commit 2aeb83e

File tree

9 files changed

+337
-58
lines changed

9 files changed

+337
-58
lines changed
 

‎.vscode/launch.template.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"skipFiles": ["<node_internals>/**"],
99
"env": {
1010
"NODE_OPTIONS": "--import=./register.js",
11-
"INSTANCE_NAME": "",
11+
"INSTANCE_NAME": "demo",
1212
"PORT": "3000"
1313
},
1414
"program": "${workspaceFolder}/packages/server/src/index.ts",

‎packages/server/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
"type": "module",
66
"scripts": {
77
"start": "NODE_OPTIONS=--import=./register.js node src/index.ts",
8-
"clean": "rm -rf ./.db*"
8+
"clean": "rm -rf ./.db* db*.json"
99
},
1010
"dependencies": {
1111
"@notesuite/common": "workspace:*",
12+
"@y/stream": "^0.0.3",
1213
"body-parser": "^1.20.2",
1314
"cors": "^2.8.5",
1415
"express": "^4.19.2",
@@ -19,7 +20,6 @@
1920
"ts-node": "^10.9.2",
2021
"webdav-server": "^2.6.2",
2122
"ws": "^8.16.0",
22-
"y-leveldb": "^0.1.2",
2323
"y-protocols": "^1.0.6",
2424
"y-websocket": "^2.0.2",
2525
"yjs": "^13.5.6"

‎packages/server/src/api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Y from 'yjs';
22
// @ts-ignore
3-
import { getYDoc } from './third-party/y-websocket.js';
3+
import { getYDoc } from './ystream/adaptor.js';
44
import type { AppContext } from './utils.js';
55
// import { exportSnapshot } from '@notesuite/common/dist/editor.js';
66

‎packages/server/src/third-party/y-websocket.js

+6-40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as encoding from 'lib0/encoding'
77
import * as decoding from 'lib0/decoding'
88
import * as map from 'lib0/map'
99
import * as lodash from 'lodash'
10+
import { getYDoc as getYstreamDoc } from '../ystream/adaptor.js'
1011

1112
const { debounce } = lodash
1213

@@ -25,44 +26,8 @@ const wsReadyStateClosed = 3 // eslint-disable-line
2526

2627
// disable gc when using snapshots!
2728
const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'
28-
const persistenceDir = `./.db-${process.env.INSTANCE_NAME || 'default'}`
29-
/**
30-
* @type {{bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise<any>, provider: any}|null}
31-
*/
32-
let persistence = null
33-
if (typeof persistenceDir === 'string') {
34-
console.info('Persisting documents to "' + persistenceDir + '"')
35-
// @ts-ignore
36-
const { LeveldbPersistence } = await import('y-leveldb')
37-
const ldb = new LeveldbPersistence(persistenceDir)
38-
persistence = {
39-
provider: ldb,
40-
bindState: async (docName, ydoc) => {
41-
const persistedYdoc = await ldb.getYDoc(docName)
42-
const newUpdates = Y.encodeStateAsUpdate(ydoc)
43-
ldb.storeUpdate(docName, newUpdates)
44-
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc))
45-
ydoc.on('update', update => {
46-
ldb.storeUpdate(docName, update)
47-
})
48-
},
49-
writeState: async (_docName, _ydoc) => {}
50-
}
51-
}
52-
53-
/**
54-
* @param {{bindState: function(string,WSSharedDoc):void,
55-
* writeState:function(string,WSSharedDoc):Promise<any>,provider:any}|null} persistence_
56-
*/
57-
export const setPersistence = persistence_ => {
58-
persistence = persistence_
59-
}
6029

61-
/**
62-
* @return {null|{bindState: function(string,WSSharedDoc):void,
63-
* writeState:function(string,WSSharedDoc):Promise<any>}|null} used persistence layer
64-
*/
65-
export const getPersistence = () => persistence
30+
let persistence = null
6631

6732
/**
6833
* @type {Map<string,WSSharedDoc>}
@@ -167,10 +132,11 @@ export const getYDoc = async (docname, gc = true) => {
167132
if (!doc) {
168133
doc = new WSSharedDoc(docname)
169134
doc.gc = gc
170-
if (persistence !== null) {
171-
await persistence.bindState(docname, doc)
172-
}
173135
docs.set(docname, doc)
136+
137+
const peerDoc = await getYstreamDoc(docname)
138+
peerDoc.on('update', update => Y.applyUpdate(doc, update))
139+
doc.on('update', update => Y.applyUpdate(peerDoc, update))
174140
}
175141
return doc;
176142
}

‎packages/server/src/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { JSONFilePreset } from 'lowdb/node';
66
import type http from 'http';
77
import {
88
setupWSConnection,
9-
getYDoc,
109
docs as serverDocs,
1110
// @ts-ignore
1211
} from './third-party/y-websocket.js';
1312
import * as Y from 'yjs';
13+
import { getYDoc } from './ystream/adaptor.js';
1414

1515
const instanceName = process.env.INSTANCE_NAME || 'default';
1616
const docs = serverDocs as Map<string, Y.Doc>;
@@ -44,7 +44,7 @@ async function initDB() {
4444
activeWorkspaceId?: string;
4545
workspaces: { id: string; rootId: string; name: string }[];
4646
} = { workspaces: [] };
47-
const db = await JSONFilePreset(`./.db-${instanceName}/db.json`, defaultData);
47+
const db = await JSONFilePreset(`./db-${instanceName}.json`, defaultData);
4848
await db.read();
4949
await db.write();
5050
return db;
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as Ystream from '@y/stream';
2+
import * as wscomm from '@y/stream/comms/websocket';
3+
import * as authentication from '@y/stream/api/authentication';
4+
import { createWSServer } from '@y/stream/comms/websocket-server';
5+
import {
6+
collectionDef,
7+
testServerIdentity,
8+
testUser,
9+
} from './auth.js';
10+
11+
const dbname = `./.db-ystream-${process.env.INSTANCE_NAME}`;
12+
13+
const remoteServer = await createWSServer({
14+
port: 9000,
15+
dbname,
16+
identity: testServerIdentity,
17+
});
18+
19+
// const comm = new wscomm.WebSocketComm('ws://localhost:9000', collectionDef);
20+
// await Ystream.remove(dbname);
21+
const ystream = await Ystream.open(dbname, {
22+
// comms: [comm],
23+
});
24+
await authentication.registerUser(ystream, testServerIdentity.user, {
25+
isTrusted: true,
26+
});
27+
await authentication.setUserIdentity(
28+
ystream,
29+
testUser.user,
30+
await testUser.user.publicKey,
31+
testUser.privateKey
32+
);
33+
34+
const { owner, name } = collectionDef;
35+
const collection = ystream.getCollection(owner, name) as Ystream.Collection;
36+
37+
38+
export async function getYDoc(id: string) {
39+
const ydoc = collection.getYdoc(id);
40+
await ydoc.whenLoaded;
41+
return ydoc;
42+
}

‎packages/server/src/ystream/auth.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as dbtypes from '@y/stream/api/dbtypes';
2+
import * as json from 'lib0/json';
3+
import * as buffer from 'lib0/buffer';
4+
import * as decoding from 'lib0/decoding';
5+
import * as ecdsa from 'lib0/crypto/ecdsa';
6+
7+
const testUserRaw = {
8+
privateKey:
9+
'{"key_ops":["sign"],"ext":true,"kty":"EC","x":"pAUmLYc-UFmPIt7leafPTbhxQyygcaW7__nPcUNCuu0wH27yS9P_pWFP1GwcsoAN","y":"u3109KjrPGsNUn2k5Whn2uHLAckQPdLNqtM4GpBEpUJwlvVDvk71-lS3YOEYJ_Sq","crv":"P-384","d":"OHnRw5an9hlSqSKg966lFRvB7dow669pVSn7sFZUi7UQh_Y9Xc95SQ6pEWsofsYD"}',
10+
user: 'AMgBeyJrZXlfb3BzIjpbInZlcmlmeSJdLCJleHQiOnRydWUsImt0eSI6IkVDIiwieCI6InBBVW1MWWMtVUZtUEl0N2xlYWZQVGJoeFF5eWdjYVc3X19uUGNVTkN1dTB3SDI3eVM5UF9wV0ZQMUd3Y3NvQU4iLCJ5IjoidTMxMDlLanJQR3NOVW4yazVXaG4ydUhMQWNrUVBkTE5xdE00R3BCRXBVSndsdlZEdms3MS1sUzNZT0VZSl9TcSIsImNydiI6IlAtMzg0In0=',
11+
};
12+
13+
const testServerIdentityRaw = {
14+
privateKey:
15+
'{"key_ops":["sign"],"ext":true,"kty":"EC","x":"CYwMakpn0onaNeCa-wqLn4Fzsris_UY4Z5gRQUA9xQOoh94YG9OHhItr6rovaYpZ","y":"74Ulju86IUMJZsYsSjxSjusLjj9U6rozZwbK9Xaqj3MgIWtnjNyjL1D-NzOP3FJ7","crv":"P-384","d":"-yKNOty9EshGL0yAOQ2q6c_b_PNCpeEK9FVPoB0wc9EUyt9BR4DZuqrC9t_DgNaF"}',
16+
user: 'AMgBeyJrZXlfb3BzIjpbInZlcmlmeSJdLCJleHQiOnRydWUsImt0eSI6IkVDIiwieCI6IkNZd01ha3BuMG9uYU5lQ2Etd3FMbjRGenNyaXNfVVk0WjVnUlFVQTl4UU9vaDk0WUc5T0hoSXRyNnJvdmFZcFoiLCJ5IjoiNzRVbGp1ODZJVU1KWnNZc1NqeFNqdXNMamo5VTZyb3pad2JLOVhhcWozTWdJV3Ruak55akwxRC1Oek9QM0ZKNyIsImNydiI6IlAtMzg0In0=',
17+
};
18+
19+
export const testUser = {
20+
privateKey: await ecdsa.importKeyJwk(json.parse(testUserRaw.privateKey)),
21+
user: dbtypes.UserIdentity.decode(
22+
decoding.createDecoder(buffer.fromBase64(testUserRaw.user))
23+
),
24+
};
25+
26+
export const testServerIdentity = {
27+
privateKey: await ecdsa.importKeyJwk(
28+
json.parse(testServerIdentityRaw.privateKey)
29+
),
30+
user: dbtypes.UserIdentity.decode(
31+
decoding.createDecoder(buffer.fromBase64(testServerIdentityRaw.user))
32+
),
33+
};
34+
35+
export const owner = testUser.user.hash;
36+
export const collectionDef = {
37+
owner: buffer.toBase64(owner),
38+
name: 'c1',
39+
};

‎pnpm-lock.yaml

+242-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/common/demo.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ test('demo', async ({ page }) => {
99
backendPort: 3000,
1010
});
1111
await page.goto(agent.web.baseUrl);
12-
// await page.pause();
12+
await page.pause();
1313

1414
await agent.web.createWorkspace(page, 'hello');
1515
await agent.web.createDoc(page, 'First Doc');
1616
await agent.web.createDoc(page, 'Second Doc');
1717
await agent.web.createDoc(page, 'Third Doc');
18-
// await page.pause();
18+
await page.pause();
1919
await agent.stop();
2020
});

0 commit comments

Comments
 (0)
Please sign in to comment.