om-memory-system 5.9.0 → 5.9.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.
@@ -5,8 +5,8 @@
5
5
  //
6
6
  // node omms-launch.mjs [--at-least-own-version] <om-memory-system arguments>
7
7
  //
8
- // Candidates: the copy named in ~/.omms/runtime.json, the global install beside
9
- // the running Node.js, and the copy that holds this file. With
8
+ // Candidates: the global install beside the running Node.js, the copy named in
9
+ // ~/.omms/runtime.json, and the copy that holds this file. On a tie the first wins. With
10
10
  // `--at-least-own-version`, the version in the `package.json` beside this file is
11
11
  // the minimum. When no candidate reaches it, the launcher runs
12
12
  // `npx --yes om-memory-system@<that version>`.
@@ -96,7 +96,11 @@ export function globalPackageRoots(execPath, platform = process.platform) {
96
96
  return brew ? [direct, join(brew[1], "lib", "node_modules", "om-memory-system")] : [direct];
97
97
  }
98
98
 
99
- /** The record's copy, the global installs, and the own copy, in that order. Invalid ones are null. */
99
+ /**
100
+ * The global installs, the record's copy, and the own copy, in that order. Invalid
101
+ * ones are null. On a tie the global install wins, so a host's cache copy does not
102
+ * keep serving after the global install catches up.
103
+ */
100
104
  export function findCandidates({ dir, execPath, ownRoot }) {
101
105
  let recorded = null;
102
106
  try {
@@ -106,7 +110,7 @@ export function findCandidates({ dir, execPath, ownRoot }) {
106
110
  /* No record, or an unreadable one: the other candidates still work. */
107
111
  }
108
112
  const globals = globalPackageRoots(execPath).map((root) => readCopy(root));
109
- return [recorded, ...globals, ownRoot ? readCopy(ownRoot) : null];
113
+ return [...globals, recorded, ownRoot ? readCopy(ownRoot) : null];
110
114
  }
111
115
 
112
116
  /**
@@ -130,6 +130,17 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
130
130
  process.once("SIGTERM", stop);
131
131
  // The page's Stop and Restart buttons reach the same process through this callback.
132
132
  const { createPowerAction } = await import("./web-power.js");
133
+ // A restart whose copy failed serves again from this process.
134
+ // Safe to call twice: it replaces the keeper and the signal handlers instead of adding more.
135
+ const resumeServer = async () => {
136
+ clearInterval(keeper);
137
+ keeper = setInterval(() => { }, 60_000);
138
+ process.off("SIGINT", stop);
139
+ process.off("SIGTERM", stop);
140
+ process.once("SIGINT", stop);
141
+ process.once("SIGTERM", stop);
142
+ await server.start();
143
+ };
133
144
  const powerAction = createPowerAction({
134
145
  stopServer: async () => {
135
146
  clearInterval(keeper);
@@ -137,13 +148,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
137
148
  process.off("SIGTERM", stop);
138
149
  await server.stop();
139
150
  },
140
- // A restart whose copy failed serves again from this process.
141
- resumeServer: async () => {
142
- keeper = setInterval(() => { }, 60_000);
143
- process.once("SIGINT", stop);
144
- process.once("SIGTERM", stop);
145
- await server.start();
146
- },
151
+ resumeServer,
147
152
  baseUrl: server.getUrl(),
148
153
  loginItem: action === "--login-item",
149
154
  autostart: options,
@@ -157,11 +162,22 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
157
162
  // Every restart onto the new copy runs the launcher, the login item's included.
158
163
  const canRestart = () => existsSync(launcherPath(ommsDir()));
159
164
  server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart, () => server.powerActionRunning));
160
- // A newer `web install` asked this web app to give up the port.
165
+ // A newer caller asked this web app to give up the port. If the caller's copy
166
+ // never takes it, restart onto the newest copy instead of leaving it empty.
167
+ const { leaveAfterStepAside } = await import("./web-step-aside.js");
168
+ const { log } = await import("../services/logger.js");
161
169
  server.setOnStepAside(async () => {
162
170
  clearInterval(keeper);
163
171
  await server.stop();
164
- process.exit(0);
172
+ await leaveAfterStepAside({
173
+ ownerAnswers: () => server.checkServerAvailable(),
174
+ restartNewest: () => powerAction("update"),
175
+ resume: resumeServer,
176
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
177
+ now: () => Date.now(),
178
+ exit: (code) => process.exit(code),
179
+ log,
180
+ });
165
181
  });
166
182
  return 0;
167
183
  }
@@ -0,0 +1,17 @@
1
+ import type { LogLevel } from "../services/logger.js";
2
+ /** How long the old web app waits for the caller's copy to answer. */
3
+ export declare const NEW_OWNER_WAIT_MS = 30000;
4
+ export interface StepAsideDeps {
5
+ /** True when an OMMS web app answers on the configured port. */
6
+ ownerAnswers: () => Promise<boolean>;
7
+ /** Restart onto the newest copy through the launcher. Returns a failure code when it did not. */
8
+ restartNewest: () => Promise<string | undefined>;
9
+ /** Serve again from this process. */
10
+ resume: () => Promise<void>;
11
+ sleep: (ms: number) => Promise<void>;
12
+ now: () => number;
13
+ exit: (code: number) => void;
14
+ log: (message: string, data: Record<string, unknown>, level?: LogLevel) => void;
15
+ }
16
+ /** Call after this web app stopped serving. It never leaves the port empty when it can help it. */
17
+ export declare function leaveAfterStepAside(deps: StepAsideDeps): Promise<void>;
@@ -0,0 +1,34 @@
1
+ // What a standalone web app does after it stops serving for a newer caller. The
2
+ // caller starts the newer copy, but a caller can quit first (a host that restarts
3
+ // for its own update), which left the port empty until some other host started a
4
+ // web app. So the old web app waits for a new owner, and restarts onto the newest
5
+ // copy itself when none comes.
6
+ /** How long the old web app waits for the caller's copy to answer. */
7
+ export const NEW_OWNER_WAIT_MS = 30_000;
8
+ const POLL_MS = 500;
9
+ /** Call after this web app stopped serving. It never leaves the port empty when it can help it. */
10
+ export async function leaveAfterStepAside(deps) {
11
+ const began = deps.now();
12
+ while (deps.now() - began < NEW_OWNER_WAIT_MS) {
13
+ if (await deps.ownerAnswers()) {
14
+ deps.exit(0);
15
+ return;
16
+ }
17
+ await deps.sleep(POLL_MS);
18
+ }
19
+ deps.log("Web app step-aside", { code: "no-new-owner" }, "warn");
20
+ // A successful restart exits this process.
21
+ const code = await deps.restartNewest();
22
+ if (code === undefined)
23
+ return;
24
+ // A failed restart can already serve again from this process.
25
+ if (await deps.ownerAnswers())
26
+ return;
27
+ try {
28
+ await deps.resume();
29
+ }
30
+ catch {
31
+ deps.log("Web app step-aside", { code: "resume-failed" }, "error");
32
+ deps.exit(1);
33
+ }
34
+ }
@@ -37,7 +37,7 @@ export interface WebUpdateDeps {
37
37
  shell: boolean;
38
38
  path?: string;
39
39
  }) => InstallChild;
40
- /** The version of the global install after npm ran, or null. */
40
+ /** The version of the global install (also read before npm runs), or null. */
41
41
  globalVersion: () => string | null;
42
42
  /** False when no restart could serve the new copy, such as a missing launcher. Checked before npm runs. */
43
43
  canRestart: () => boolean;
@@ -80,6 +80,11 @@ export declare class WebUpdate {
80
80
  /** Read npm `latest`. A failed read keeps the last result. Calls during a read share it. */
81
81
  check(): Promise<void>;
82
82
  private read;
83
+ /**
84
+ * The older of the running copy and the global install. A web app can run from a
85
+ * host's cache copy that is already current while the global install stays behind.
86
+ */
87
+ private installedVersion;
83
88
  /** Start the install the Update button starts, once a check found a release. */
84
89
  private maybeAutoInstall;
85
90
  status(): UpdateStatus;
@@ -1,5 +1,6 @@
1
1
  import { posix, win32 } from "node:path";
2
2
  import { availableUpdate, latestNpmVersion } from "./update-check.js";
3
+ import { compareVersions } from "./version-compare.js";
3
4
  // The web app's npm release check. It reads npm `latest` at start, every 10
4
5
  // minutes, and when an open page reads the status and the last check is over a
5
6
  // minute old. It keeps the result in memory for `GET /api/web/status`. It imports
@@ -58,9 +59,19 @@ export class WebUpdate {
58
59
  this.deps.log("Web app update check failed", { code: "unreachable" }, "error");
59
60
  return;
60
61
  }
61
- this.available = availableUpdate(this.deps.version, latest);
62
+ this.available = availableUpdate(this.installedVersion(), latest);
62
63
  this.maybeAutoInstall();
63
64
  }
65
+ /**
66
+ * The older of the running copy and the global install. A web app can run from a
67
+ * host's cache copy that is already current while the global install stays behind.
68
+ */
69
+ installedVersion() {
70
+ const global = this.deps.globalVersion();
71
+ return global !== null && compareVersions(global, this.deps.version) === -1
72
+ ? global
73
+ : this.deps.version;
74
+ }
64
75
  /** Start the install the Update button starts, once a check found a release. */
65
76
  maybeAutoInstall() {
66
77
  if (this.state === "installing" || this.state === "restarting")