@peerbit/server 1.1.2 → 2.0.0

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/src/server.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from "http";
2
- import { fromBase64 } from "@peerbit/crypto";
2
+ import { fromBase64, sha256Base64Sync } from "@peerbit/crypto";
3
3
  import { deserialize } from "@dao-xyz/borsh";
4
4
  import {
5
5
  Program,
@@ -16,33 +16,59 @@ import {
16
16
  getPackageName,
17
17
  loadPassword,
18
18
  NotFoundError,
19
+ getNodePath,
19
20
  } from "./config.js";
20
21
  import { setMaxListeners } from "events";
21
22
  import { create } from "./peerbit.js";
22
23
  import { Peerbit } from "peerbit";
23
24
  import { getSchema } from "@dao-xyz/borsh";
24
- import { StartByBase64, StartByVariant, StartProgram } from "./types.js";
25
+ import {
26
+ InstallDependency,
27
+ StartByBase64,
28
+ StartByVariant,
29
+ StartProgram,
30
+ } from "./types.js";
25
31
  import {
26
32
  ADDRESS_PATH,
27
33
  BOOTSTRAP_PATH,
34
+ TERMINATE_PATH,
28
35
  INSTALL_PATH,
29
36
  LOCAL_PORT,
30
37
  PEER_ID_PATH,
31
38
  PROGRAMS_PATH,
32
39
  PROGRAM_PATH,
40
+ RESTART_PATH,
33
41
  } from "./routes.js";
34
- import { client } from "./client.js";
35
-
42
+ import { Session } from "./session.js";
43
+ import fs from "fs";
44
+ import { exit } from "process";
45
+ import { spawn, fork, execSync } from "child_process";
46
+ import tmp from "tmp";
47
+ import path from "path";
48
+ import { base58btc } from "multiformats/bases/base58";
36
49
  import { dirname } from "path";
37
50
  import { fileURLToPath } from "url";
51
+ import { Level } from "level";
52
+ import { MemoryLevel } from "memory-level";
38
53
 
39
54
  const __dirname = dirname(fileURLToPath(import.meta.url));
40
55
 
41
- export const createPassword = async (): Promise<string> => {
42
- const fs = await import("fs");
43
- const configDir = await getHomeConfigDir();
56
+ export const stopAndWait = (server: http.Server) => {
57
+ let closed = false;
58
+ server.on("close", () => {
59
+ closed = true;
60
+ });
61
+ server.close();
62
+ return waitFor(() => closed);
63
+ };
64
+
65
+ export const createPassword = async (
66
+ configDirectory: string,
67
+ password?: string
68
+ ): Promise<string> => {
69
+ const configDir = configDirectory ?? (await getHomeConfigDir());
44
70
  const credentialsPath = await getCredentialsPath(configDir);
45
- if (await checkExistPath(credentialsPath)) {
71
+ if (!password && (await checkExistPath(credentialsPath))) {
46
72
  throw new Error(
47
73
  "Config path for credentials: " + credentialsPath + ", already exist"
48
74
  );
@@ -54,46 +80,99 @@ export const createPassword = async (): Promise<string> => {
54
80
 
55
81
  console.log(`Created config folder ${configDir}`);
56
82
 
57
- const password = uuid();
83
+ password = password || uuid();
58
84
  fs.writeFileSync(
59
85
  credentialsPath,
60
86
  JSON.stringify({ username: "admin", password })
61
87
  );
62
88
  console.log(`Created credentials at ${credentialsPath}`);
63
- return password;
89
+ return password!;
64
90
  };
65
91
 
66
- export const loadOrCreatePassword = async (): Promise<string> => {
67
- try {
68
- return await loadPassword();
69
- } catch (error) {
70
- if (error instanceof NotFoundError) {
71
- return createPassword();
92
+ export const loadOrCreatePassword = async (
93
+ configDirectory: string,
94
+ password?: string
95
+ ): Promise<string> => {
96
+ if (!password) {
97
+ try {
98
+ return await loadPassword(configDirectory);
99
+ } catch (error) {
100
+ if (error instanceof NotFoundError) {
101
+ return createPassword(configDirectory, password);
102
+ }
103
+ throw error;
72
104
  }
73
- throw error;
105
+ } else {
106
+ return createPassword(configDirectory, password);
74
107
  }
75
108
  };
76
109
  export const startServerWithNode = async (properties: {
77
110
  directory?: string;
78
111
  domain?: string;
79
112
  bootstrap?: boolean;
113
+ newSession?: boolean;
114
+ password?: string;
115
+ ports?: {
116
+ node: number;
117
+ api: number;
118
+ };
119
+ restart?: () => void;
80
120
  }) => {
81
121
  const peer = await create({
82
- directory: properties.directory,
122
+ directory:
123
+ properties.directory != null
124
+ ? getNodePath(properties.directory)
125
+ : undefined,
83
126
  domain: properties.domain,
127
+ listenPort: properties.ports?.node,
84
128
  });
85
129
 
86
130
  if (properties.bootstrap) {
87
131
  await peer.bootstrap();
88
132
  }
133
+ const sessionDirectory =
134
+ properties.directory != null
135
+ ? path.join(properties.directory, "session")
136
+ : undefined;
137
+ const session = new Session(
138
+ sessionDirectory
139
+ ? new Level<string, Uint8Array>(sessionDirectory, {
140
+ valueEncoding: "view",
141
+ keyEncoding: "utf-8",
142
+ })
143
+ : new MemoryLevel({ valueEncoding: "view", keyEncoding: "utf-8" })
144
+ );
145
+ if (!properties.newSession) {
146
+ for (const [string] of await session.imports.all()) {
147
+ await import(string);
148
+ }
149
+ for (const [address] of await session.programs.all()) {
150
+ // TODO args
151
+ try {
152
+ await peer.open(address, { timeout: 3000 });
153
+ } catch (error) {
154
+ console.error(error);
155
+ }
156
+ }
157
+ } else {
158
+ await session.clear();
159
+ }
89
160
 
90
- const server = await startServer(peer);
161
+ const server = await startApiServer(peer, {
162
+ port: properties.ports?.api,
163
+ configDirectory:
164
+ properties.directory != null
165
+ ? path.join(properties.directory, "server")
166
+ : undefined || getHomeConfigDir(),
167
+ session,
168
+ password: properties.password,
169
+ });
91
170
  const printNodeInfo = async () => {
92
171
  console.log("Starting node with address(es): ");
93
- const id = await (await client()).peer.id.get();
172
+ const id = peer.peerId.toString();
94
173
  console.log("id: " + id);
95
174
  console.log("Addresses: ");
96
- for (const a of await (await client()).peer.addresses.get()) {
175
+ for (const a of peer.getMultiaddrs()) {
97
176
  console.log(a.toString());
98
177
  }
99
178
  };
@@ -101,16 +180,26 @@ export const startServerWithNode = async (properties: {
101
180
  await printNodeInfo();
102
181
  const shutDownHook = async (
103
182
  controller: { stop: () => any },
104
- server: {
105
- close: () => void;
106
- }
183
+ server: http.Server
107
184
  ) => {
108
- const { exit } = await import("process");
109
- process.on("SIGINT", async () => {
110
- console.log("Shutting down node");
111
- await server.close();
112
- await controller.stop();
113
- exit();
185
+ ["SIGTERM", "SIGINT", "SIGUSR1", "SIGUSR2"].forEach((code) => {
186
+ process.on(code, async () => {
187
+ if (server.listening) {
188
+ console.log("Shutting down node");
189
+ await stopAndWait(server);
190
+ await waitFor(() => closed);
191
+ await controller.stop();
192
+ }
193
+ exit();
194
+ });
195
+ });
196
+ process.on("exit", async () => {
197
+ if (server.listening) {
198
+ console.log("Shutting down node");
199
+ await stopAndWait(server);
200
+ await waitFor(() => closed);
201
+ await controller.stop();
202
+ }
114
203
  });
115
204
  };
116
205
  await shutDownHook(peer, server);
@@ -133,14 +222,78 @@ const getProgramFromPath = (
133
222
  throw new Error("Invalid path");
134
223
  }
135
224
  const address = decodeURIComponent(path[pathIndex]);
136
- return client.handler.items.get(address);
225
+ return client.handler?.items.get(address);
137
226
  };
227
+ function findPeerbitProgramFolder(inputDirectory: string): string | null {
228
+ let currentDir = path.resolve(inputDirectory);
229
+
230
+ while (currentDir !== "/") {
231
+ // Stop at the root directory
232
+ const nodeModulesPath = path.join(currentDir, "node_modules");
233
+ const packageJsonPath = path.join(
234
+ nodeModulesPath,
235
+ "@peerbit",
236
+ "program",
237
+ "package.json"
238
+ );
239
+
240
+ if (fs.existsSync(packageJsonPath)) {
241
+ const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
242
+ const packageData = JSON.parse(packageJsonContent);
138
243
 
139
- export const startServer = async (
244
+ if (packageData.name === "@peerbit/program") {
245
+ return currentDir;
246
+ }
247
+ }
248
+
249
+ currentDir = path.dirname(currentDir);
250
+ }
251
+
252
+ return null;
253
+ }
254
+
255
+ export const startApiServer = async (
140
256
  client: ProgramClient,
141
- port: number = LOCAL_PORT
257
+ options: {
258
+ configDirectory: string;
259
+ session?: Session;
260
+ port?: number;
261
+ password?: string;
262
+ }
142
263
  ): Promise<http.Server> => {
143
- const password = await loadOrCreatePassword();
264
+ const port = options?.port ?? LOCAL_PORT;
265
+ const password = await loadOrCreatePassword(
266
+ options.configDirectory,
267
+ options?.password
268
+ );
269
+
270
+ const restart = async () => {
271
+ await client.stop();
272
+ await stopAndWait(server);
273
+
274
+ // We filter out the reset command, since restarting means that we want to resume something
275
+ spawn(
276
+ process.argv.shift()!,
277
+ [
278
+ ...process.execArgv,
279
+ ...process.argv.filter((x) => x !== "--reset" && x !== "-r"),
280
+ ],
281
+ {
282
+ cwd: process.cwd(),
283
+ detached: true,
284
+ stdio: "inherit",
285
+ gid: process.getgid!(),
286
+ }
287
+ );
288
+
289
+ /* process.on("exit", async () => {
290
+ child.kill("SIGINT")
291
+ });
292
+ process.on("SIGINT", async () => {
293
+ child.kill("SIGINT")
294
+ }); */
295
+ process.exit(0);
296
+ };
144
297
 
145
298
  const adminACL = (req: http.IncomingMessage): boolean => {
146
299
  const auth = req.headers["authorization"];
@@ -203,9 +356,8 @@ export const startServer = async (
203
356
  switch (req.method) {
204
357
  case "GET":
205
358
  try {
206
- const keys = JSON.stringify([
207
- ...(client as Peerbit).handler.items.keys(),
208
- ]);
359
+ const ref = (client as Peerbit).handler?.items?.keys() || [];
360
+ const keys = JSON.stringify([...ref]);
209
361
  res.setHeader("Content-Type", "application/json");
210
362
  res.writeHead(200);
211
363
  res.end(keys);
@@ -249,11 +401,16 @@ export const startServer = async (
249
401
 
250
402
  const program = getProgramFromPath(client as Peerbit, req, 1);
251
403
  if (program) {
404
+ let closed = false;
252
405
  if (queryData === "true") {
253
- await program.drop();
406
+ closed = await program.drop();
254
407
  } else {
255
- await program.close();
408
+ closed = await program.close();
409
+ }
410
+ if (closed) {
411
+ await options?.session?.programs.remove(program.address);
256
412
  }
413
+
257
414
  res.writeHead(200);
258
415
  res.end();
259
416
  } else {
@@ -292,7 +449,18 @@ export const startServer = async (
292
449
  }
293
450
  client
294
451
  .open(program) // TODO all users to pass args
295
- .then((program) => {
452
+ .then(async (program) => {
453
+ // TODO what if this is a reopen?
454
+ console.log(
455
+ "OPEN ADDRESS",
456
+ program.address,
457
+ (client as Peerbit).directory,
458
+ await client.services.blocks.has(program.address)
459
+ );
460
+ await options?.session?.programs.add(
461
+ program.address,
462
+ new Uint8Array()
463
+ );
296
464
  res.writeHead(200);
297
465
  res.end(program.address.toString());
298
466
  })
@@ -315,31 +483,60 @@ export const startServer = async (
315
483
  switch (req.method) {
316
484
  case "PUT":
317
485
  getBody(req, async (body) => {
318
- const name = body;
486
+ const installArgs: InstallDependency = JSON.parse(body);
319
487
 
320
- let packageName = name;
321
- if (name.endsWith(".tgz")) {
322
- packageName = await getPackageName(name);
488
+ const packageName = installArgs.name; // @abc/123
489
+ let installName = installArgs.name; // abc123.tgz or @abc/123 (npm package name)
490
+ let clear: (() => void) | undefined;
491
+ if (installArgs.type === "tgz") {
492
+ const binary = fromBase64(installArgs.base64);
493
+ const tempFile = tmp.fileSync({
494
+ name:
495
+ base58btc.encode(Buffer.from(installName)) +
496
+ uuid() +
497
+ ".tgz",
498
+ });
499
+ fs.writeFileSync(tempFile.fd, binary);
500
+ clear = () => tempFile.removeCallback();
501
+ installName = tempFile.name;
502
+ } else {
503
+ clear = undefined;
323
504
  }
324
505
 
325
- if (!name || name.length === 0) {
506
+ if (!installName || installName.length === 0) {
326
507
  res.writeHead(400);
327
- res.end("Invalid package: " + name);
508
+ res.end("Invalid package: " + packageName);
328
509
  } else {
329
- const child_process = await import("child_process");
330
510
  try {
331
511
  // TODO do this without sudo. i.e. for servers provide arguments so that this app folder is writeable by default by the user
332
- child_process.execSync(
333
- `sudo npm install ${name} --prefix ${__dirname} --no-save --no-package-lock`
512
+ const installDir =
513
+ process.env.PEERBIT_MODULES_PATH ||
514
+ findPeerbitProgramFolder(__dirname);
515
+ let permission = "";
516
+ if (!installDir) {
517
+ res.writeHead(400);
518
+ res.end("Missing installation directory");
519
+ return;
520
+ }
521
+ try {
522
+ fs.accessSync(installDir, fs.constants.W_OK);
523
+ } catch (error) {
524
+ permission = "sudo";
525
+ }
526
+
527
+ console.log("Installing package: " + installName);
528
+ execSync(
529
+ `${permission} npm install ${installName} --prefix ${installDir} --no-save --no-package-lock`
334
530
  ); // TODO omit=dev ? but this makes breaks the tests after running once?
335
531
  } catch (error: any) {
336
532
  res.writeHead(400);
337
533
  res.end(
338
- "Failed ot install library: " +
339
- name +
534
+ "Failed to install library: " +
535
+ packageName +
340
536
  ". " +
341
537
  error.toString()
342
538
  );
539
+ clear?.();
343
540
  return;
344
541
  }
345
542
 
@@ -353,6 +550,10 @@ export const startServer = async (
353
550
  await import(
354
551
  /* webpackIgnore: true */ /* @vite-ignore */ packageName
355
552
  );
553
+ await options?.session?.imports.add(
554
+ packageName,
555
+ new Uint8Array()
556
+ );
356
557
  const programsPost = getProgramFromVariants()?.map((x) =>
357
558
  getSchema(x)
358
559
  );
@@ -370,6 +571,7 @@ export const startServer = async (
370
571
  } catch (e: any) {
371
572
  res.writeHead(400);
372
573
  res.end(e.message.toString?.());
574
+ clear?.();
373
575
  }
374
576
  }
375
577
  });
@@ -392,6 +594,30 @@ export const startServer = async (
392
594
  res.end();
393
595
  break;
394
596
 
597
+ default:
598
+ r404();
599
+ break;
600
+ }
601
+ } else if (req.url.startsWith(RESTART_PATH)) {
602
+ switch (req.method) {
603
+ case "POST":
604
+ res.writeHead(200);
605
+ res.end();
606
+ restart();
607
+ break;
608
+
609
+ default:
610
+ r404();
611
+ break;
612
+ }
613
+ } else if (req.url.startsWith(TERMINATE_PATH)) {
614
+ switch (req.method) {
615
+ case "POST":
616
+ res.writeHead(200);
617
+ res.end();
618
+ process.exit(0);
619
+ break;
620
+
395
621
  default:
396
622
  r404();
397
623
  break;
@@ -431,6 +657,7 @@ export const startServer = async (
431
657
  });
432
658
  });
433
659
  });
660
+ await waitFor(() => server.listening);
434
661
  console.log("API available at port", port);
435
662
  return server;
436
663
  };
package/src/session.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { AbstractLevel } from "abstract-level";
2
+
3
+ export class Session {
4
+ programs: KV;
5
+ imports: KV;
6
+ constructor(
7
+ readonly level: AbstractLevel<
8
+ string | Buffer | Uint8Array,
9
+ string,
10
+ Uint8Array
11
+ >
12
+ ) {
13
+ this.imports = new KV(
14
+ this.level.sublevel<string, Uint8Array>("imports", {
15
+ keyEncoding: "utf8",
16
+ valueEncoding: "view",
17
+ })
18
+ );
19
+ this.programs = new KV(
20
+ this.level.sublevel<string, Uint8Array>("programs", {
21
+ keyEncoding: "utf8",
22
+ valueEncoding: "view",
23
+ })
24
+ );
25
+ }
26
+
27
+ async clear() {
28
+ await this.imports.clear();
29
+ await this.programs.clear();
30
+ }
31
+ }
32
+
33
+ export class KV {
34
+ constructor(
35
+ readonly level: AbstractLevel<
36
+ string | Buffer | Uint8Array,
37
+ string,
38
+ Uint8Array
39
+ >
40
+ ) {}
41
+
42
+ add(key: string, arg: Uint8Array) {
43
+ return this.level.put(key, arg);
44
+ }
45
+
46
+ remove(key: string) {
47
+ return this.level.del(key);
48
+ }
49
+
50
+ async all(): Promise<[string, Uint8Array][]> {
51
+ const res: [string, Uint8Array][] = [];
52
+ for await (const [key, value] of this.level.iterator()) {
53
+ res.push([key, value]);
54
+ }
55
+ return res;
56
+ }
57
+
58
+ async open() {
59
+ await this.level.open();
60
+ }
61
+
62
+ async close() {
63
+ await this.level.close();
64
+ }
65
+
66
+ async clear() {
67
+ await this.level.clear();
68
+ }
69
+ }
package/src/types.ts CHANGED
@@ -5,3 +5,16 @@ export interface StartByBase64 {
5
5
  base64: string;
6
6
  }
7
7
  export type StartProgram = StartByVariant | StartByBase64;
8
+
9
+ export interface InstallWithTGZ {
10
+ type: "tgz";
11
+ name: string;
12
+ base64: string;
13
+ }
14
+
15
+ export interface InstallWithNPM {
16
+ type: "npm";
17
+ name: string;
18
+ }
19
+
20
+ export type InstallDependency = InstallWithTGZ | InstallWithNPM;