@portel/photon 1.32.2 → 1.32.4
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/README.md +8 -4
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +56 -14
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.js +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +5 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +452 -175
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +34 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +57 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +2492 -1442
- package/dist/beam.bundle.js.map +4 -4
- package/dist/claude-code-plugin.js +9 -3
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +5 -0
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +12 -6
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +187 -489
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +2 -1
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +57 -29
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +120 -31
- package/dist/daemon/server.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +19 -8
- package/dist/loader.js.map +1 -1
- package/dist/photons/marketplace.photon.d.ts.map +1 -1
- package/dist/photons/marketplace.photon.js +34 -7
- package/dist/photons/marketplace.photon.js.map +1 -1
- package/dist/photons/marketplace.photon.ts +35 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -6
- package/dist/server.js.map +1 -1
- package/dist/types/server-types.d.ts +2 -0
- package/dist/types/server-types.d.ts.map +1 -1
- package/dist/version-notify.d.ts +5 -0
- package/dist/version-notify.d.ts.map +1 -1
- package/dist/version-notify.js +57 -7
- package/dist/version-notify.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +8 -3
- package/dist/watcher.js.map +1 -1
- package/package.json +89 -73
package/dist/daemon/server.js
CHANGED
|
@@ -49,36 +49,43 @@ const ownerFile = getOwnerFilePath(socketPath);
|
|
|
49
49
|
let daemonOwnershipConfirmed = false;
|
|
50
50
|
async function isSocketResponsive(target) {
|
|
51
51
|
// Windows named pipes have no filesystem entry; skip the FS gate on
|
|
52
|
-
// win32 and let
|
|
53
|
-
//
|
|
54
|
-
//
|
|
52
|
+
// win32 and let the socket probe the pipe directly. Register the
|
|
53
|
+
// error handler before connect() so Bun cannot surface ENOENT before
|
|
54
|
+
// listeners exist.
|
|
55
55
|
const isPipe = process.platform === 'win32' && target.startsWith('\\\\.\\pipe\\');
|
|
56
56
|
if (!isPipe && !fs.existsSync(target))
|
|
57
57
|
return false;
|
|
58
58
|
return new Promise((resolve) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
const client = new net.Socket();
|
|
60
|
+
let done = false;
|
|
61
|
+
const finish = (alive) => {
|
|
62
|
+
if (done)
|
|
63
|
+
return;
|
|
64
|
+
done = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
if (alive) {
|
|
67
|
+
client.end();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
client.destroy();
|
|
71
|
+
}
|
|
72
|
+
resolve(alive);
|
|
73
|
+
};
|
|
69
74
|
const timer = setTimeout(() => {
|
|
70
|
-
|
|
71
|
-
resolve(false);
|
|
75
|
+
finish(false);
|
|
72
76
|
}, 1000);
|
|
73
|
-
client.on('connect', () => {
|
|
74
|
-
clearTimeout(timer);
|
|
75
|
-
client.destroy();
|
|
76
|
-
resolve(true);
|
|
77
|
-
});
|
|
78
77
|
client.on('error', () => {
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
finish(false);
|
|
79
|
+
});
|
|
80
|
+
client.on('connect', () => {
|
|
81
|
+
finish(true);
|
|
81
82
|
});
|
|
83
|
+
try {
|
|
84
|
+
client.connect(target);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
finish(false);
|
|
88
|
+
}
|
|
82
89
|
});
|
|
83
90
|
}
|
|
84
91
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
@@ -193,6 +200,13 @@ const workingDirs = new Map(); // compositeKey -> workingDir
|
|
|
193
200
|
// Keyed by resolved file path (realpathSync), not composite key.
|
|
194
201
|
const fileWatchers = new Map();
|
|
195
202
|
const watchDebounce = new Map(); // keyed by base:filename, not photon
|
|
203
|
+
const activeFileWatcherReloads = new Set(); // resolved file paths currently being reloaded
|
|
204
|
+
const HOT_RELOAD_DEBOUNCE_MS = parseNonNegativeEnvInt('PHOTON_DAEMON_HOT_RELOAD_DEBOUNCE_MS', 1000);
|
|
205
|
+
const PROACTIVE_METADATA_DEBOUNCE_MS = parseNonNegativeEnvInt('PHOTON_PROACTIVE_METADATA_DEBOUNCE_MS', 1000);
|
|
206
|
+
function parseNonNegativeEnvInt(name, fallback) {
|
|
207
|
+
const parsed = Number(process.env[name]);
|
|
208
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
209
|
+
}
|
|
196
210
|
const photonSourceStats = new Map();
|
|
197
211
|
/** Snapshot the stat fields we use as change signal. Null on ENOENT etc. */
|
|
198
212
|
function statOrNull(filePath) {
|
|
@@ -212,6 +226,21 @@ function recordPhotonSourceStat(key, photonPath) {
|
|
|
212
226
|
if (s)
|
|
213
227
|
photonSourceStats.set(key, s);
|
|
214
228
|
}
|
|
229
|
+
function samePhotonSourceStat(a, b) {
|
|
230
|
+
return !!a && a.mtimeMs === b.mtimeMs && a.size === b.size && a.ino === b.ino;
|
|
231
|
+
}
|
|
232
|
+
function photonSourceAlreadyLoaded(photonName, photonPath) {
|
|
233
|
+
const current = statOrNull(photonPath);
|
|
234
|
+
if (!current)
|
|
235
|
+
return false;
|
|
236
|
+
for (const [key, storedPath] of photonPaths.entries()) {
|
|
237
|
+
if (!key.endsWith(`::${photonName}`) || storedPath !== photonPath)
|
|
238
|
+
continue;
|
|
239
|
+
if (samePhotonSourceStat(photonSourceStats.get(key), current))
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
215
244
|
/**
|
|
216
245
|
* Compare the current file stat against the last-recorded stat. If the
|
|
217
246
|
* file has changed since the cached photon was loaded, trigger a
|
|
@@ -232,9 +261,7 @@ async function statGate(key, photonName, photonPath, workingDir) {
|
|
|
232
261
|
const current = statOrNull(photonPath);
|
|
233
262
|
if (!current)
|
|
234
263
|
return;
|
|
235
|
-
if (current
|
|
236
|
-
current.size === cached.size &&
|
|
237
|
-
current.ino === cached.ino) {
|
|
264
|
+
if (samePhotonSourceStat(cached, current)) {
|
|
238
265
|
return;
|
|
239
266
|
}
|
|
240
267
|
logger.info('stat-gate: source changed since last load, syncing reload', {
|
|
@@ -266,6 +293,9 @@ const stateDirWatchers = new Map();
|
|
|
266
293
|
// @scheduled cron changes and @webhook additions reach the declared-set
|
|
267
294
|
// without a daemon restart. See watchBaseForProactiveMetadata().
|
|
268
295
|
const baseDirWatchers = new Map();
|
|
296
|
+
// photonDir -> SafeWatcher: startup watcher for new .photon.ts files in each
|
|
297
|
+
// active base. Tracked separately so Bun poll timers are closed on shutdown.
|
|
298
|
+
const startupPhotonDirWatchers = new Map();
|
|
269
299
|
import { compositeKey as _compositeKey, declaredKey as _declaredKey, webhookKey as _webhookKey, locationKey as _locationKey, findByPhoton, asScheduleKey, } from './registry-keys.js';
|
|
270
300
|
/**
|
|
271
301
|
* Create a composite key from photonName + workingDir for map lookups.
|
|
@@ -1572,7 +1602,7 @@ async function discoverProactiveMetadataAtBoot() {
|
|
|
1572
1602
|
* `@scheduled` cron expressions or `@webhook` additions only reached
|
|
1573
1603
|
* the daemon at restart.
|
|
1574
1604
|
*
|
|
1575
|
-
* Deduped by filename via the existing watchDebounce map
|
|
1605
|
+
* Deduped by filename via the existing watchDebounce map so
|
|
1576
1606
|
* editors that save-twice don't trigger two scans. Skips files that
|
|
1577
1607
|
* aren't `.photon.ts`.
|
|
1578
1608
|
*/
|
|
@@ -1640,7 +1670,7 @@ function watchBaseForProactiveMetadata(basePath, _isDefaultBase) {
|
|
|
1640
1670
|
});
|
|
1641
1671
|
}
|
|
1642
1672
|
})();
|
|
1643
|
-
},
|
|
1673
|
+
}, PROACTIVE_METADATA_DEBOUNCE_MS);
|
|
1644
1674
|
timer.unref();
|
|
1645
1675
|
watchDebounce.set(debounceKey, timer);
|
|
1646
1676
|
};
|
|
@@ -4320,7 +4350,7 @@ async function applyPatchToInstance(instance, photonName, ops, workingDir) {
|
|
|
4320
4350
|
if (keys.length === 0)
|
|
4321
4351
|
return;
|
|
4322
4352
|
// Get current state as plain JSON
|
|
4323
|
-
const snapshot = await snapshotState(instance, photonName);
|
|
4353
|
+
const snapshot = await snapshotState(instance, photonName, workingDir);
|
|
4324
4354
|
if (!snapshot)
|
|
4325
4355
|
return;
|
|
4326
4356
|
// Apply patch operations
|
|
@@ -4515,7 +4545,7 @@ function watchPhotonFile(photonName, photonPath) {
|
|
|
4515
4545
|
return;
|
|
4516
4546
|
try {
|
|
4517
4547
|
const watcher = safeWatchFile(watchPath, (eventType) => {
|
|
4518
|
-
//
|
|
4548
|
+
// Trailing debounce: wait for write bursts to settle before reloading.
|
|
4519
4549
|
const existing = watchDebounce.get(watchPath);
|
|
4520
4550
|
if (existing)
|
|
4521
4551
|
clearTimeout(existing);
|
|
@@ -4554,10 +4584,25 @@ function watchPhotonFile(photonName, photonPath) {
|
|
|
4554
4584
|
}
|
|
4555
4585
|
if (!fs.existsSync(photonPath))
|
|
4556
4586
|
return;
|
|
4587
|
+
if (activeFileWatcherReloads.has(watchPath)) {
|
|
4588
|
+
logger.debug('Coalescing file watcher event during active reload', {
|
|
4589
|
+
photonName,
|
|
4590
|
+
path: photonPath,
|
|
4591
|
+
});
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
if (photonSourceAlreadyLoaded(photonName, photonPath)) {
|
|
4595
|
+
logger.debug('Ignoring file watcher event for already-loaded source', {
|
|
4596
|
+
photonName,
|
|
4597
|
+
path: photonPath,
|
|
4598
|
+
});
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4557
4601
|
logger.info('File changed, auto-reloading', { photonName, path: photonPath });
|
|
4558
4602
|
// Invalidate cached state keys so they're re-extracted from fresh source
|
|
4559
4603
|
stateKeysCache.delete(photonName);
|
|
4560
4604
|
try {
|
|
4605
|
+
activeFileWatcherReloads.add(watchPath);
|
|
4561
4606
|
await reloadPhoton(photonName, photonPath);
|
|
4562
4607
|
}
|
|
4563
4608
|
catch (err) {
|
|
@@ -4571,8 +4616,11 @@ function watchPhotonFile(photonName, photonPath) {
|
|
|
4571
4616
|
error: getErrorMessage(err),
|
|
4572
4617
|
});
|
|
4573
4618
|
}
|
|
4619
|
+
finally {
|
|
4620
|
+
activeFileWatcherReloads.delete(watchPath);
|
|
4621
|
+
}
|
|
4574
4622
|
})();
|
|
4575
|
-
},
|
|
4623
|
+
}, HOT_RELOAD_DEBOUNCE_MS);
|
|
4576
4624
|
watchDebounce.set(watchPath, timer);
|
|
4577
4625
|
});
|
|
4578
4626
|
if (typeof watcher.on === 'function') {
|
|
@@ -5117,15 +5165,33 @@ function startupWatchPhotons() {
|
|
|
5117
5165
|
const defaultBase = getDefaultContext().baseDir;
|
|
5118
5166
|
const bases = new Set([defaultBase]);
|
|
5119
5167
|
for (const base of listActiveBases()) {
|
|
5168
|
+
if (!shouldStartupWatchBase(base.path, defaultBase))
|
|
5169
|
+
continue;
|
|
5120
5170
|
bases.add(base.path);
|
|
5121
5171
|
}
|
|
5122
5172
|
for (const photonDir of bases) {
|
|
5123
5173
|
startupWatchPhotonDir(photonDir, defaultBase);
|
|
5124
5174
|
}
|
|
5125
5175
|
}
|
|
5176
|
+
function shouldStartupWatchBase(basePath, defaultBase) {
|
|
5177
|
+
const resolved = path.resolve(basePath);
|
|
5178
|
+
if (resolved === path.resolve(defaultBase))
|
|
5179
|
+
return true;
|
|
5180
|
+
// Temporary directories are common in Beam and daemon regression tests. The
|
|
5181
|
+
// bases registry keeps them while the OS temp cleaner has not removed them,
|
|
5182
|
+
// but a production daemon should not spend long-lived watchers on them at
|
|
5183
|
+
// every startup. If a user actively invokes a temp photon later, the normal
|
|
5184
|
+
// request path still loads it and registers on-demand watchers.
|
|
5185
|
+
const tmpRoot = path.resolve(os.tmpdir());
|
|
5186
|
+
if (resolved === tmpRoot || resolved.startsWith(tmpRoot + path.sep))
|
|
5187
|
+
return false;
|
|
5188
|
+
return true;
|
|
5189
|
+
}
|
|
5126
5190
|
function startupWatchPhotonDir(photonDir, defaultBase) {
|
|
5127
5191
|
if (!fs.existsSync(photonDir))
|
|
5128
5192
|
return;
|
|
5193
|
+
if (startupPhotonDirWatchers.has(photonDir))
|
|
5194
|
+
return;
|
|
5129
5195
|
// Host mode: when the default base is host-disabled, skip the file
|
|
5130
5196
|
// watcher, the eager onInitialize loader, and the directory watcher
|
|
5131
5197
|
// entirely. They all activate background work that the marker is
|
|
@@ -5250,6 +5316,7 @@ function startupWatchPhotonDir(photonDir, defaultBase) {
|
|
|
5250
5316
|
if (typeof dirWatcher.on === 'function') {
|
|
5251
5317
|
dirWatcher.on('error', () => { }); // Non-fatal
|
|
5252
5318
|
}
|
|
5319
|
+
startupPhotonDirWatchers.set(photonDir, dirWatcher);
|
|
5253
5320
|
}
|
|
5254
5321
|
catch {
|
|
5255
5322
|
logger.warn('Failed to watch photon directory for new files', { dir: photonDir });
|
|
@@ -5297,7 +5364,13 @@ function startServer() {
|
|
|
5297
5364
|
cleanupSocketSubscriptions(socket);
|
|
5298
5365
|
});
|
|
5299
5366
|
socket.on('error', (error) => {
|
|
5300
|
-
|
|
5367
|
+
const code = error?.code;
|
|
5368
|
+
if (code === 'ECONNRESET' || code === 'EPIPE') {
|
|
5369
|
+
logger.debug('Socket closed by client', { error: getErrorMessage(error) });
|
|
5370
|
+
}
|
|
5371
|
+
else {
|
|
5372
|
+
logger.warn('Socket error', { error: getErrorMessage(error) });
|
|
5373
|
+
}
|
|
5301
5374
|
connectedSockets.delete(socket);
|
|
5302
5375
|
cleanupSocketSubscriptions(socket);
|
|
5303
5376
|
});
|
|
@@ -5632,6 +5705,22 @@ function shutdown() {
|
|
|
5632
5705
|
for (const photonPath of fileWatchers.keys()) {
|
|
5633
5706
|
unwatchPhotonFile(photonPath);
|
|
5634
5707
|
}
|
|
5708
|
+
for (const watcher of startupPhotonDirWatchers.values()) {
|
|
5709
|
+
watcher.close();
|
|
5710
|
+
}
|
|
5711
|
+
startupPhotonDirWatchers.clear();
|
|
5712
|
+
for (const watcher of baseDirWatchers.values()) {
|
|
5713
|
+
watcher.close();
|
|
5714
|
+
}
|
|
5715
|
+
baseDirWatchers.clear();
|
|
5716
|
+
for (const watcher of parentDirWatchers.values()) {
|
|
5717
|
+
watcher.close();
|
|
5718
|
+
}
|
|
5719
|
+
parentDirWatchers.clear();
|
|
5720
|
+
for (const watcher of stateDirWatchers.values()) {
|
|
5721
|
+
watcher.close();
|
|
5722
|
+
}
|
|
5723
|
+
stateDirWatchers.clear();
|
|
5635
5724
|
// Clean up poll-based watchers (bun fallback)
|
|
5636
5725
|
for (const timer of pollTimers) {
|
|
5637
5726
|
clearInterval(timer);
|