jazz-run 0.8.11 → 0.8.13
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +19 -0
- package/dist/createWorkerAccount.js +80 -0
- package/dist/createWorkerAccount.js.map +1 -0
- package/dist/index.js +36 -6
- package/dist/index.js.map +1 -1
- package/dist/startSyncServer.js +60 -0
- package/dist/startSyncServer.js.map +1 -0
- package/dist/test/createWorkerAccount.test.js +25 -0
- package/dist/test/createWorkerAccount.test.js.map +1 -0
- package/package.json +5 -5
- package/src/createWorkerAccount.ts +114 -0
- package/src/index.ts +74 -6
- package/src/startSyncServer.ts +95 -0
- package/src/test/createWorkerAccount.test.ts +32 -0
- package/dist/accountCreate.js +0 -88
- package/dist/accountCreate.js.map +0 -1
- package/dist/startSync.js +0 -76
- package/dist/startSync.js.map +0 -1
- package/src/accountCreate.ts +0 -125
- package/src/startSync.ts +0 -125
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# jazz-run
|
2
2
|
|
3
|
+
## 0.8.13
|
4
|
+
|
5
|
+
### Patch Changes
|
6
|
+
|
7
|
+
- Updated dependencies [fd011d7]
|
8
|
+
- Updated dependencies [e0dd006]
|
9
|
+
- jazz-tools@0.8.13
|
10
|
+
- cojson-transport-ws@0.8.13
|
11
|
+
|
12
|
+
## 0.8.12
|
13
|
+
|
14
|
+
### Patch Changes
|
15
|
+
|
16
|
+
- Updated dependencies [6ed75eb]
|
17
|
+
- cojson-storage-sqlite@0.8.12
|
18
|
+
- cojson@0.8.12
|
19
|
+
- cojson-transport-ws@0.8.12
|
20
|
+
- jazz-tools@0.8.12
|
21
|
+
|
3
22
|
## 0.8.11
|
4
23
|
|
5
24
|
### Patch Changes
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import { createWebSocketPeer } from "cojson-transport-ws";
|
2
|
+
import { WebSocket } from "ws";
|
3
|
+
import { Account, WasmCrypto, createJazzContext, isControlledAccount, } from "jazz-tools";
|
4
|
+
import { fixedCredentialsAuth, randomSessionProvider } from "jazz-tools";
|
5
|
+
export const createWorkerAccount = async ({ name, peer: peerAddr, }) => {
|
6
|
+
const crypto = await WasmCrypto.create();
|
7
|
+
const peer = createWebSocketPeer({
|
8
|
+
id: "upstream",
|
9
|
+
websocket: new WebSocket(peerAddr),
|
10
|
+
role: "server",
|
11
|
+
});
|
12
|
+
const account = await Account.create({
|
13
|
+
creationProps: { name },
|
14
|
+
peersToLoadFrom: [peer],
|
15
|
+
crypto,
|
16
|
+
});
|
17
|
+
if (!isControlledAccount(account)) {
|
18
|
+
throw new Error("account is not a controlled account");
|
19
|
+
}
|
20
|
+
const accountCoValue = account._raw.core;
|
21
|
+
const accountProfileCoValue = account.profile._raw.core;
|
22
|
+
const syncManager = account._raw.core.node.syncManager;
|
23
|
+
await Promise.all([
|
24
|
+
syncManager.syncCoValue(accountCoValue),
|
25
|
+
syncManager.syncCoValue(accountProfileCoValue),
|
26
|
+
]);
|
27
|
+
await Promise.all([
|
28
|
+
waitForSync(account, peer, accountCoValue),
|
29
|
+
waitForSync(account, peer, accountProfileCoValue),
|
30
|
+
]);
|
31
|
+
// Spawn a second peer to double check that the account is fully synced
|
32
|
+
const peer2 = createWebSocketPeer({
|
33
|
+
id: "upstream2",
|
34
|
+
websocket: new WebSocket(peerAddr),
|
35
|
+
role: "server",
|
36
|
+
});
|
37
|
+
await createJazzContext({
|
38
|
+
auth: fixedCredentialsAuth({
|
39
|
+
accountID: account.id,
|
40
|
+
secret: account._raw.agentSecret,
|
41
|
+
}),
|
42
|
+
sessionProvider: randomSessionProvider,
|
43
|
+
peersToLoadFrom: [peer2],
|
44
|
+
crypto,
|
45
|
+
});
|
46
|
+
return {
|
47
|
+
accountId: account.id,
|
48
|
+
agentSecret: account._raw.agentSecret,
|
49
|
+
};
|
50
|
+
};
|
51
|
+
function waitForSync(account, peer, coValue) {
|
52
|
+
const syncManager = account._raw.core.node.syncManager;
|
53
|
+
const peerState = syncManager.peers[peer.id];
|
54
|
+
return new Promise((resolve) => {
|
55
|
+
const unsubscribe = peerState?.optimisticKnownStates.subscribe((id, peerKnownState) => {
|
56
|
+
if (id !== coValue.id)
|
57
|
+
return;
|
58
|
+
const knownState = coValue.knownState();
|
59
|
+
const synced = isEqualSession(knownState.sessions, peerKnownState.sessions);
|
60
|
+
if (synced) {
|
61
|
+
resolve(true);
|
62
|
+
unsubscribe?.();
|
63
|
+
}
|
64
|
+
});
|
65
|
+
});
|
66
|
+
}
|
67
|
+
function isEqualSession(a, b) {
|
68
|
+
const keysA = Object.keys(a);
|
69
|
+
const keysB = Object.keys(b);
|
70
|
+
if (keysA.length !== keysB.length) {
|
71
|
+
return false;
|
72
|
+
}
|
73
|
+
for (const sessionId of keysA) {
|
74
|
+
if (a[sessionId] !== b[sessionId]) {
|
75
|
+
return false;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
return true;
|
79
|
+
}
|
80
|
+
//# sourceMappingURL=createWorkerAccount.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"createWorkerAccount.js","sourceRoot":"","sources":["../src/createWorkerAccount.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
@@ -1,15 +1,45 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
2
|
/* istanbul ignore file -- @preserve */
|
3
|
-
import { Command } from "@effect/cli";
|
3
|
+
import { Command, Options } from "@effect/cli";
|
4
4
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
5
|
-
import { Effect } from "effect";
|
6
|
-
import {
|
7
|
-
import {
|
5
|
+
import { Console, Effect } from "effect";
|
6
|
+
import { createWorkerAccount } from "./createWorkerAccount.js";
|
7
|
+
import { startSyncServer } from "./startSyncServer.js";
|
8
8
|
const jazzTools = Command.make("jazz-tools");
|
9
|
-
const
|
9
|
+
const nameOption = Options.text("name").pipe(Options.withAlias("n"));
|
10
|
+
const peerOption = Options.text("peer")
|
11
|
+
.pipe(Options.withAlias("p"))
|
12
|
+
.pipe(Options.withDefault("wss://cloud.jazz.tools"));
|
13
|
+
const createAccountCommand = Command.make("create", { name: nameOption, peer: peerOption }, ({ name, peer }) => {
|
14
|
+
return Effect.gen(function* () {
|
15
|
+
const { accountId, agentSecret } = yield* Effect.promise(() => createWorkerAccount({ name, peer }));
|
16
|
+
yield* Console.log(`# Credentials for Jazz account "${name}":
|
17
|
+
JAZZ_WORKER_ACCOUNT=${accountId}
|
18
|
+
JAZZ_WORKER_SECRET=${agentSecret}
|
19
|
+
`);
|
20
|
+
});
|
21
|
+
});
|
22
|
+
const accountCommand = Command.make("account").pipe(Command.withSubcommands([createAccountCommand]));
|
23
|
+
const portOption = Options.text("port")
|
24
|
+
.pipe(Options.withAlias("p"))
|
25
|
+
.pipe(Options.withDescription("Select a different port for the WebSocket server. Default is 4200"))
|
26
|
+
.pipe(Options.withDefault("4200"));
|
27
|
+
const inMemoryOption = Options.boolean("in-memory").pipe(Options.withDescription("Use an in-memory storage instead of file-based"));
|
28
|
+
const dbOption = Options.file("db")
|
29
|
+
.pipe(Options.withDescription("The path to the file where to store the data. Default is 'sync-db/storage.db'"))
|
30
|
+
.pipe(Options.withDefault("sync-db/storage.db"));
|
31
|
+
export const startSyncServerCommand = Command.make("sync", { port: portOption, inMemory: inMemoryOption, db: dbOption }, ({ port, inMemory, db }) => {
|
32
|
+
return Effect.gen(function* () {
|
33
|
+
yield* Effect.promise(() => startSyncServer({ port, inMemory, db }));
|
34
|
+
Console.log(`COJSON sync server listening on ws://127.0.0.1:${port}`);
|
35
|
+
// Keep the server up
|
36
|
+
yield* Effect.never;
|
37
|
+
});
|
38
|
+
});
|
39
|
+
const command = jazzTools.pipe(Command.withSubcommands([accountCommand, startSyncServerCommand]));
|
10
40
|
const cli = Command.run(command, {
|
11
41
|
name: "Jazz CLI Tools",
|
12
|
-
version: "v0.
|
42
|
+
version: "v0.8.11",
|
13
43
|
});
|
14
44
|
Effect.suspend(() => cli(process.argv)).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
15
45
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,uCAAuC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { ControlledAgent, LocalNode, WasmCrypto } from "cojson";
|
2
|
+
import { WebSocketServer } from "ws";
|
3
|
+
import { createServer } from "http";
|
4
|
+
import { createWebSocketPeer } from "cojson-transport-ws";
|
5
|
+
import { SQLiteStorage } from "cojson-storage-sqlite";
|
6
|
+
import { dirname } from "node:path";
|
7
|
+
import { mkdir } from "node:fs/promises";
|
8
|
+
export const startSyncServer = async ({ port, inMemory, db, }) => {
|
9
|
+
const crypto = await WasmCrypto.create();
|
10
|
+
const server = createServer((req, res) => {
|
11
|
+
if (req.url === "/health") {
|
12
|
+
res.writeHead(200);
|
13
|
+
res.end("ok");
|
14
|
+
}
|
15
|
+
});
|
16
|
+
const wss = new WebSocketServer({ noServer: true });
|
17
|
+
const agentSecret = crypto.newRandomAgentSecret();
|
18
|
+
const agentID = crypto.getAgentID(agentSecret);
|
19
|
+
const localNode = new LocalNode(new ControlledAgent(agentSecret, crypto), crypto.newRandomSessionID(agentID), crypto);
|
20
|
+
if (!inMemory) {
|
21
|
+
await mkdir(dirname(db), { recursive: true });
|
22
|
+
const storage = await SQLiteStorage.asPeer({ filename: db });
|
23
|
+
localNode.syncManager.addPeer(storage);
|
24
|
+
}
|
25
|
+
wss.on("connection", function connection(ws, req) {
|
26
|
+
// ping/pong for the connection liveness
|
27
|
+
const pinging = setInterval(() => {
|
28
|
+
ws.send(JSON.stringify({
|
29
|
+
type: "ping",
|
30
|
+
time: Date.now(),
|
31
|
+
dc: "unknown",
|
32
|
+
}));
|
33
|
+
}, 1500);
|
34
|
+
ws.on("close", () => {
|
35
|
+
clearInterval(pinging);
|
36
|
+
});
|
37
|
+
const clientAddress = req.headers["x-forwarded-for"]
|
38
|
+
?.split(",")[0]
|
39
|
+
?.trim() || req.socket.remoteAddress;
|
40
|
+
const clientId = clientAddress + "@" + new Date().toISOString();
|
41
|
+
localNode.syncManager.addPeer(createWebSocketPeer({
|
42
|
+
id: clientId,
|
43
|
+
role: "client",
|
44
|
+
websocket: ws,
|
45
|
+
expectPings: false,
|
46
|
+
batchingByDefault: false,
|
47
|
+
}));
|
48
|
+
ws.on("error", (e) => console.error(`Error on connection ${clientId}:`, e));
|
49
|
+
});
|
50
|
+
server.on("upgrade", function upgrade(req, socket, head) {
|
51
|
+
if (req.url !== "/health") {
|
52
|
+
wss.handleUpgrade(req, socket, head, function done(ws) {
|
53
|
+
wss.emit("connection", ws, req);
|
54
|
+
});
|
55
|
+
}
|
56
|
+
});
|
57
|
+
server.listen(port ? parseInt(port) : undefined);
|
58
|
+
return server;
|
59
|
+
};
|
60
|
+
//# sourceMappingURL=startSyncServer.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"startSyncServer.js","sourceRoot":"","sources":["../src/startSyncServer.ts"],"names":[],"mappings":"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"}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { describe, onTestFinished, it, expect } from "vitest";
|
2
|
+
import { startSyncServer } from "../startSyncServer.js";
|
3
|
+
import { createWorkerAccount } from "../createWorkerAccount.js";
|
4
|
+
describe("createWorkerAccount - integration tests", () => {
|
5
|
+
it("should create a worker account using the local sync server", async () => {
|
6
|
+
// Pass port: undefined to let the server choose a random port
|
7
|
+
const server = await startSyncServer({ port: undefined, inMemory: true, db: '' });
|
8
|
+
onTestFinished(() => {
|
9
|
+
server.close();
|
10
|
+
});
|
11
|
+
const address = server.address();
|
12
|
+
if (typeof address !== 'object' || address === null) {
|
13
|
+
throw new Error('Server address is not an object');
|
14
|
+
}
|
15
|
+
const { accountId, agentSecret } = await createWorkerAccount({ name: "test", peer: `ws://localhost:${address.port}` });
|
16
|
+
expect(accountId).toBeDefined();
|
17
|
+
expect(agentSecret).toBeDefined();
|
18
|
+
});
|
19
|
+
it("should create a worker account using the Jazz cloud", async () => {
|
20
|
+
const { accountId, agentSecret } = await createWorkerAccount({ name: "test", peer: `wss://cloud.jazz.tools` });
|
21
|
+
expect(accountId).toBeDefined();
|
22
|
+
expect(agentSecret).toBeDefined();
|
23
|
+
});
|
24
|
+
});
|
25
|
+
//# sourceMappingURL=createWorkerAccount.test.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"createWorkerAccount.test.js","sourceRoot":"","sources":["../../src/test/createWorkerAccount.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAEhE,QAAQ,CAAC,yCAAyC,EAAE,GAAG,EAAE;IACrD,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QACxE,8DAA8D;QAC9D,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAElF,cAAc,CAAC,GAAG,EAAE;YAChB,MAAM,CAAC,KAAK,EAAE,CAAA;QAClB,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QAEjC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,MAAM,mBAAmB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAEvH,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;QAChC,MAAM,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,MAAM,mBAAmB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAC,CAAC;QAE/G,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;QAChC,MAAM,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
"bin": "./dist/index.js",
|
4
4
|
"type": "module",
|
5
5
|
"license": "MIT",
|
6
|
-
"version": "0.8.
|
6
|
+
"version": "0.8.13",
|
7
7
|
"dependencies": {
|
8
8
|
"@effect/cli": "^0.41.2",
|
9
9
|
"@effect/platform-node": "^0.57.2",
|
@@ -11,11 +11,11 @@
|
|
11
11
|
"@effect/printer-ansi": "^0.34.5",
|
12
12
|
"@effect/schema": "^0.71.1",
|
13
13
|
"@effect/typeclass": "^0.25.5",
|
14
|
-
"cojson": "0.8.
|
15
|
-
"cojson-storage-sqlite": "0.8.
|
16
|
-
"cojson-transport-ws": "0.8.
|
14
|
+
"cojson": "0.8.12",
|
15
|
+
"cojson-storage-sqlite": "0.8.12",
|
16
|
+
"cojson-transport-ws": "0.8.13",
|
17
17
|
"effect": "^3.6.5",
|
18
|
-
"jazz-tools": "0.8.
|
18
|
+
"jazz-tools": "0.8.13",
|
19
19
|
"ws": "^8.14.2"
|
20
20
|
},
|
21
21
|
"devDependencies": {
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import { createWebSocketPeer } from "cojson-transport-ws";
|
2
|
+
import { WebSocket } from "ws";
|
3
|
+
import {
|
4
|
+
Account,
|
5
|
+
Peer,
|
6
|
+
WasmCrypto,
|
7
|
+
createJazzContext,
|
8
|
+
isControlledAccount,
|
9
|
+
} from "jazz-tools";
|
10
|
+
import { fixedCredentialsAuth, randomSessionProvider } from "jazz-tools";
|
11
|
+
import { CoValueCore } from "cojson";
|
12
|
+
|
13
|
+
export const createWorkerAccount = async ({
|
14
|
+
name,
|
15
|
+
peer: peerAddr,
|
16
|
+
}: {
|
17
|
+
name: string;
|
18
|
+
peer: string;
|
19
|
+
}) => {
|
20
|
+
const crypto = await WasmCrypto.create();
|
21
|
+
|
22
|
+
const peer = createWebSocketPeer({
|
23
|
+
id: "upstream",
|
24
|
+
websocket: new WebSocket(peerAddr),
|
25
|
+
role: "server",
|
26
|
+
});
|
27
|
+
|
28
|
+
const account = await Account.create({
|
29
|
+
creationProps: { name },
|
30
|
+
peersToLoadFrom: [peer],
|
31
|
+
crypto,
|
32
|
+
});
|
33
|
+
|
34
|
+
if (!isControlledAccount(account)) {
|
35
|
+
throw new Error("account is not a controlled account");
|
36
|
+
}
|
37
|
+
|
38
|
+
const accountCoValue = account._raw.core;
|
39
|
+
const accountProfileCoValue = account.profile!._raw.core;
|
40
|
+
const syncManager = account._raw.core.node.syncManager;
|
41
|
+
|
42
|
+
await Promise.all([
|
43
|
+
syncManager.syncCoValue(accountCoValue),
|
44
|
+
syncManager.syncCoValue(accountProfileCoValue),
|
45
|
+
]);
|
46
|
+
|
47
|
+
await Promise.all([
|
48
|
+
waitForSync(account, peer, accountCoValue),
|
49
|
+
waitForSync(account, peer, accountProfileCoValue),
|
50
|
+
]);
|
51
|
+
|
52
|
+
// Spawn a second peer to double check that the account is fully synced
|
53
|
+
const peer2 = createWebSocketPeer({
|
54
|
+
id: "upstream2",
|
55
|
+
websocket: new WebSocket(peerAddr),
|
56
|
+
role: "server",
|
57
|
+
});
|
58
|
+
|
59
|
+
await createJazzContext({
|
60
|
+
auth: fixedCredentialsAuth({
|
61
|
+
accountID: account.id,
|
62
|
+
secret: account._raw.agentSecret,
|
63
|
+
}),
|
64
|
+
sessionProvider: randomSessionProvider,
|
65
|
+
peersToLoadFrom: [peer2],
|
66
|
+
crypto,
|
67
|
+
});
|
68
|
+
|
69
|
+
return {
|
70
|
+
accountId: account.id,
|
71
|
+
agentSecret: account._raw.agentSecret,
|
72
|
+
};
|
73
|
+
};
|
74
|
+
|
75
|
+
function waitForSync(account: Account, peer: Peer, coValue: CoValueCore) {
|
76
|
+
const syncManager = account._raw.core.node.syncManager;
|
77
|
+
const peerState = syncManager.peers[peer.id];
|
78
|
+
|
79
|
+
return new Promise((resolve) => {
|
80
|
+
const unsubscribe = peerState?.optimisticKnownStates.subscribe(
|
81
|
+
(id, peerKnownState) => {
|
82
|
+
if (id !== coValue.id) return;
|
83
|
+
|
84
|
+
const knownState = coValue.knownState();
|
85
|
+
|
86
|
+
const synced = isEqualSession(
|
87
|
+
knownState.sessions,
|
88
|
+
peerKnownState.sessions,
|
89
|
+
);
|
90
|
+
if (synced) {
|
91
|
+
resolve(true);
|
92
|
+
unsubscribe?.();
|
93
|
+
}
|
94
|
+
},
|
95
|
+
);
|
96
|
+
});
|
97
|
+
}
|
98
|
+
|
99
|
+
function isEqualSession(a: Record<string, number>, b: Record<string, number>) {
|
100
|
+
const keysA = Object.keys(a);
|
101
|
+
const keysB = Object.keys(b);
|
102
|
+
|
103
|
+
if (keysA.length !== keysB.length) {
|
104
|
+
return false;
|
105
|
+
}
|
106
|
+
|
107
|
+
for (const sessionId of keysA) {
|
108
|
+
if (a[sessionId] !== b[sessionId]) {
|
109
|
+
return false;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
return true;
|
114
|
+
}
|
package/src/index.ts
CHANGED
@@ -1,18 +1,86 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
2
|
/* istanbul ignore file -- @preserve */
|
3
|
-
import { Command } from "@effect/cli";
|
3
|
+
import { Command, Options } from "@effect/cli";
|
4
4
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
5
|
-
import { Effect } from "effect";
|
6
|
-
import {
|
7
|
-
import {
|
5
|
+
import { Console, Effect } from "effect";
|
6
|
+
import { createWorkerAccount } from "./createWorkerAccount.js";
|
7
|
+
import { startSyncServer } from "./startSyncServer.js";
|
8
8
|
|
9
9
|
const jazzTools = Command.make("jazz-tools");
|
10
10
|
|
11
|
-
const
|
11
|
+
const nameOption = Options.text("name").pipe(Options.withAlias("n"));
|
12
|
+
const peerOption = Options.text("peer")
|
13
|
+
.pipe(Options.withAlias("p"))
|
14
|
+
.pipe(Options.withDefault("wss://cloud.jazz.tools"));
|
15
|
+
|
16
|
+
const createAccountCommand = Command.make(
|
17
|
+
"create",
|
18
|
+
{ name: nameOption, peer: peerOption },
|
19
|
+
({ name, peer }) => {
|
20
|
+
return Effect.gen(function* () {
|
21
|
+
const { accountId, agentSecret } = yield* Effect.promise(() =>
|
22
|
+
createWorkerAccount({ name, peer }),
|
23
|
+
);
|
24
|
+
|
25
|
+
yield* Console.log(`# Credentials for Jazz account "${name}":
|
26
|
+
JAZZ_WORKER_ACCOUNT=${accountId}
|
27
|
+
JAZZ_WORKER_SECRET=${agentSecret}
|
28
|
+
`);
|
29
|
+
});
|
30
|
+
},
|
31
|
+
);
|
32
|
+
|
33
|
+
const accountCommand = Command.make("account").pipe(
|
34
|
+
Command.withSubcommands([createAccountCommand]),
|
35
|
+
);
|
36
|
+
|
37
|
+
const portOption = Options.text("port")
|
38
|
+
.pipe(Options.withAlias("p"))
|
39
|
+
.pipe(
|
40
|
+
Options.withDescription(
|
41
|
+
"Select a different port for the WebSocket server. Default is 4200",
|
42
|
+
),
|
43
|
+
)
|
44
|
+
.pipe(Options.withDefault("4200"));
|
45
|
+
|
46
|
+
const inMemoryOption = Options.boolean("in-memory").pipe(
|
47
|
+
Options.withDescription("Use an in-memory storage instead of file-based"),
|
48
|
+
);
|
49
|
+
|
50
|
+
const dbOption = Options.file("db")
|
51
|
+
.pipe(
|
52
|
+
Options.withDescription(
|
53
|
+
"The path to the file where to store the data. Default is 'sync-db/storage.db'",
|
54
|
+
),
|
55
|
+
)
|
56
|
+
.pipe(Options.withDefault("sync-db/storage.db"));
|
57
|
+
|
58
|
+
export const startSyncServerCommand = Command.make(
|
59
|
+
"sync",
|
60
|
+
{ port: portOption, inMemory: inMemoryOption, db: dbOption },
|
61
|
+
({ port, inMemory, db }) => {
|
62
|
+
return Effect.gen(function* () {
|
63
|
+
yield* Effect.promise(() =>
|
64
|
+
startSyncServer({ port, inMemory, db }),
|
65
|
+
);
|
66
|
+
|
67
|
+
Console.log(
|
68
|
+
`COJSON sync server listening on ws://127.0.0.1:${port}`,
|
69
|
+
);
|
70
|
+
|
71
|
+
// Keep the server up
|
72
|
+
yield* Effect.never;
|
73
|
+
});
|
74
|
+
},
|
75
|
+
);
|
76
|
+
|
77
|
+
const command = jazzTools.pipe(
|
78
|
+
Command.withSubcommands([accountCommand, startSyncServerCommand]),
|
79
|
+
);
|
12
80
|
|
13
81
|
const cli = Command.run(command, {
|
14
82
|
name: "Jazz CLI Tools",
|
15
|
-
version: "v0.
|
83
|
+
version: "v0.8.11",
|
16
84
|
});
|
17
85
|
|
18
86
|
Effect.suspend(() => cli(process.argv)).pipe(
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import { ControlledAgent, LocalNode, WasmCrypto } from "cojson";
|
2
|
+
import { WebSocketServer } from "ws";
|
3
|
+
import { createServer } from "http";
|
4
|
+
|
5
|
+
import { createWebSocketPeer } from "cojson-transport-ws";
|
6
|
+
import { SQLiteStorage } from "cojson-storage-sqlite";
|
7
|
+
import { dirname } from "node:path";
|
8
|
+
import { mkdir } from "node:fs/promises";
|
9
|
+
|
10
|
+
export const startSyncServer = async ({
|
11
|
+
port,
|
12
|
+
inMemory,
|
13
|
+
db,
|
14
|
+
}: {
|
15
|
+
port: string | undefined;
|
16
|
+
inMemory: boolean;
|
17
|
+
db: string;
|
18
|
+
}) => {
|
19
|
+
const crypto = await WasmCrypto.create();
|
20
|
+
|
21
|
+
const server = createServer((req, res) => {
|
22
|
+
if (req.url === "/health") {
|
23
|
+
res.writeHead(200);
|
24
|
+
res.end("ok");
|
25
|
+
}
|
26
|
+
});
|
27
|
+
const wss = new WebSocketServer({ noServer: true });
|
28
|
+
|
29
|
+
const agentSecret = crypto.newRandomAgentSecret();
|
30
|
+
const agentID = crypto.getAgentID(agentSecret);
|
31
|
+
|
32
|
+
const localNode = new LocalNode(
|
33
|
+
new ControlledAgent(agentSecret, crypto),
|
34
|
+
crypto.newRandomSessionID(agentID),
|
35
|
+
crypto,
|
36
|
+
);
|
37
|
+
|
38
|
+
if (!inMemory) {
|
39
|
+
await mkdir(dirname(db), { recursive: true })
|
40
|
+
|
41
|
+
const storage = await SQLiteStorage.asPeer({ filename: db });
|
42
|
+
|
43
|
+
localNode.syncManager.addPeer(storage);
|
44
|
+
}
|
45
|
+
|
46
|
+
wss.on("connection", function connection(ws, req) {
|
47
|
+
// ping/pong for the connection liveness
|
48
|
+
const pinging = setInterval(() => {
|
49
|
+
ws.send(
|
50
|
+
JSON.stringify({
|
51
|
+
type: "ping",
|
52
|
+
time: Date.now(),
|
53
|
+
dc: "unknown",
|
54
|
+
}),
|
55
|
+
);
|
56
|
+
}, 1500);
|
57
|
+
|
58
|
+
ws.on("close", () => {
|
59
|
+
clearInterval(pinging);
|
60
|
+
});
|
61
|
+
|
62
|
+
const clientAddress =
|
63
|
+
(req.headers["x-forwarded-for"] as string | undefined)
|
64
|
+
?.split(",")[0]
|
65
|
+
?.trim() || req.socket.remoteAddress;
|
66
|
+
|
67
|
+
const clientId = clientAddress + "@" + new Date().toISOString();
|
68
|
+
|
69
|
+
localNode.syncManager.addPeer(
|
70
|
+
createWebSocketPeer({
|
71
|
+
id: clientId,
|
72
|
+
role: "client",
|
73
|
+
websocket: ws,
|
74
|
+
expectPings: false,
|
75
|
+
batchingByDefault: false,
|
76
|
+
}),
|
77
|
+
);
|
78
|
+
|
79
|
+
ws.on("error", (e) =>
|
80
|
+
console.error(`Error on connection ${clientId}:`, e),
|
81
|
+
);
|
82
|
+
});
|
83
|
+
|
84
|
+
server.on("upgrade", function upgrade(req, socket, head) {
|
85
|
+
if (req.url !== "/health") {
|
86
|
+
wss.handleUpgrade(req, socket, head, function done(ws) {
|
87
|
+
wss.emit("connection", ws, req);
|
88
|
+
});
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
server.listen(port ? parseInt(port) : undefined);
|
93
|
+
|
94
|
+
return server;
|
95
|
+
};
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { describe, onTestFinished, it, expect } from "vitest";
|
2
|
+
import { startSyncServer } from "../startSyncServer.js";
|
3
|
+
import { createWorkerAccount } from "../createWorkerAccount.js";
|
4
|
+
|
5
|
+
describe("createWorkerAccount - integration tests", () => {
|
6
|
+
it("should create a worker account using the local sync server", async () => {
|
7
|
+
// Pass port: undefined to let the server choose a random port
|
8
|
+
const server = await startSyncServer({ port: undefined, inMemory: true, db: '' });
|
9
|
+
|
10
|
+
onTestFinished(() => {
|
11
|
+
server.close()
|
12
|
+
});
|
13
|
+
|
14
|
+
const address = server.address();
|
15
|
+
|
16
|
+
if (typeof address !== 'object' || address === null) {
|
17
|
+
throw new Error('Server address is not an object');
|
18
|
+
}
|
19
|
+
|
20
|
+
const { accountId, agentSecret } = await createWorkerAccount({ name: "test", peer: `ws://localhost:${address.port}` });
|
21
|
+
|
22
|
+
expect(accountId).toBeDefined();
|
23
|
+
expect(agentSecret).toBeDefined();
|
24
|
+
});
|
25
|
+
|
26
|
+
it("should create a worker account using the Jazz cloud", async () => {
|
27
|
+
const { accountId, agentSecret } = await createWorkerAccount({ name: "test", peer: `wss://cloud.jazz.tools` });
|
28
|
+
|
29
|
+
expect(accountId).toBeDefined();
|
30
|
+
expect(agentSecret).toBeDefined();
|
31
|
+
});
|
32
|
+
});
|
package/dist/accountCreate.js
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
2
|
-
import { Console, Effect } from "effect";
|
3
|
-
import { createWebSocketPeer } from "cojson-transport-ws";
|
4
|
-
import { WebSocket } from "ws";
|
5
|
-
import { Account, WasmCrypto, createJazzContext, isControlledAccount, } from "jazz-tools";
|
6
|
-
import { fixedCredentialsAuth, randomSessionProvider } from "jazz-tools";
|
7
|
-
const name = Options.text("name").pipe(Options.withAlias("n"));
|
8
|
-
const peer = Options.text("peer")
|
9
|
-
.pipe(Options.withAlias("p"))
|
10
|
-
.pipe(Options.withDefault("wss://cloud.jazz.tools"));
|
11
|
-
const accountCreate = Command.make("create", { name, peer }, ({ name, peer: peerAddr }) => {
|
12
|
-
return Effect.gen(function* () {
|
13
|
-
const crypto = yield* Effect.promise(() => WasmCrypto.create());
|
14
|
-
const peer = createWebSocketPeer({
|
15
|
-
id: "upstream",
|
16
|
-
websocket: new WebSocket(peerAddr),
|
17
|
-
role: "server",
|
18
|
-
});
|
19
|
-
const account = yield* Effect.promise(async () => Account.create({
|
20
|
-
creationProps: { name },
|
21
|
-
peersToLoadFrom: [peer],
|
22
|
-
crypto,
|
23
|
-
}));
|
24
|
-
if (!isControlledAccount(account)) {
|
25
|
-
throw new Error("account is not a controlled account");
|
26
|
-
}
|
27
|
-
const accountCoValue = account._raw.core;
|
28
|
-
const accountProfileCoValue = account.profile._raw.core;
|
29
|
-
const syncManager = account._raw.core.node.syncManager;
|
30
|
-
yield* Effect.promise(() => syncManager.syncCoValue(accountCoValue));
|
31
|
-
yield* Effect.promise(() => syncManager.syncCoValue(accountProfileCoValue));
|
32
|
-
yield* Effect.promise(() => Promise.all([
|
33
|
-
waitForSync(account, peer, accountCoValue),
|
34
|
-
waitForSync(account, peer, accountProfileCoValue),
|
35
|
-
]));
|
36
|
-
// Spawn a second peer to double check that the account is fully synced
|
37
|
-
const peer2 = createWebSocketPeer({
|
38
|
-
id: "upstream2",
|
39
|
-
websocket: new WebSocket(peerAddr),
|
40
|
-
role: "server",
|
41
|
-
});
|
42
|
-
yield* Effect.promise(async () => createJazzContext({
|
43
|
-
auth: fixedCredentialsAuth({
|
44
|
-
accountID: account.id,
|
45
|
-
secret: account._raw.agentSecret,
|
46
|
-
}),
|
47
|
-
sessionProvider: randomSessionProvider,
|
48
|
-
peersToLoadFrom: [peer2],
|
49
|
-
crypto,
|
50
|
-
}));
|
51
|
-
yield* Console.log(`# Credentials for Jazz account "${name}":
|
52
|
-
JAZZ_WORKER_ACCOUNT=${account.id}
|
53
|
-
JAZZ_WORKER_SECRET=${account._raw.agentSecret}
|
54
|
-
`);
|
55
|
-
});
|
56
|
-
});
|
57
|
-
const accountBase = Command.make("account");
|
58
|
-
export const account = accountBase.pipe(Command.withSubcommands([accountCreate]));
|
59
|
-
function waitForSync(account, peer, coValue) {
|
60
|
-
const syncManager = account._raw.core.node.syncManager;
|
61
|
-
const peerState = syncManager.peers[peer.id];
|
62
|
-
return new Promise((resolve) => {
|
63
|
-
const unsubscribe = peerState?.optimisticKnownStates.subscribe((id, peerKnownState) => {
|
64
|
-
if (id !== coValue.id)
|
65
|
-
return;
|
66
|
-
const knownState = coValue.knownState();
|
67
|
-
const synced = isEqualSession(knownState.sessions, peerKnownState.sessions);
|
68
|
-
if (synced) {
|
69
|
-
resolve(true);
|
70
|
-
unsubscribe?.();
|
71
|
-
}
|
72
|
-
});
|
73
|
-
});
|
74
|
-
}
|
75
|
-
function isEqualSession(a, b) {
|
76
|
-
const keysA = Object.keys(a);
|
77
|
-
const keysB = Object.keys(b);
|
78
|
-
if (keysA.length !== keysB.length) {
|
79
|
-
return false;
|
80
|
-
}
|
81
|
-
for (const sessionId of keysA) {
|
82
|
-
if (a[sessionId] !== b[sessionId]) {
|
83
|
-
return false;
|
84
|
-
}
|
85
|
-
}
|
86
|
-
return true;
|
87
|
-
}
|
88
|
-
//# sourceMappingURL=accountCreate.js.map
|
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"accountCreate.js","sourceRoot":"","sources":["../src/accountCreate.ts"],"names":[],"mappings":"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"}
|
package/dist/startSync.js
DELETED
@@ -1,76 +0,0 @@
|
|
1
|
-
/* istanbul ignore file -- @preserve */
|
2
|
-
import { Command, Options } from "@effect/cli";
|
3
|
-
import { ControlledAgent, LocalNode, WasmCrypto } from "cojson";
|
4
|
-
import { WebSocketServer } from "ws";
|
5
|
-
import { createServer } from "http";
|
6
|
-
import { createWebSocketPeer } from "cojson-transport-ws";
|
7
|
-
import { Effect } from "effect";
|
8
|
-
import { SQLiteStorage } from "cojson-storage-sqlite";
|
9
|
-
import { dirname } from "node:path";
|
10
|
-
import { mkdir } from "node:fs/promises";
|
11
|
-
const port = Options.text("port")
|
12
|
-
.pipe(Options.withAlias("p"))
|
13
|
-
.pipe(Options.withDescription("Select a different port for the WebSocket server. Default is 4200"))
|
14
|
-
.pipe(Options.withDefault("4200"));
|
15
|
-
const inMemory = Options.boolean("in-memory").pipe(Options.withDescription("Use an in-memory storage instead of file-based"));
|
16
|
-
const db = Options.file("db")
|
17
|
-
.pipe(Options.withDescription("The path to the file where to store the data. Default is 'sync-db/storage.db'"))
|
18
|
-
.pipe(Options.withDefault("sync-db/storage.db"));
|
19
|
-
export const startSync = Command.make("sync", { port, inMemory, db }, ({ port, inMemory, db }) => {
|
20
|
-
return Effect.gen(function* () {
|
21
|
-
const crypto = yield* Effect.promise(() => WasmCrypto.create());
|
22
|
-
const server = createServer((req, res) => {
|
23
|
-
if (req.url === "/health") {
|
24
|
-
res.writeHead(200);
|
25
|
-
res.end("ok");
|
26
|
-
}
|
27
|
-
});
|
28
|
-
const wss = new WebSocketServer({ noServer: true });
|
29
|
-
console.log("COJSON sync server listening on port " + port);
|
30
|
-
const agentSecret = crypto.newRandomAgentSecret();
|
31
|
-
const agentID = crypto.getAgentID(agentSecret);
|
32
|
-
const localNode = new LocalNode(new ControlledAgent(agentSecret, crypto), crypto.newRandomSessionID(agentID), crypto);
|
33
|
-
if (!inMemory) {
|
34
|
-
yield* Effect.promise(() => mkdir(dirname(db), { recursive: true }));
|
35
|
-
const storage = yield* Effect.promise(() => SQLiteStorage.asPeer({ filename: db }));
|
36
|
-
localNode.syncManager.addPeer(storage);
|
37
|
-
}
|
38
|
-
wss.on("connection", function connection(ws, req) {
|
39
|
-
// ping/pong for the connection liveness
|
40
|
-
const pinging = setInterval(() => {
|
41
|
-
ws.send(JSON.stringify({
|
42
|
-
type: "ping",
|
43
|
-
time: Date.now(),
|
44
|
-
dc: "unknown",
|
45
|
-
}));
|
46
|
-
}, 1500);
|
47
|
-
ws.on("close", () => {
|
48
|
-
clearInterval(pinging);
|
49
|
-
});
|
50
|
-
const clientAddress = req.headers["x-forwarded-for"]
|
51
|
-
?.split(",")[0]
|
52
|
-
?.trim() || req.socket.remoteAddress;
|
53
|
-
const clientId = clientAddress + "@" + new Date().toISOString();
|
54
|
-
localNode.syncManager.addPeer(createWebSocketPeer({
|
55
|
-
id: clientId,
|
56
|
-
role: "client",
|
57
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
58
|
-
websocket: ws, // TODO: fix types
|
59
|
-
expectPings: false,
|
60
|
-
batchingByDefault: false
|
61
|
-
}));
|
62
|
-
ws.on("error", (e) => console.error(`Error on connection ${clientId}:`, e));
|
63
|
-
});
|
64
|
-
server.on("upgrade", function upgrade(req, socket, head) {
|
65
|
-
if (req.url !== "/health") {
|
66
|
-
wss.handleUpgrade(req, socket, head, function done(ws) {
|
67
|
-
wss.emit("connection", ws, req);
|
68
|
-
});
|
69
|
-
}
|
70
|
-
});
|
71
|
-
server.listen(parseInt(port));
|
72
|
-
// Keep the server up
|
73
|
-
yield* Effect.never;
|
74
|
-
});
|
75
|
-
});
|
76
|
-
//# sourceMappingURL=startSync.js.map
|
package/dist/startSync.js.map
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"startSync.js","sourceRoot":"","sources":["../src/startSync.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;KAC5B,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;KAC5B,IAAI,CACD,OAAO,CAAC,eAAe,CACnB,mEAAmE,CACtE,CACJ;KACA,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;AAEvC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAC9C,OAAO,CAAC,eAAe,CAAC,gDAAgD,CAAC,CAC5E,CAAC;AAEF,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;KACxB,IAAI,CACD,OAAO,CAAC,eAAe,CACnB,+EAA+E,CAClF,CACJ;KACA,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC,CAAC;AAErD,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CACjC,MAAM,EACN,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EACtB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,EAAE;IACvB,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAEhE,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACrC,IAAI,GAAG,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBACxB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpD,OAAO,CAAC,GAAG,CAAC,uCAAuC,GAAG,IAAI,CAAC,CAAC;QAE5D,MAAM,WAAW,GAAG,MAAM,CAAC,oBAAoB,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QAE/C,MAAM,SAAS,GAAG,IAAI,SAAS,CAC3B,IAAI,eAAe,CAAC,WAAW,EAAE,MAAM,CAAC,EACxC,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAClC,MAAM,CACT,CAAC;QAEF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CACvB,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAC1C,CAAC;YAEF,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CACvC,aAAa,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CACzC,CAAC;YAEF,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;QAED,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,SAAS,UAAU,CAAC,EAAE,EAAE,GAAG;YAC5C,wCAAwC;YACxC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE;gBAC7B,EAAE,CAAC,IAAI,CACH,IAAI,CAAC,SAAS,CAAC;oBACX,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE;oBAChB,EAAE,EAAE,SAAS;iBAChB,CAAC,CACL,CAAC;YACN,CAAC,EAAE,IAAI,CAAC,CAAC;YAET,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBAChB,aAAa,CAAC,OAAO,CAAC,CAAC;YAC3B,CAAC,CAAC,CAAC;YAEH,MAAM,aAAa,GACd,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAwB;gBAClD,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACf,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;YAE7C,MAAM,QAAQ,GAAG,aAAa,GAAG,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAEhE,SAAS,CAAC,WAAW,CAAC,OAAO,CACzB,mBAAmB,CAAC;gBAChB,EAAE,EAAE,QAAQ;gBACZ,IAAI,EAAE,QAAQ;gBACd,8DAA8D;gBAC9D,SAAS,EAAE,EAAS,EAAE,kBAAkB;gBACxC,WAAW,EAAE,KAAK;gBAClB,iBAAiB,EAAE,KAAK;aAC3B,CAAC,CACL,CAAC;YAEF,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CACjB,OAAO,CAAC,KAAK,CAAC,uBAAuB,QAAQ,GAAG,EAAE,CAAC,CAAC,CACvD,CAAC;QACN,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI;YACnD,IAAI,GAAG,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBACxB,GAAG,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,EAAE;oBACjD,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;gBACpC,CAAC,CAAC,CAAC;YACP,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QAE9B,qBAAqB;QACrB,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;IACxB,CAAC,CAAC,CAAC;AACP,CAAC,CACJ,CAAC"}
|
package/src/accountCreate.ts
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
2
|
-
import { Console, Effect } from "effect";
|
3
|
-
import { createWebSocketPeer } from "cojson-transport-ws";
|
4
|
-
import { WebSocket } from "ws";
|
5
|
-
import {
|
6
|
-
Account,
|
7
|
-
Peer,
|
8
|
-
WasmCrypto,
|
9
|
-
createJazzContext,
|
10
|
-
isControlledAccount,
|
11
|
-
} from "jazz-tools";
|
12
|
-
import { fixedCredentialsAuth, randomSessionProvider } from "jazz-tools";
|
13
|
-
import { CoValueCore } from "cojson";
|
14
|
-
|
15
|
-
const name = Options.text("name").pipe(Options.withAlias("n"));
|
16
|
-
const peer = Options.text("peer")
|
17
|
-
.pipe(Options.withAlias("p"))
|
18
|
-
.pipe(Options.withDefault("wss://cloud.jazz.tools"));
|
19
|
-
|
20
|
-
const accountCreate = Command.make(
|
21
|
-
"create",
|
22
|
-
{ name, peer },
|
23
|
-
({ name, peer: peerAddr }) => {
|
24
|
-
return Effect.gen(function* () {
|
25
|
-
const crypto = yield* Effect.promise(() => WasmCrypto.create());
|
26
|
-
|
27
|
-
const peer = createWebSocketPeer({
|
28
|
-
id: "upstream",
|
29
|
-
websocket: new WebSocket(peerAddr),
|
30
|
-
role: "server",
|
31
|
-
});
|
32
|
-
|
33
|
-
const account: Account = yield* Effect.promise(async () =>
|
34
|
-
Account.create({
|
35
|
-
creationProps: { name },
|
36
|
-
peersToLoadFrom: [peer],
|
37
|
-
crypto,
|
38
|
-
}),
|
39
|
-
);
|
40
|
-
if (!isControlledAccount(account)) {
|
41
|
-
throw new Error("account is not a controlled account");
|
42
|
-
}
|
43
|
-
|
44
|
-
const accountCoValue = account._raw.core;
|
45
|
-
const accountProfileCoValue = account.profile!._raw.core;
|
46
|
-
const syncManager = account._raw.core.node.syncManager;
|
47
|
-
|
48
|
-
yield* Effect.promise(() =>
|
49
|
-
syncManager.syncCoValue(accountCoValue),
|
50
|
-
);
|
51
|
-
yield* Effect.promise(() =>
|
52
|
-
syncManager.syncCoValue(accountProfileCoValue),
|
53
|
-
);
|
54
|
-
|
55
|
-
yield* Effect.promise(() => Promise.all([
|
56
|
-
waitForSync(account, peer, accountCoValue),
|
57
|
-
waitForSync(account, peer, accountProfileCoValue),
|
58
|
-
]));
|
59
|
-
|
60
|
-
// Spawn a second peer to double check that the account is fully synced
|
61
|
-
const peer2 = createWebSocketPeer({
|
62
|
-
id: "upstream2",
|
63
|
-
websocket: new WebSocket(peerAddr),
|
64
|
-
role: "server",
|
65
|
-
});
|
66
|
-
|
67
|
-
yield* Effect.promise(async () =>
|
68
|
-
createJazzContext({
|
69
|
-
auth: fixedCredentialsAuth({
|
70
|
-
accountID: account.id,
|
71
|
-
secret: account._raw.agentSecret,
|
72
|
-
}),
|
73
|
-
sessionProvider: randomSessionProvider,
|
74
|
-
peersToLoadFrom: [peer2],
|
75
|
-
crypto,
|
76
|
-
}),
|
77
|
-
);
|
78
|
-
|
79
|
-
yield* Console.log(`# Credentials for Jazz account "${name}":
|
80
|
-
JAZZ_WORKER_ACCOUNT=${account.id}
|
81
|
-
JAZZ_WORKER_SECRET=${account._raw.agentSecret}
|
82
|
-
`);
|
83
|
-
});
|
84
|
-
},
|
85
|
-
);
|
86
|
-
|
87
|
-
const accountBase = Command.make("account");
|
88
|
-
|
89
|
-
export const account = accountBase.pipe(Command.withSubcommands([accountCreate]));
|
90
|
-
|
91
|
-
function waitForSync(account: Account, peer: Peer, coValue: CoValueCore) {
|
92
|
-
const syncManager = account._raw.core.node.syncManager;
|
93
|
-
const peerState = syncManager.peers[peer.id];
|
94
|
-
|
95
|
-
return new Promise((resolve) => {
|
96
|
-
const unsubscribe = peerState?.optimisticKnownStates.subscribe((id, peerKnownState) => {
|
97
|
-
if (id !== coValue.id) return;
|
98
|
-
|
99
|
-
const knownState = coValue.knownState();
|
100
|
-
|
101
|
-
const synced = isEqualSession(knownState.sessions, peerKnownState.sessions);
|
102
|
-
if (synced) {
|
103
|
-
resolve(true);
|
104
|
-
unsubscribe?.();
|
105
|
-
}
|
106
|
-
});
|
107
|
-
});
|
108
|
-
}
|
109
|
-
|
110
|
-
function isEqualSession(a: Record<string, number>, b: Record<string, number>) {
|
111
|
-
const keysA = Object.keys(a);
|
112
|
-
const keysB = Object.keys(b);
|
113
|
-
|
114
|
-
if (keysA.length !== keysB.length) {
|
115
|
-
return false;
|
116
|
-
}
|
117
|
-
|
118
|
-
for (const sessionId of keysA) {
|
119
|
-
if (a[sessionId] !== b[sessionId]) {
|
120
|
-
return false;
|
121
|
-
}
|
122
|
-
}
|
123
|
-
|
124
|
-
return true;
|
125
|
-
}
|
package/src/startSync.ts
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
/* istanbul ignore file -- @preserve */
|
2
|
-
import { Command, Options } from "@effect/cli";
|
3
|
-
import { ControlledAgent, LocalNode, WasmCrypto } from "cojson";
|
4
|
-
import { WebSocketServer } from "ws";
|
5
|
-
import { createServer } from "http";
|
6
|
-
|
7
|
-
import { createWebSocketPeer } from "cojson-transport-ws";
|
8
|
-
import { Effect } from "effect";
|
9
|
-
import { SQLiteStorage } from "cojson-storage-sqlite";
|
10
|
-
import { dirname } from "node:path";
|
11
|
-
import { mkdir } from "node:fs/promises";
|
12
|
-
|
13
|
-
const port = Options.text("port")
|
14
|
-
.pipe(Options.withAlias("p"))
|
15
|
-
.pipe(
|
16
|
-
Options.withDescription(
|
17
|
-
"Select a different port for the WebSocket server. Default is 4200",
|
18
|
-
),
|
19
|
-
)
|
20
|
-
.pipe(Options.withDefault("4200"));
|
21
|
-
|
22
|
-
const inMemory = Options.boolean("in-memory").pipe(
|
23
|
-
Options.withDescription("Use an in-memory storage instead of file-based"),
|
24
|
-
);
|
25
|
-
|
26
|
-
const db = Options.file("db")
|
27
|
-
.pipe(
|
28
|
-
Options.withDescription(
|
29
|
-
"The path to the file where to store the data. Default is 'sync-db/storage.db'",
|
30
|
-
),
|
31
|
-
)
|
32
|
-
.pipe(Options.withDefault("sync-db/storage.db"));
|
33
|
-
|
34
|
-
export const startSync = Command.make(
|
35
|
-
"sync",
|
36
|
-
{ port, inMemory, db },
|
37
|
-
({ port, inMemory, db }) => {
|
38
|
-
return Effect.gen(function* () {
|
39
|
-
const crypto = yield* Effect.promise(() => WasmCrypto.create());
|
40
|
-
|
41
|
-
const server = createServer((req, res) => {
|
42
|
-
if (req.url === "/health") {
|
43
|
-
res.writeHead(200);
|
44
|
-
res.end("ok");
|
45
|
-
}
|
46
|
-
});
|
47
|
-
const wss = new WebSocketServer({ noServer: true });
|
48
|
-
|
49
|
-
console.log("COJSON sync server listening on port " + port);
|
50
|
-
|
51
|
-
const agentSecret = crypto.newRandomAgentSecret();
|
52
|
-
const agentID = crypto.getAgentID(agentSecret);
|
53
|
-
|
54
|
-
const localNode = new LocalNode(
|
55
|
-
new ControlledAgent(agentSecret, crypto),
|
56
|
-
crypto.newRandomSessionID(agentID),
|
57
|
-
crypto,
|
58
|
-
);
|
59
|
-
|
60
|
-
if (!inMemory) {
|
61
|
-
yield* Effect.promise(() =>
|
62
|
-
mkdir(dirname(db), { recursive: true }),
|
63
|
-
);
|
64
|
-
|
65
|
-
const storage = yield* Effect.promise(() =>
|
66
|
-
SQLiteStorage.asPeer({ filename: db }),
|
67
|
-
);
|
68
|
-
|
69
|
-
localNode.syncManager.addPeer(storage);
|
70
|
-
}
|
71
|
-
|
72
|
-
wss.on("connection", function connection(ws, req) {
|
73
|
-
// ping/pong for the connection liveness
|
74
|
-
const pinging = setInterval(() => {
|
75
|
-
ws.send(
|
76
|
-
JSON.stringify({
|
77
|
-
type: "ping",
|
78
|
-
time: Date.now(),
|
79
|
-
dc: "unknown",
|
80
|
-
}),
|
81
|
-
);
|
82
|
-
}, 1500);
|
83
|
-
|
84
|
-
ws.on("close", () => {
|
85
|
-
clearInterval(pinging);
|
86
|
-
});
|
87
|
-
|
88
|
-
const clientAddress =
|
89
|
-
(req.headers["x-forwarded-for"] as string | undefined)
|
90
|
-
?.split(",")[0]
|
91
|
-
?.trim() || req.socket.remoteAddress;
|
92
|
-
|
93
|
-
const clientId = clientAddress + "@" + new Date().toISOString();
|
94
|
-
|
95
|
-
localNode.syncManager.addPeer(
|
96
|
-
createWebSocketPeer({
|
97
|
-
id: clientId,
|
98
|
-
role: "client",
|
99
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
100
|
-
websocket: ws as any, // TODO: fix types
|
101
|
-
expectPings: false,
|
102
|
-
batchingByDefault: false
|
103
|
-
}),
|
104
|
-
);
|
105
|
-
|
106
|
-
ws.on("error", (e) =>
|
107
|
-
console.error(`Error on connection ${clientId}:`, e),
|
108
|
-
);
|
109
|
-
});
|
110
|
-
|
111
|
-
server.on("upgrade", function upgrade(req, socket, head) {
|
112
|
-
if (req.url !== "/health") {
|
113
|
-
wss.handleUpgrade(req, socket, head, function done(ws) {
|
114
|
-
wss.emit("connection", ws, req);
|
115
|
-
});
|
116
|
-
}
|
117
|
-
});
|
118
|
-
|
119
|
-
server.listen(parseInt(port));
|
120
|
-
|
121
|
-
// Keep the server up
|
122
|
-
yield* Effect.never;
|
123
|
-
});
|
124
|
-
},
|
125
|
-
);
|