amalgm 0.1.166 → 0.1.168
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/lib/supervisor.js
CHANGED
|
@@ -47,6 +47,7 @@ const {
|
|
|
47
47
|
runtimeServiceScripts,
|
|
48
48
|
} = require('./runtime-manifest');
|
|
49
49
|
const {
|
|
50
|
+
UPDATE_TARGET_MISMATCH,
|
|
50
51
|
checkForUpdate,
|
|
51
52
|
shouldAutoUpdate,
|
|
52
53
|
updatePackage,
|
|
@@ -58,19 +59,6 @@ const PORT_ALLOCATION_LOCK_FILE = path.join(PORT_ALLOCATION_LOCK_DIR, 'owner.jso
|
|
|
58
59
|
const PORT_ALLOCATION_LOCK_TIMEOUT_MS = 45_000;
|
|
59
60
|
const PORT_ALLOCATION_LOCK_STALE_MS = 120_000;
|
|
60
61
|
|
|
61
|
-
// npm installs land in the npm prefix, which is only the tree this process runs
|
|
62
|
-
// from when the runtime was started from that prefix. When the service points at
|
|
63
|
-
// another install (e.g. the CLI bundled inside the desktop app), the update can
|
|
64
|
-
// never apply by restarting, so restarting would loop forever.
|
|
65
|
-
function installedPackageVersionOnDisk() {
|
|
66
|
-
try {
|
|
67
|
-
const raw = fs.readFileSync(path.join(PACKAGE_ROOT_DIR, 'package.json'), 'utf8');
|
|
68
|
-
return JSON.parse(raw).version || null;
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
62
|
const CHILD_RESTART_POLICY = {
|
|
75
63
|
initialDelayMs: 1000,
|
|
76
64
|
maxDelayMs: 30000,
|
|
@@ -838,6 +826,7 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
|
|
|
838
826
|
if (stopped || inFlight || installedUpdate) return;
|
|
839
827
|
inFlight = true;
|
|
840
828
|
const checkedAt = new Date().toISOString();
|
|
829
|
+
let check = null;
|
|
841
830
|
try {
|
|
842
831
|
writeAutoUpdateState({
|
|
843
832
|
status: 'checking',
|
|
@@ -848,7 +837,7 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
|
|
|
848
837
|
last_checked_at: checkedAt,
|
|
849
838
|
last_error: null,
|
|
850
839
|
});
|
|
851
|
-
|
|
840
|
+
check = await checkForUpdate({ currentVersion: PACKAGE_VERSION });
|
|
852
841
|
if (!check.updateAvailable) {
|
|
853
842
|
writeAutoUpdateState({
|
|
854
843
|
status: 'current',
|
|
@@ -893,10 +882,13 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
|
|
|
893
882
|
if (trimmed) log(trimmed, stream);
|
|
894
883
|
},
|
|
895
884
|
});
|
|
896
|
-
|
|
897
|
-
|
|
885
|
+
installedUpdate = true;
|
|
886
|
+
await waitForIdleAndRestart(check);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
// Restarting cannot apply an update that landed outside the tree this
|
|
889
|
+
// process runs from, so restarts are suppressed to avoid a restart loop.
|
|
890
|
+
if (error?.code === UPDATE_TARGET_MISMATCH && check) {
|
|
898
891
|
suppressedTarget = check.targetVersion;
|
|
899
|
-
const reason = `installed ${check.targetVersion}, but this runtime still runs ${diskVersion || PACKAGE_VERSION} from ${PACKAGE_ROOT_DIR}; restart suppressed to avoid a restart loop`;
|
|
900
892
|
writeAutoUpdateState({
|
|
901
893
|
status: 'restart_suppressed',
|
|
902
894
|
current_version: PACKAGE_VERSION,
|
|
@@ -905,14 +897,11 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
|
|
|
905
897
|
critical: isCriticalUpdate(check),
|
|
906
898
|
service_managed: serviceManaged,
|
|
907
899
|
last_checked_at: checkedAt,
|
|
908
|
-
last_error:
|
|
900
|
+
last_error: error.message,
|
|
909
901
|
});
|
|
910
|
-
log(
|
|
902
|
+
log(error.message, 'stderr');
|
|
911
903
|
return;
|
|
912
904
|
}
|
|
913
|
-
installedUpdate = true;
|
|
914
|
-
await waitForIdleAndRestart(check);
|
|
915
|
-
} catch (error) {
|
|
916
905
|
writeAutoUpdateState({
|
|
917
906
|
status: 'error',
|
|
918
907
|
current_version: PACKAGE_VERSION,
|
package/lib/updater.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const {
|
|
7
8
|
AMALGM_RUNTIME_LABEL,
|
|
9
|
+
PACKAGE_ROOT,
|
|
8
10
|
} = require('./paths');
|
|
9
11
|
const {
|
|
10
12
|
npmTagForRuntimeLabel,
|
|
11
13
|
} = require('./runtime-identity');
|
|
12
14
|
|
|
13
15
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
16
|
+
const UPDATE_TARGET_MISMATCH = 'AMALGM_UPDATE_TARGET_MISMATCH';
|
|
14
17
|
const DEFAULT_VIEW_TIMEOUT_MS = 15_000;
|
|
15
18
|
const DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60_000;
|
|
16
19
|
const DEFAULT_NPM_UPDATE_CACHE = path.join(os.tmpdir(), 'amalgm-npm-cache');
|
|
@@ -64,6 +67,41 @@ function npmCommand() {
|
|
|
64
67
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// npm lays out global installs as <prefix>/lib/node_modules/<name> on POSIX and
|
|
71
|
+
// <prefix>/node_modules/<name> on Windows. Walking that layout backwards from
|
|
72
|
+
// the package root this process runs from recovers the prefix that owns the
|
|
73
|
+
// running install, so updates land in the tree the service actually boots —
|
|
74
|
+
// not whatever prefix the inherited environment points at (toolchain dirs on
|
|
75
|
+
// Fly machines, the desktop app bundle on Mac). Returns null when the root is
|
|
76
|
+
// not a global-install layout (e.g. a repo checkout).
|
|
77
|
+
function globalPrefixForPackageRoot(packageRoot = PACKAGE_ROOT) {
|
|
78
|
+
const root = path.resolve(packageRoot);
|
|
79
|
+
const nodeModules = path.dirname(root);
|
|
80
|
+
if (path.basename(nodeModules) !== 'node_modules') return null;
|
|
81
|
+
const parent = path.dirname(nodeModules);
|
|
82
|
+
return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function packageVersionOnDisk(packageRoot = PACKAGE_ROOT) {
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(path.join(path.resolve(packageRoot), 'package.json'), 'utf8');
|
|
88
|
+
return JSON.parse(raw).version || null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function npmInstallArgs(tag, options = {}) {
|
|
95
|
+
const args = ['install', '-g', `amalgm@${tag}`];
|
|
96
|
+
const prefix = options.npmPrefix !== undefined
|
|
97
|
+
? options.npmPrefix
|
|
98
|
+
: globalPrefixForPackageRoot(options.packageRoot);
|
|
99
|
+
// The --prefix flag outranks every npm config layer, including an inherited
|
|
100
|
+
// npm_config_prefix pointing at a tree this process does not run from.
|
|
101
|
+
if (prefix) args.push('--prefix', prefix);
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
|
|
67
105
|
function npmInstallTag(options = {}) {
|
|
68
106
|
const explicit = options.tag || options.channel;
|
|
69
107
|
if (explicit && explicit !== true) return String(explicit).replace(/^@/, '');
|
|
@@ -157,7 +195,7 @@ async function npmPackageVersionForTag(tag, options = {}) {
|
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
async function installNpmPackageTag(tag, options = {}) {
|
|
160
|
-
const result = await runNpm(
|
|
198
|
+
const result = await runNpm(npmInstallArgs(tag, options), {
|
|
161
199
|
timeoutMs: options.timeoutMs || DEFAULT_INSTALL_TIMEOUT_MS,
|
|
162
200
|
env: options.env,
|
|
163
201
|
onOutput: options.onOutput,
|
|
@@ -187,6 +225,18 @@ async function updatePackage(options = {}) {
|
|
|
187
225
|
const check = options.check || await checkForUpdate(options);
|
|
188
226
|
if (!check.updateAvailable) return { ...check, updated: false };
|
|
189
227
|
await installNpmPackageTag(check.tag, options);
|
|
228
|
+
const packageRoot = path.resolve(options.packageRoot || PACKAGE_ROOT);
|
|
229
|
+
const diskVersion = packageVersionOnDisk(packageRoot);
|
|
230
|
+
if (diskVersion !== check.targetVersion) {
|
|
231
|
+
const error = new Error(
|
|
232
|
+
`npm reported installing amalgm@${check.tag} (${check.targetVersion}), but the install this runtime boots from at ${packageRoot} still carries ${diskVersion || 'no readable version'}. The update landed in a different npm prefix and will never take effect here.`,
|
|
233
|
+
);
|
|
234
|
+
error.code = UPDATE_TARGET_MISMATCH;
|
|
235
|
+
error.targetVersion = check.targetVersion;
|
|
236
|
+
error.installedVersion = diskVersion;
|
|
237
|
+
error.packageRoot = packageRoot;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
190
240
|
return { ...check, updated: true };
|
|
191
241
|
}
|
|
192
242
|
|
|
@@ -211,14 +261,18 @@ module.exports = {
|
|
|
211
261
|
_test: {
|
|
212
262
|
npmCommand,
|
|
213
263
|
npmEnv,
|
|
264
|
+
npmInstallArgs,
|
|
214
265
|
npmSpawnOptions,
|
|
215
266
|
},
|
|
267
|
+
UPDATE_TARGET_MISMATCH,
|
|
216
268
|
checkForUpdate,
|
|
217
269
|
compareVersions,
|
|
218
270
|
envDisablesAutoUpdate,
|
|
271
|
+
globalPrefixForPackageRoot,
|
|
219
272
|
installNpmPackageTag,
|
|
220
273
|
npmInstallTag,
|
|
221
274
|
npmPackageVersionForTag,
|
|
275
|
+
packageVersionOnDisk,
|
|
222
276
|
parseSemver,
|
|
223
277
|
shouldAutoUpdate,
|
|
224
278
|
updatePackage,
|
package/package.json
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* a doc byte must outlive everything.
|
|
12
12
|
*
|
|
13
13
|
* Ownership rules the wire shape enforces:
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
14
|
+
* - The stable account id is stamped here from enrollment, never accepted
|
|
15
|
+
* from the publisher. Cross-account display identity is authenticated and
|
|
16
|
+
* added by the gateway, then passed through this state unchanged.
|
|
16
17
|
* - The blob is opaque. Only the surface bound to the same resource on the
|
|
17
18
|
* other end knows its coordinate system (text caret, canvas x,y, nothing).
|
|
18
19
|
* - Absence is the default presence must re-earn: entries live TTL_MS from
|
|
@@ -63,6 +64,7 @@ function peersOf(resource) {
|
|
|
63
64
|
return peers ? Array.from(peers.values(), (peer) => ({
|
|
64
65
|
session: peer.session,
|
|
65
66
|
user: peer.user,
|
|
67
|
+
...(peer.identity ? { identity: peer.identity } : {}),
|
|
66
68
|
blob: peer.blob,
|
|
67
69
|
at: peer.at,
|
|
68
70
|
activeAt: peer.activeAt,
|
|
@@ -223,6 +225,7 @@ function replaceCloudPresence(input) {
|
|
|
223
225
|
peers.set(peer.session, {
|
|
224
226
|
session: peer.session,
|
|
225
227
|
user: peer.user,
|
|
228
|
+
...(peer.identity && typeof peer.identity === 'object' ? { identity: peer.identity } : {}),
|
|
226
229
|
blob: peer.blob ?? null,
|
|
227
230
|
at: Number(peer.at) || now,
|
|
228
231
|
activeAt: Number(peer.activeAt) || now,
|
|
@@ -77,6 +77,7 @@ test('the runtime tunnel bridges presence in both directions without a durable q
|
|
|
77
77
|
peers: [{
|
|
78
78
|
session: 'ps_direct',
|
|
79
79
|
user: 'direct-user',
|
|
80
|
+
identity: { name: 'Direct Person', avatarUrl: 'https://example.com/direct.png' },
|
|
80
81
|
blob: { anchor: 8 },
|
|
81
82
|
at: 20,
|
|
82
83
|
activeAt: 20,
|
|
@@ -92,6 +93,7 @@ test('the runtime tunnel bridges presence in both directions without a durable q
|
|
|
92
93
|
peers: [{
|
|
93
94
|
session: 'ps_direct',
|
|
94
95
|
user: 'direct-user',
|
|
96
|
+
identity: { name: 'Direct Person', avatarUrl: 'https://example.com/direct.png' },
|
|
95
97
|
blob: { anchor: 8 },
|
|
96
98
|
at: 20,
|
|
97
99
|
activeAt: 20,
|
|
@@ -165,11 +165,22 @@ test('shared presence forwards local facts, merges cloud peers, and clears them
|
|
|
165
165
|
resource: localResource,
|
|
166
166
|
peers: [
|
|
167
167
|
{ session: 'ps_local', user: 'official-local', blob: null, at: 10, activeAt: 10 },
|
|
168
|
-
{
|
|
168
|
+
{
|
|
169
|
+
session: 'ps_remote',
|
|
170
|
+
user: 'remote-user',
|
|
171
|
+
identity: { name: 'Remote Person', avatarUrl: 'https://example.com/remote.png' },
|
|
172
|
+
blob: { anchor: 9 },
|
|
173
|
+
at: 11,
|
|
174
|
+
activeAt: 11,
|
|
175
|
+
},
|
|
169
176
|
],
|
|
170
177
|
});
|
|
171
178
|
assert.equal(publishes.length, 1, 'an inbound cloud roster never echoes back upstream');
|
|
172
179
|
assert.deepEqual(frames.at(-1).peers.map((peer) => peer.session).sort(), ['ps_local', 'ps_remote']);
|
|
180
|
+
assert.deepEqual(
|
|
181
|
+
frames.at(-1).peers.find((peer) => peer.session === 'ps_remote').identity,
|
|
182
|
+
{ name: 'Remote Person', avatarUrl: 'https://example.com/remote.png' },
|
|
183
|
+
);
|
|
173
184
|
|
|
174
185
|
await sharedRest.handleReplicaRevoked(
|
|
175
186
|
{ resourceId: cloudResourceId, reason: 'access-removed' },
|