@peerbit/server 1.1.1 → 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,
@@ -11,33 +11,64 @@ import { waitFor } from "@peerbit/time";
11
11
  import { v4 as uuid } from "uuid";
12
12
  import {
13
13
  checkExistPath,
14
- getConfigDir,
14
+ getHomeConfigDir,
15
15
  getCredentialsPath,
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";
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";
49
+ import { dirname } from "path";
50
+ import { fileURLToPath } from "url";
51
+ import { Level } from "level";
52
+ import { MemoryLevel } from "memory-level";
53
+
54
+ const __dirname = dirname(fileURLToPath(import.meta.url));
55
+
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
+ };
35
64
 
36
- export const createPassword = async (): Promise<string> => {
37
- const fs = await import("fs");
38
- const configDir = await getConfigDir();
65
+ export const createPassword = async (
66
+ configDirectory: string,
67
+ password?: string
68
+ ): Promise<string> => {
69
+ const configDir = configDirectory ?? (await getHomeConfigDir());
39
70
  const credentialsPath = await getCredentialsPath(configDir);
40
- if (await checkExistPath(credentialsPath)) {
71
+ if (!password && (await checkExistPath(credentialsPath))) {
41
72
  throw new Error(
42
73
  "Config path for credentials: " + credentialsPath + ", already exist"
43
74
  );
@@ -49,46 +80,99 @@ export const createPassword = async (): Promise<string> => {
49
80
 
50
81
  console.log(`Created config folder ${configDir}`);
51
82
 
52
- const password = uuid();
83
+ password = password || uuid();
53
84
  fs.writeFileSync(
54
85
  credentialsPath,
55
86
  JSON.stringify({ username: "admin", password })
56
87
  );
57
88
  console.log(`Created credentials at ${credentialsPath}`);
58
- return password;
89
+ return password!;
59
90
  };
60
91
 
61
- export const loadOrCreatePassword = async (): Promise<string> => {
62
- try {
63
- return await loadPassword();
64
- } catch (error) {
65
- if (error instanceof NotFoundError) {
66
- 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;
67
104
  }
68
- throw error;
105
+ } else {
106
+ return createPassword(configDirectory, password);
69
107
  }
70
108
  };
71
109
  export const startServerWithNode = async (properties: {
72
110
  directory?: string;
73
111
  domain?: string;
74
112
  bootstrap?: boolean;
113
+ newSession?: boolean;
114
+ password?: string;
115
+ ports?: {
116
+ node: number;
117
+ api: number;
118
+ };
119
+ restart?: () => void;
75
120
  }) => {
76
121
  const peer = await create({
77
- directory: properties.directory,
122
+ directory:
123
+ properties.directory != null
124
+ ? getNodePath(properties.directory)
125
+ : undefined,
78
126
  domain: properties.domain,
127
+ listenPort: properties.ports?.node,
79
128
  });
80
129
 
81
130
  if (properties.bootstrap) {
82
131
  await peer.bootstrap();
83
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
+ }
84
160
 
85
- 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
+ });
86
170
  const printNodeInfo = async () => {
87
171
  console.log("Starting node with address(es): ");
88
- const id = await (await client()).peer.id.get();
172
+ const id = peer.peerId.toString();
89
173
  console.log("id: " + id);
90
174
  console.log("Addresses: ");
91
- for (const a of await (await client()).peer.addresses.get()) {
175
+ for (const a of peer.getMultiaddrs()) {
92
176
  console.log(a.toString());
93
177
  }
94
178
  };
@@ -96,16 +180,26 @@ export const startServerWithNode = async (properties: {
96
180
  await printNodeInfo();
97
181
  const shutDownHook = async (
98
182
  controller: { stop: () => any },
99
- server: {
100
- close: () => void;
101
- }
183
+ server: http.Server
102
184
  ) => {
103
- const { exit } = await import("process");
104
- process.on("SIGINT", async () => {
105
- console.log("Shutting down node");
106
- await server.close();
107
- await controller.stop();
108
- 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
+ }
109
203
  });
110
204
  };
111
205
  await shutDownHook(peer, server);
@@ -128,14 +222,78 @@ const getProgramFromPath = (
128
222
  throw new Error("Invalid path");
129
223
  }
130
224
  const address = decodeURIComponent(path[pathIndex]);
131
- return client.handler.items.get(address);
225
+ return client.handler?.items.get(address);
132
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);
133
243
 
134
- 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 (
135
256
  client: ProgramClient,
136
- port: number = LOCAL_PORT
257
+ options: {
258
+ configDirectory: string;
259
+ session?: Session;
260
+ port?: number;
261
+ password?: string;
262
+ }
137
263
  ): Promise<http.Server> => {
138
- 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
+ };
139
297
 
140
298
  const adminACL = (req: http.IncomingMessage): boolean => {
141
299
  const auth = req.headers["authorization"];
@@ -198,9 +356,8 @@ export const startServer = async (
198
356
  switch (req.method) {
199
357
  case "GET":
200
358
  try {
201
- const keys = JSON.stringify([
202
- ...(client as Peerbit).handler.items.keys(),
203
- ]);
359
+ const ref = (client as Peerbit).handler?.items?.keys() || [];
360
+ const keys = JSON.stringify([...ref]);
204
361
  res.setHeader("Content-Type", "application/json");
205
362
  res.writeHead(200);
206
363
  res.end(keys);
@@ -244,11 +401,16 @@ export const startServer = async (
244
401
 
245
402
  const program = getProgramFromPath(client as Peerbit, req, 1);
246
403
  if (program) {
404
+ let closed = false;
247
405
  if (queryData === "true") {
248
- await program.drop();
406
+ closed = await program.drop();
249
407
  } else {
250
- await program.close();
408
+ closed = await program.close();
409
+ }
410
+ if (closed) {
411
+ await options?.session?.programs.remove(program.address);
251
412
  }
413
+
252
414
  res.writeHead(200);
253
415
  res.end();
254
416
  } else {
@@ -287,7 +449,18 @@ export const startServer = async (
287
449
  }
288
450
  client
289
451
  .open(program) // TODO all users to pass args
290
- .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
+ );
291
464
  res.writeHead(200);
292
465
  res.end(program.address.toString());
293
466
  })
@@ -310,30 +483,60 @@ export const startServer = async (
310
483
  switch (req.method) {
311
484
  case "PUT":
312
485
  getBody(req, async (body) => {
313
- const name = body;
486
+ const installArgs: InstallDependency = JSON.parse(body);
314
487
 
315
- let packageName = name;
316
- if (name.endsWith(".tgz")) {
317
- 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;
318
504
  }
319
505
 
320
- if (!name || name.length === 0) {
506
+ if (!installName || installName.length === 0) {
321
507
  res.writeHead(400);
322
- res.end("Invalid package: " + name);
508
+ res.end("Invalid package: " + packageName);
323
509
  } else {
324
- const child_process = await import("child_process");
325
510
  try {
326
- child_process.execSync(
327
- `npm install ${name} --no-save --no-package-lock`
511
+ // TODO do this without sudo. i.e. for servers provide arguments so that this app folder is writeable by default by the user
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`
328
530
  ); // TODO omit=dev ? but this makes breaks the tests after running once?
329
531
  } catch (error: any) {
330
532
  res.writeHead(400);
331
533
  res.end(
332
- "Failed ot install library: " +
333
- name +
534
+ "Failed to install library: " +
535
+ packageName +
334
536
  ". " +
335
537
  error.toString()
336
538
  );
539
+ clear?.();
337
540
  return;
338
541
  }
339
542
 
@@ -347,6 +550,10 @@ export const startServer = async (
347
550
  await import(
348
551
  /* webpackIgnore: true */ /* @vite-ignore */ packageName
349
552
  );
553
+ await options?.session?.imports.add(
554
+ packageName,
555
+ new Uint8Array()
556
+ );
350
557
  const programsPost = getProgramFromVariants()?.map((x) =>
351
558
  getSchema(x)
352
559
  );
@@ -364,6 +571,7 @@ export const startServer = async (
364
571
  } catch (e: any) {
365
572
  res.writeHead(400);
366
573
  res.end(e.message.toString?.());
574
+ clear?.();
367
575
  }
368
576
  }
369
577
  });
@@ -386,6 +594,30 @@ export const startServer = async (
386
594
  res.end();
387
595
  break;
388
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
+
389
621
  default:
390
622
  r404();
391
623
  break;
@@ -425,6 +657,7 @@ export const startServer = async (
425
657
  });
426
658
  });
427
659
  });
660
+ await waitFor(() => server.listening);
428
661
  console.log("API available at port", port);
429
662
  return server;
430
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;