@portel/photon 1.11.0 → 1.12.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/README.md +81 -72
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +5 -0
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts +1 -2
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +140 -191
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +44 -1
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +874 -20
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +83 -60
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +16 -2
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +1 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +2794 -322
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/package-app.d.ts.map +1 -1
- package/dist/cli/commands/package-app.js +116 -35
- package/dist/cli/commands/package-app.js.map +1 -1
- package/dist/context-store.d.ts +5 -0
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +9 -0
- package/dist/context-store.js.map +1 -1
- package/dist/daemon/server.js +303 -6
- package/dist/daemon/server.js.map +1 -1
- package/dist/loader.d.ts +4 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +186 -1
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +21 -1
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +6 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +22 -0
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/tunnel.photon.d.ts +5 -9
- package/dist/photons/tunnel.photon.d.ts.map +1 -1
- package/dist/photons/tunnel.photon.js +36 -96
- package/dist/photons/tunnel.photon.js.map +1 -1
- package/dist/photons/tunnel.photon.ts +40 -112
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +27 -2
- package/dist/server.js.map +1 -1
- package/dist/test-runner.d.ts +13 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +529 -122
- package/dist/test-runner.js.map +1 -1
- package/package.json +22 -6
package/dist/daemon/server.js
CHANGED
|
@@ -21,6 +21,9 @@ import { createLogger } from '../shared/logger.js';
|
|
|
21
21
|
import { getErrorMessage } from '../shared/error-handler.js';
|
|
22
22
|
import { timingSafeEqual, readBody, SimpleRateLimiter } from '../shared/security.js';
|
|
23
23
|
import { audit } from '../shared/audit.js';
|
|
24
|
+
import fastJsonPatch from 'fast-json-patch';
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
26
|
+
const jsonPatchCompare = fastJsonPatch.compare;
|
|
24
27
|
// Command line args: socketPath (global daemon only needs socket path)
|
|
25
28
|
const socketPath = process.argv[2];
|
|
26
29
|
const logger = createLogger({
|
|
@@ -592,6 +595,10 @@ async function getOrCreateSessionManager(photonName, photonPath, workingDir) {
|
|
|
592
595
|
manager = new SessionManager(pathToUse, photonName, idleTimeout, logger.child({ scope: photonName }), workingDir);
|
|
593
596
|
sessionManagers.set(key, manager);
|
|
594
597
|
photonPaths.set(key, pathToUse);
|
|
598
|
+
// Also store under bare photonName so snapshotState/persistInstanceState can find it
|
|
599
|
+
if (!photonPaths.has(photonName)) {
|
|
600
|
+
photonPaths.set(photonName, pathToUse);
|
|
601
|
+
}
|
|
595
602
|
if (workingDir) {
|
|
596
603
|
workingDirs.set(key, workingDir);
|
|
597
604
|
watchWorkingDir(workingDir);
|
|
@@ -1034,6 +1041,68 @@ async function handleRequest(request, socket) {
|
|
|
1034
1041
|
data: { instances, current, autoInstance, metadata },
|
|
1035
1042
|
};
|
|
1036
1043
|
}
|
|
1044
|
+
// ── Undo / Redo ──────────────────────────────────────────────
|
|
1045
|
+
if (request.method === '_undo' || request.method === '_redo') {
|
|
1046
|
+
const isUndo = request.method === '_undo';
|
|
1047
|
+
const instanceLabel = session.instanceName || 'default';
|
|
1048
|
+
const key = undoKey(photonName, instanceLabel);
|
|
1049
|
+
const source = isUndo ? undoPast.get(key) : undoFuture.get(key);
|
|
1050
|
+
const dest = isUndo ? undoFuture : undoPast;
|
|
1051
|
+
if (!source || source.length === 0) {
|
|
1052
|
+
return {
|
|
1053
|
+
type: 'result',
|
|
1054
|
+
id: request.id,
|
|
1055
|
+
success: true,
|
|
1056
|
+
data: { error: `Nothing to ${isUndo ? 'undo' : 'redo'}` },
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const entry = source.pop();
|
|
1060
|
+
const opsToApply = isUndo ? entry.inversePatch : entry.patch;
|
|
1061
|
+
// Apply patch to live instance
|
|
1062
|
+
await applyPatchToInstance(session.instance, photonName, opsToApply);
|
|
1063
|
+
// Persist the new state
|
|
1064
|
+
await persistInstanceState(session.instance, photonName, session.instanceName, request.workingDir);
|
|
1065
|
+
// Push to opposite stack
|
|
1066
|
+
let destStack = dest.get(key);
|
|
1067
|
+
if (!destStack) {
|
|
1068
|
+
destStack = [];
|
|
1069
|
+
dest.set(key, destStack);
|
|
1070
|
+
}
|
|
1071
|
+
destStack.push(entry);
|
|
1072
|
+
// Generate the resulting patch for this undo/redo action
|
|
1073
|
+
const actionPatch = isUndo ? entry.inversePatch : entry.patch;
|
|
1074
|
+
const actionInverse = isUndo ? entry.patch : entry.inversePatch;
|
|
1075
|
+
const stateChangedPayload = {
|
|
1076
|
+
event: 'state-changed',
|
|
1077
|
+
method: request.method,
|
|
1078
|
+
params: { undoneMethod: entry.method },
|
|
1079
|
+
instance: instanceLabel,
|
|
1080
|
+
data: { method: entry.method, action: isUndo ? 'undo' : 'redo' },
|
|
1081
|
+
...(actionPatch.length > 0 && { patch: actionPatch }),
|
|
1082
|
+
...(actionInverse.length > 0 && { inversePatch: actionInverse }),
|
|
1083
|
+
uri: `photon://${photonName}/${instanceLabel}`,
|
|
1084
|
+
};
|
|
1085
|
+
// Publish to instance-specific channel for multi-instance isolation
|
|
1086
|
+
publishToChannel(`${photonName}:${instanceLabel}:state-changed`, stateChangedPayload, socket);
|
|
1087
|
+
// Log undo/redo to event log
|
|
1088
|
+
await appendEventLog(photonName, instanceLabel, {
|
|
1089
|
+
method: request.method,
|
|
1090
|
+
params: { undoneMethod: entry.method },
|
|
1091
|
+
instance: instanceLabel,
|
|
1092
|
+
patch: actionPatch,
|
|
1093
|
+
inversePatch: actionInverse,
|
|
1094
|
+
}, request.workingDir);
|
|
1095
|
+
return {
|
|
1096
|
+
type: 'result',
|
|
1097
|
+
id: request.id,
|
|
1098
|
+
success: true,
|
|
1099
|
+
data: {
|
|
1100
|
+
action: isUndo ? 'undo' : 'redo',
|
|
1101
|
+
method: entry.method,
|
|
1102
|
+
message: `${isUndo ? 'Undid' : 'Redid'} "${entry.method}"`,
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1037
1106
|
// ─────────────────────────────────────────────────────────────
|
|
1038
1107
|
// ── Instance-scoped execution (one-shot, no session mutation) ──
|
|
1039
1108
|
// If targetInstance is set, load that instance, execute on it,
|
|
@@ -1053,6 +1122,8 @@ async function handleRequest(request, socket) {
|
|
|
1053
1122
|
publishToChannel(emit.channel, emit, socket);
|
|
1054
1123
|
}
|
|
1055
1124
|
};
|
|
1125
|
+
// Snapshot state before execution for JSON Patch diffing
|
|
1126
|
+
const preSnapshot = await snapshotState(targetInst, photonName);
|
|
1056
1127
|
const startTime = Date.now();
|
|
1057
1128
|
const result = await sessionManager.loader.executeTool(targetInst, request.method, request.args || {}, { outputHandler });
|
|
1058
1129
|
const durationMs = Date.now() - startTime;
|
|
@@ -1076,12 +1147,41 @@ async function handleRequest(request, socket) {
|
|
|
1076
1147
|
durationMs,
|
|
1077
1148
|
});
|
|
1078
1149
|
await persistInstanceState(targetInst, photonName, targetName, request.workingDir);
|
|
1079
|
-
|
|
1150
|
+
// Generate JSON Patch (RFC 6902) forward + inverse ops
|
|
1151
|
+
const postSnapshot = preSnapshot ? await snapshotState(targetInst, photonName) : null;
|
|
1152
|
+
const patch = preSnapshot && postSnapshot ? jsonPatchCompare(preSnapshot, postSnapshot) : [];
|
|
1153
|
+
const inversePatch = preSnapshot && postSnapshot ? jsonPatchCompare(postSnapshot, preSnapshot) : [];
|
|
1154
|
+
const instanceLabel = targetName || 'default';
|
|
1155
|
+
const stateChangedPayload = {
|
|
1080
1156
|
event: 'state-changed',
|
|
1081
1157
|
method: request.method,
|
|
1082
|
-
|
|
1158
|
+
params: request.args || {},
|
|
1159
|
+
instance: instanceLabel,
|
|
1083
1160
|
data: result,
|
|
1084
|
-
|
|
1161
|
+
...(patch.length > 0 && { patch }),
|
|
1162
|
+
...(inversePatch.length > 0 && { inversePatch }),
|
|
1163
|
+
uri: `photon://${photonName}/${instanceLabel}`,
|
|
1164
|
+
};
|
|
1165
|
+
// Publish to instance-specific channel for multi-instance isolation
|
|
1166
|
+
publishToChannel(`${photonName}:${instanceLabel}:state-changed`, stateChangedPayload, socket);
|
|
1167
|
+
// Append to event log
|
|
1168
|
+
if (patch.length > 0) {
|
|
1169
|
+
await appendEventLog(photonName, instanceLabel, {
|
|
1170
|
+
method: request.method,
|
|
1171
|
+
params: request.args || {},
|
|
1172
|
+
instance: instanceLabel,
|
|
1173
|
+
patch,
|
|
1174
|
+
inversePatch,
|
|
1175
|
+
}, request.workingDir);
|
|
1176
|
+
}
|
|
1177
|
+
// Track for undo/redo
|
|
1178
|
+
if (patch.length > 0) {
|
|
1179
|
+
pushUndoEntry(photonName, instanceLabel, {
|
|
1180
|
+
method: request.method,
|
|
1181
|
+
patch,
|
|
1182
|
+
inversePatch,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1085
1185
|
return { type: 'result', id: request.id, success: true, data: result, durationMs };
|
|
1086
1186
|
}
|
|
1087
1187
|
logger.info('Executing request', {
|
|
@@ -1097,6 +1197,8 @@ async function handleRequest(request, socket) {
|
|
|
1097
1197
|
logger.debug('Published to channel', { channel: emit.channel });
|
|
1098
1198
|
}
|
|
1099
1199
|
};
|
|
1200
|
+
// Snapshot state before execution for JSON Patch diffing
|
|
1201
|
+
const preSnapshot = await snapshotState(session.instance, photonName);
|
|
1100
1202
|
const startTime = Date.now();
|
|
1101
1203
|
const result = await sessionManager.loader.executeTool(session.instance, request.method, request.args || {}, { outputHandler });
|
|
1102
1204
|
const durationMs = Date.now() - startTime;
|
|
@@ -1121,12 +1223,42 @@ async function handleRequest(request, socket) {
|
|
|
1121
1223
|
});
|
|
1122
1224
|
// Persist reactive state after each tool call
|
|
1123
1225
|
await persistInstanceState(session.instance, photonName, session.instanceName, request.workingDir);
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1226
|
+
// Generate JSON Patch (RFC 6902) forward + inverse ops
|
|
1227
|
+
const postSnapshot = preSnapshot ? await snapshotState(session.instance, photonName) : null;
|
|
1228
|
+
const patch = preSnapshot && postSnapshot ? jsonPatchCompare(preSnapshot, postSnapshot) : [];
|
|
1229
|
+
const inversePatch = preSnapshot && postSnapshot ? jsonPatchCompare(postSnapshot, preSnapshot) : [];
|
|
1230
|
+
const instanceLabel = session.instanceName || 'default';
|
|
1231
|
+
const stateChangedPayload = {
|
|
1126
1232
|
event: 'state-changed',
|
|
1127
1233
|
method: request.method,
|
|
1234
|
+
params: request.args || {},
|
|
1235
|
+
instance: instanceLabel,
|
|
1128
1236
|
data: result,
|
|
1129
|
-
|
|
1237
|
+
...(patch.length > 0 && { patch }),
|
|
1238
|
+
...(inversePatch.length > 0 && { inversePatch }),
|
|
1239
|
+
uri: `photon://${photonName}/${instanceLabel}`,
|
|
1240
|
+
};
|
|
1241
|
+
// Notify subscribers that state may have changed (instance-specific channel)
|
|
1242
|
+
const targetLabel = session.instanceName || 'default';
|
|
1243
|
+
publishToChannel(`${photonName}:${targetLabel}:state-changed`, stateChangedPayload, socket);
|
|
1244
|
+
// Append to event log
|
|
1245
|
+
if (patch.length > 0) {
|
|
1246
|
+
await appendEventLog(photonName, instanceLabel, {
|
|
1247
|
+
method: request.method,
|
|
1248
|
+
params: request.args || {},
|
|
1249
|
+
instance: instanceLabel,
|
|
1250
|
+
patch,
|
|
1251
|
+
inversePatch,
|
|
1252
|
+
}, request.workingDir);
|
|
1253
|
+
}
|
|
1254
|
+
// Track for undo/redo
|
|
1255
|
+
if (patch.length > 0) {
|
|
1256
|
+
pushUndoEntry(photonName, instanceLabel, {
|
|
1257
|
+
method: request.method,
|
|
1258
|
+
patch,
|
|
1259
|
+
inversePatch,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1130
1262
|
return { type: 'result', id: request.id, success: true, data: result, durationMs };
|
|
1131
1263
|
}
|
|
1132
1264
|
catch (error) {
|
|
@@ -1193,6 +1325,34 @@ async function getStateKeys(photonName, photonPath) {
|
|
|
1193
1325
|
return [];
|
|
1194
1326
|
}
|
|
1195
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Capture a JSON-serializable snapshot of a photon instance's state.
|
|
1330
|
+
* Returns null for non-stateful photons (no state keys).
|
|
1331
|
+
* Used for JSON Patch diffing (pre/post execution).
|
|
1332
|
+
*/
|
|
1333
|
+
async function snapshotState(instance, photonName) {
|
|
1334
|
+
const photonPath = photonPaths.get(photonName);
|
|
1335
|
+
if (!photonPath)
|
|
1336
|
+
return null;
|
|
1337
|
+
const keys = await getStateKeys(photonName, photonPath);
|
|
1338
|
+
if (keys.length === 0)
|
|
1339
|
+
return null;
|
|
1340
|
+
const target = instance?.instance ?? instance;
|
|
1341
|
+
const snapshot = {};
|
|
1342
|
+
for (const key of keys) {
|
|
1343
|
+
const value = target[key];
|
|
1344
|
+
if (value === undefined)
|
|
1345
|
+
continue;
|
|
1346
|
+
if (value && typeof value === 'object' && value._propertyName) {
|
|
1347
|
+
snapshot[key] = value.toJSON ? value.toJSON() : globalThis.Array.from(value);
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
snapshot[key] = value;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
// Deep-clone to freeze the snapshot (avoid reference sharing with live state)
|
|
1354
|
+
return JSON.parse(JSON.stringify(snapshot));
|
|
1355
|
+
}
|
|
1196
1356
|
/**
|
|
1197
1357
|
* Persist reactive collection state to disk after each tool call.
|
|
1198
1358
|
* Only persists properties identified as 'state' injection type
|
|
@@ -1240,6 +1400,143 @@ async function persistInstanceState(instance, photonName, instanceName, workingD
|
|
|
1240
1400
|
}
|
|
1241
1401
|
}
|
|
1242
1402
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
1403
|
+
// EVENT LOG (JSONL append-only per instance)
|
|
1404
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
1405
|
+
/** Monotonically increasing sequence number per photon:instance */
|
|
1406
|
+
const eventLogSeq = new Map();
|
|
1407
|
+
const EVENT_LOG_MAX_SIZE = parseInt(process.env.PHOTON_EVENT_LOG_MAX_SIZE || '', 10) || 10 * 1024 * 1024; // 10MB default
|
|
1408
|
+
/**
|
|
1409
|
+
* Get the event log path for a photon instance.
|
|
1410
|
+
* Co-located with state file: ~/.photon/state/{photon}/{instance}.log
|
|
1411
|
+
*/
|
|
1412
|
+
function getInstanceLogPath(photonName, instanceName, baseDir) {
|
|
1413
|
+
const name = instanceName || 'default';
|
|
1414
|
+
const dir = baseDir || getDefaultContext().baseDir;
|
|
1415
|
+
return path.join(dir, 'state', photonName, `${name}.log`);
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Append an event entry to the JSONL event log.
|
|
1419
|
+
* Handles log rotation when file exceeds EVENT_LOG_MAX_SIZE.
|
|
1420
|
+
*/
|
|
1421
|
+
async function appendEventLog(photonName, instanceName, entry, workingDir) {
|
|
1422
|
+
try {
|
|
1423
|
+
const logPath = getInstanceLogPath(photonName, instanceName, workingDir);
|
|
1424
|
+
const fsPromises = await import('fs/promises');
|
|
1425
|
+
await fsPromises.mkdir(path.dirname(logPath), { recursive: true });
|
|
1426
|
+
// Get next sequence number
|
|
1427
|
+
const seqKey = `${photonName}:${instanceName}`;
|
|
1428
|
+
const seq = (eventLogSeq.get(seqKey) || 0) + 1;
|
|
1429
|
+
eventLogSeq.set(seqKey, seq);
|
|
1430
|
+
const logEntry = {
|
|
1431
|
+
seq,
|
|
1432
|
+
timestamp: new Date().toISOString(),
|
|
1433
|
+
method: entry.method,
|
|
1434
|
+
params: entry.params,
|
|
1435
|
+
instance: entry.instance,
|
|
1436
|
+
patch: entry.patch,
|
|
1437
|
+
inversePatch: entry.inversePatch,
|
|
1438
|
+
};
|
|
1439
|
+
const line = JSON.stringify(logEntry) + '\n';
|
|
1440
|
+
// Check rotation before appending
|
|
1441
|
+
try {
|
|
1442
|
+
const stat = await fsPromises.stat(logPath);
|
|
1443
|
+
if (stat.size >= EVENT_LOG_MAX_SIZE) {
|
|
1444
|
+
const rotatedPath = logPath + '.1';
|
|
1445
|
+
await fsPromises.rename(logPath, rotatedPath).catch(() => { });
|
|
1446
|
+
logger.debug('Rotated event log', { photon: photonName, instance: instanceName });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
catch {
|
|
1450
|
+
// File doesn't exist yet — that's fine
|
|
1451
|
+
}
|
|
1452
|
+
await fsPromises.appendFile(logPath, line);
|
|
1453
|
+
logger.debug('Appended event log', { photon: photonName, instance: instanceName, seq });
|
|
1454
|
+
}
|
|
1455
|
+
catch (error) {
|
|
1456
|
+
logger.error('Failed to append event log', {
|
|
1457
|
+
photon: photonName,
|
|
1458
|
+
error: getErrorMessage(error),
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/** past stack per photon:instance */
|
|
1463
|
+
const undoPast = new Map();
|
|
1464
|
+
/** future stack per photon:instance */
|
|
1465
|
+
const undoFuture = new Map();
|
|
1466
|
+
const UNDO_STACK_LIMIT = 100;
|
|
1467
|
+
function undoKey(photonName, instance) {
|
|
1468
|
+
return `${photonName}:${instance || 'default'}`;
|
|
1469
|
+
}
|
|
1470
|
+
function pushUndoEntry(photonName, instance, entry) {
|
|
1471
|
+
const key = undoKey(photonName, instance);
|
|
1472
|
+
let past = undoPast.get(key);
|
|
1473
|
+
if (!past) {
|
|
1474
|
+
past = [];
|
|
1475
|
+
undoPast.set(key, past);
|
|
1476
|
+
}
|
|
1477
|
+
past.push(entry);
|
|
1478
|
+
if (past.length > UNDO_STACK_LIMIT)
|
|
1479
|
+
past.shift();
|
|
1480
|
+
// New mutation clears the redo future
|
|
1481
|
+
undoFuture.set(key, []);
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Apply a JSON Patch to a live photon instance's state.
|
|
1485
|
+
* Snapshots state as plain JSON, applies patch, then rehydrates instance properties.
|
|
1486
|
+
*/
|
|
1487
|
+
async function applyPatchToInstance(instance, photonName, ops) {
|
|
1488
|
+
// Use the already-imported fastJsonPatch module
|
|
1489
|
+
const applyPatch = fastJsonPatch.applyPatch.bind(fastJsonPatch);
|
|
1490
|
+
const photonPath = photonPaths.get(photonName);
|
|
1491
|
+
if (!photonPath)
|
|
1492
|
+
return;
|
|
1493
|
+
const keys = await getStateKeys(photonName, photonPath);
|
|
1494
|
+
if (keys.length === 0)
|
|
1495
|
+
return;
|
|
1496
|
+
// Get current state as plain JSON
|
|
1497
|
+
const snapshot = await snapshotState(instance, photonName);
|
|
1498
|
+
if (!snapshot)
|
|
1499
|
+
return;
|
|
1500
|
+
// Apply patch operations
|
|
1501
|
+
applyPatch(snapshot, ops, true, true);
|
|
1502
|
+
// Rehydrate instance properties from patched snapshot
|
|
1503
|
+
const target = instance?.instance ?? instance;
|
|
1504
|
+
for (const key of keys) {
|
|
1505
|
+
if (!(key in snapshot))
|
|
1506
|
+
continue;
|
|
1507
|
+
const current = target[key];
|
|
1508
|
+
const patched = snapshot[key];
|
|
1509
|
+
if (current && typeof current === 'object' && current._propertyName) {
|
|
1510
|
+
// ReactiveArray — clear and replace contents
|
|
1511
|
+
if (typeof current.splice === 'function') {
|
|
1512
|
+
current.splice(0, current.length, ...patched);
|
|
1513
|
+
}
|
|
1514
|
+
else if (current instanceof Map || (current.clear && current.set)) {
|
|
1515
|
+
current.clear();
|
|
1516
|
+
for (const [k, v] of Object.entries(patched))
|
|
1517
|
+
current.set(k, v);
|
|
1518
|
+
}
|
|
1519
|
+
else if (current instanceof Set || (current.clear && current.add)) {
|
|
1520
|
+
current.clear();
|
|
1521
|
+
for (const v of patched)
|
|
1522
|
+
current.add(v);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
else if (globalThis.Array.isArray(current)) {
|
|
1526
|
+
current.splice(0, current.length, ...patched);
|
|
1527
|
+
}
|
|
1528
|
+
else if (typeof current === 'object' && current !== null) {
|
|
1529
|
+
// Plain object — replace properties
|
|
1530
|
+
for (const k of Object.keys(current))
|
|
1531
|
+
delete current[k];
|
|
1532
|
+
Object.assign(current, patched);
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
target[key] = patched;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
1243
1540
|
// FILE WATCHING (Auto Hot-Reload)
|
|
1244
1541
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
1245
1542
|
function watchPhotonFile(photonName, photonPath) {
|