@amalgm/shell 0.1.33 → 0.1.35
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/PURPOSE.md +26 -1
- package/dist/files-register-host.d.ts +14 -0
- package/dist/files-register-host.js +49 -20
- package/dist/files-register-host.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/materialized-graph.d.ts +4 -0
- package/dist/materialized-graph.js +8 -0
- package/dist/materialized-graph.js.map +1 -1
- package/dist/user-ground-host.d.ts +17 -0
- package/dist/user-ground-host.js +250 -32
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/host.d.ts +2 -0
- package/dist/watching/host.js +1 -0
- package/dist/watching/host.js.map +1 -1
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -8,7 +8,7 @@ import Database from "better-sqlite3";
|
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir } from "./filesystem.js";
|
|
9
9
|
import { ContentCacheDownload, hashContentFile } from "./content-cache-host.js";
|
|
10
10
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
11
|
-
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
11
|
+
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
12
12
|
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
|
|
13
13
|
import { inspectGitRegistration, } from "./git-registration-host.js";
|
|
14
14
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
@@ -126,17 +126,24 @@ export class UserGroundHost {
|
|
|
126
126
|
throw new Error("this user has no cloud entity registry");
|
|
127
127
|
return cloud;
|
|
128
128
|
},
|
|
129
|
-
materializeWorkspace: async (input) =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
materializeWorkspace: async (input) => {
|
|
130
|
+
const installed = await installCloudWorkspace({
|
|
131
|
+
identity,
|
|
132
|
+
userRoot,
|
|
133
|
+
database: this.databasePath(identity),
|
|
134
|
+
cacheDir: this.cacheDir(identity),
|
|
135
|
+
bindingDir,
|
|
136
|
+
resourceId: privateEntityResourceId(identity.userId, sha256Hex),
|
|
137
|
+
workspace: input.workspace,
|
|
138
|
+
reference: input.reference,
|
|
139
|
+
references: input.references,
|
|
140
|
+
records: input.records,
|
|
141
|
+
destinationParent: input.destinationParent,
|
|
142
|
+
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
|
|
143
|
+
});
|
|
144
|
+
this.scheduleRescan();
|
|
145
|
+
return installed;
|
|
146
|
+
},
|
|
140
147
|
coverWorkspace: async ({ workspaceId, baseline }) => {
|
|
141
148
|
const health = this.ensureWatchers(identity, baseline === "authoritative-install" ? [workspaceId] : []);
|
|
142
149
|
return { active: true, roots: health.contentHandles };
|
|
@@ -282,7 +289,10 @@ export class UserGroundHost {
|
|
|
282
289
|
declaration: this.options.declaration ?? shippedUserHomeDeclaration,
|
|
283
290
|
now: this.options.now?.() ?? new Date(),
|
|
284
291
|
});
|
|
285
|
-
scanAndRegister({
|
|
292
|
+
await scanAndRegister({
|
|
293
|
+
identity, userRoot, database, cacheDir,
|
|
294
|
+
onScan: this.options.onDetectScan,
|
|
295
|
+
});
|
|
286
296
|
return localValue(identity, userRoot, database);
|
|
287
297
|
},
|
|
288
298
|
install: async (_who, cloud) => {
|
|
@@ -575,11 +585,12 @@ export class UserGroundHost {
|
|
|
575
585
|
if (!state)
|
|
576
586
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
577
587
|
const observations = this.watchHost.observations();
|
|
578
|
-
const localRecords = scanAndRegister({
|
|
588
|
+
const localRecords = await scanAndRegister({
|
|
579
589
|
identity,
|
|
580
590
|
userRoot: this.userRoot(identity),
|
|
581
591
|
database: this.databasePath(identity),
|
|
582
592
|
cacheDir: this.cacheDir(identity),
|
|
593
|
+
onScan: this.options.onDetectScan,
|
|
583
594
|
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion.paths])),
|
|
584
595
|
});
|
|
585
596
|
const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
|
|
@@ -744,6 +755,10 @@ function initializeDatabase(file) {
|
|
|
744
755
|
absolute_path TEXT NOT NULL,
|
|
745
756
|
device_number INTEGER NOT NULL,
|
|
746
757
|
inode INTEGER NOT NULL,
|
|
758
|
+
byte_size INTEGER,
|
|
759
|
+
modified_time_ms REAL,
|
|
760
|
+
changed_time_ms REAL,
|
|
761
|
+
filesystem_mode INTEGER,
|
|
747
762
|
UNIQUE(resource_id, root_uuid, relative_path)
|
|
748
763
|
);
|
|
749
764
|
CREATE TABLE IF NOT EXISTS cloud_outbox (
|
|
@@ -757,6 +772,18 @@ function initializeDatabase(file) {
|
|
|
757
772
|
created_at TEXT NOT NULL
|
|
758
773
|
);
|
|
759
774
|
`);
|
|
775
|
+
const currentEntityColumns = new Set(database.prepare("PRAGMA table_info(entities)").all()
|
|
776
|
+
.map(({ name }) => name));
|
|
777
|
+
const fingerprintColumns = [
|
|
778
|
+
["byte_size", "INTEGER"],
|
|
779
|
+
["modified_time_ms", "REAL"],
|
|
780
|
+
["changed_time_ms", "REAL"],
|
|
781
|
+
["filesystem_mode", "INTEGER"],
|
|
782
|
+
];
|
|
783
|
+
for (const [name, type] of fingerprintColumns) {
|
|
784
|
+
if (!currentEntityColumns.has(name))
|
|
785
|
+
database.exec(`ALTER TABLE entities ADD COLUMN ${name} ${type}`);
|
|
786
|
+
}
|
|
760
787
|
return database;
|
|
761
788
|
}
|
|
762
789
|
function readRows(file) {
|
|
@@ -769,7 +796,9 @@ function readRows(file) {
|
|
|
769
796
|
uuid, type, parent_uuid AS parentUUID, name, status, version,
|
|
770
797
|
payload_version AS payloadVersion, transport_version AS transportVersion,
|
|
771
798
|
relative_path AS relativePath, absolute_path AS absolutePath,
|
|
772
|
-
device_number AS deviceNumber, inode
|
|
799
|
+
device_number AS deviceNumber, inode,
|
|
800
|
+
byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
|
|
801
|
+
changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode
|
|
773
802
|
FROM entities ORDER BY uuid
|
|
774
803
|
`).all();
|
|
775
804
|
}
|
|
@@ -823,7 +852,8 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
823
852
|
INSERT INTO entities(
|
|
824
853
|
uuid, resource_id, root_uuid, type, parent_uuid, name, status, version, payload_version,
|
|
825
854
|
transport_version, relative_path, absolute_path, device_number, inode
|
|
826
|
-
|
|
855
|
+
, byte_size, modified_time_ms, changed_time_ms, filesystem_mode
|
|
856
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
827
857
|
ON CONFLICT(uuid) DO UPDATE SET
|
|
828
858
|
resource_id = excluded.resource_id,
|
|
829
859
|
root_uuid = excluded.root_uuid,
|
|
@@ -837,7 +867,11 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
837
867
|
relative_path = excluded.relative_path,
|
|
838
868
|
absolute_path = excluded.absolute_path,
|
|
839
869
|
device_number = excluded.device_number,
|
|
840
|
-
inode = excluded.inode
|
|
870
|
+
inode = excluded.inode,
|
|
871
|
+
byte_size = excluded.byte_size,
|
|
872
|
+
modified_time_ms = excluded.modified_time_ms,
|
|
873
|
+
changed_time_ms = excluded.changed_time_ms,
|
|
874
|
+
filesystem_mode = excluded.filesystem_mode
|
|
841
875
|
`);
|
|
842
876
|
const remove = database.prepare("DELETE FROM entities WHERE uuid = ? AND resource_id = ? AND root_uuid = ?");
|
|
843
877
|
const previousByUuid = new Map(previousRows.map((row) => [row.uuid, row]));
|
|
@@ -857,7 +891,11 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
857
891
|
|| old.relativePath !== row.relativePath
|
|
858
892
|
|| old.absolutePath !== row.absolutePath
|
|
859
893
|
|| old.deviceNumber !== row.deviceNumber
|
|
860
|
-
|| old.inode !== row.inode
|
|
894
|
+
|| old.inode !== row.inode
|
|
895
|
+
|| old.byteSize !== row.byteSize
|
|
896
|
+
|| old.modifiedTimeMs !== row.modifiedTimeMs
|
|
897
|
+
|| old.changedTimeMs !== row.changedTimeMs
|
|
898
|
+
|| old.filesystemMode !== row.filesystemMode;
|
|
861
899
|
});
|
|
862
900
|
const replace = database.transaction(() => {
|
|
863
901
|
replaceIdentity.run(identity.userId, identity.userEmail, identity.deviceId);
|
|
@@ -866,7 +904,7 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
866
904
|
remove.run(old.uuid, resourceId, rootUUID);
|
|
867
905
|
}
|
|
868
906
|
for (const row of changed)
|
|
869
|
-
upsert.run(row.record.uuid, resourceId, rootUUID, row.record.type, row.record.parentUUID, row.record.name, row.record.status, row.record.version, row.record.payloadVersion, row.record.transportVersion, row.relativePath, row.absolutePath, row.deviceNumber, row.inode);
|
|
907
|
+
upsert.run(row.record.uuid, resourceId, rootUUID, row.record.type, row.record.parentUUID, row.record.name, row.record.status, row.record.version, row.record.payloadVersion, row.record.transportVersion, row.relativePath, row.absolutePath, row.deviceNumber, row.inode, row.byteSize, row.modifiedTimeMs, row.changedTimeMs, row.filesystemMode);
|
|
870
908
|
});
|
|
871
909
|
replace();
|
|
872
910
|
}
|
|
@@ -943,6 +981,24 @@ function directoryExists(path) {
|
|
|
943
981
|
return false;
|
|
944
982
|
}
|
|
945
983
|
}
|
|
984
|
+
function statFingerprint(stats) {
|
|
985
|
+
return {
|
|
986
|
+
deviceNumber: stats.dev,
|
|
987
|
+
inode: stats.ino,
|
|
988
|
+
byteSize: stats.size,
|
|
989
|
+
modifiedTimeMs: stats.mtimeMs,
|
|
990
|
+
changedTimeMs: stats.ctimeMs,
|
|
991
|
+
filesystemMode: stats.mode,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function sameStatFingerprint(row, stats) {
|
|
995
|
+
return row.deviceNumber === stats.dev
|
|
996
|
+
&& row.inode === stats.ino
|
|
997
|
+
&& row.byteSize === stats.size
|
|
998
|
+
&& row.modifiedTimeMs === stats.mtimeMs
|
|
999
|
+
&& row.changedTimeMs === stats.ctimeMs
|
|
1000
|
+
&& row.filesystemMode === stats.mode;
|
|
1001
|
+
}
|
|
946
1002
|
/** The binding is the durable machine-local declaration of an external root.
|
|
947
1003
|
* Registration and Watch both derive their roots from this one inventory, so
|
|
948
1004
|
* correctness never depends on receiving the reference-rendering OS event. */
|
|
@@ -966,8 +1022,8 @@ function materializedWorkspaceBindings(bindingDir) {
|
|
|
966
1022
|
}
|
|
967
1023
|
return bindings.sort((left, right) => left.workspaceId.localeCompare(right.workspaceId));
|
|
968
1024
|
}
|
|
969
|
-
function scanAndRegister(input) {
|
|
970
|
-
const { identity, userRoot, database, cacheDir, suspicions } = input;
|
|
1025
|
+
async function scanAndRegister(input) {
|
|
1026
|
+
const { identity, userRoot, database, cacheDir, suspicions, onScan } = input;
|
|
971
1027
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
972
1028
|
const existing = readRows(database);
|
|
973
1029
|
const ignoreFile = join(userRoot, ".amalgmignore");
|
|
@@ -989,7 +1045,31 @@ function scanAndRegister(input) {
|
|
|
989
1045
|
const missingPortableReference = [...boundRoots.keys()]
|
|
990
1046
|
.some((workspaceId) => !existingReferenceIds.has(workspaceId));
|
|
991
1047
|
const deferredRootIds = new Set();
|
|
992
|
-
const
|
|
1048
|
+
const scan = async (parameters) => {
|
|
1049
|
+
const counters = {
|
|
1050
|
+
entries: 0,
|
|
1051
|
+
metadataReused: 0,
|
|
1052
|
+
contentReads: 0,
|
|
1053
|
+
gitInspections: 0,
|
|
1054
|
+
repositoryCaptures: 0,
|
|
1055
|
+
};
|
|
1056
|
+
const started = performance.now();
|
|
1057
|
+
const result = scanRoot({ ...parameters, evidence: counters });
|
|
1058
|
+
onScan?.({
|
|
1059
|
+
rootId: parameters.rootUUID,
|
|
1060
|
+
directory: parameters.rootPath,
|
|
1061
|
+
scope: parameters.suspects === null || parameters.suspects === undefined
|
|
1062
|
+
? "complete"
|
|
1063
|
+
: parameters.suspects.length === 0 ? "none" : "paths",
|
|
1064
|
+
...counters,
|
|
1065
|
+
durationMs: performance.now() - started,
|
|
1066
|
+
});
|
|
1067
|
+
// Each root commits its local projection before Detect yields. Native
|
|
1068
|
+
// callbacks can therefore preserve new suspicion between large roots.
|
|
1069
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1070
|
+
return result;
|
|
1071
|
+
};
|
|
1072
|
+
const core = await scan({
|
|
993
1073
|
identity,
|
|
994
1074
|
resourceId,
|
|
995
1075
|
rootUUID: coreUUID,
|
|
@@ -1023,7 +1103,7 @@ function scanAndRegister(input) {
|
|
|
1023
1103
|
}
|
|
1024
1104
|
const existingRoot = existing.find((row) => row.uuid === workspaceId && row.parentUUID === null);
|
|
1025
1105
|
const existingWorkspaceRows = existing.filter((row) => row.rootUUID === workspaceId);
|
|
1026
|
-
const scanned =
|
|
1106
|
+
const scanned = await scan({
|
|
1027
1107
|
identity,
|
|
1028
1108
|
resourceId,
|
|
1029
1109
|
rootUUID: workspaceId,
|
|
@@ -1045,7 +1125,7 @@ function scanAndRegister(input) {
|
|
|
1045
1125
|
.map(portableRecord);
|
|
1046
1126
|
}
|
|
1047
1127
|
function scanRoot(input) {
|
|
1048
|
-
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, } = input;
|
|
1128
|
+
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, } = input;
|
|
1049
1129
|
const existingRows = (input.existingRows ?? readRows(database))
|
|
1050
1130
|
.filter((row) => row.resourceId === resourceId && row.rootUUID === rootUUID);
|
|
1051
1131
|
const existingByPath = new Map(existingRows.map((row) => [row.relativePath, row]));
|
|
@@ -1063,6 +1143,10 @@ function scanRoot(input) {
|
|
|
1063
1143
|
absolutePath: row.absolutePath,
|
|
1064
1144
|
deviceNumber: row.deviceNumber,
|
|
1065
1145
|
inode: row.inode,
|
|
1146
|
+
byteSize: row.byteSize,
|
|
1147
|
+
modifiedTimeMs: row.modifiedTimeMs,
|
|
1148
|
+
changedTimeMs: row.changedTimeMs,
|
|
1149
|
+
filesystemMode: row.filesystemMode,
|
|
1066
1150
|
})),
|
|
1067
1151
|
referenceWorkspaceIds: existingRows
|
|
1068
1152
|
.filter((row) => row.type === "reference" && row.payloadVersion && UUID.test(row.payloadVersion))
|
|
@@ -1073,6 +1157,7 @@ function scanRoot(input) {
|
|
|
1073
1157
|
const entries = [];
|
|
1074
1158
|
const referenceWorkspaceIds = new Set();
|
|
1075
1159
|
const rootStats = lstatSync(rootPath);
|
|
1160
|
+
evidence && (evidence.entries += 1);
|
|
1076
1161
|
entries.push({
|
|
1077
1162
|
uuid: rootUUID,
|
|
1078
1163
|
type: rootType,
|
|
@@ -1082,8 +1167,7 @@ function scanRoot(input) {
|
|
|
1082
1167
|
absolutePath: rootPath,
|
|
1083
1168
|
payloadVersion: null,
|
|
1084
1169
|
transportVersion: null,
|
|
1085
|
-
|
|
1086
|
-
inode: rootStats.ino,
|
|
1170
|
+
...statFingerprint(rootStats),
|
|
1087
1171
|
});
|
|
1088
1172
|
const touchesSuspicion = (path) => suspects === null
|
|
1089
1173
|
|| pathIsSuspect(path, suspects)
|
|
@@ -1122,13 +1206,21 @@ function scanRoot(input) {
|
|
|
1122
1206
|
continue;
|
|
1123
1207
|
const absolutePath = join(directory, child.name);
|
|
1124
1208
|
const stats = lstatSync(absolutePath);
|
|
1209
|
+
evidence && (evidence.entries += 1);
|
|
1125
1210
|
const existing = existingByPath.get(relativePath);
|
|
1126
1211
|
const uuid = existing?.uuid || randomUUID();
|
|
1127
|
-
const
|
|
1212
|
+
const stableDuringCompleteCatchUp = suspects === null
|
|
1213
|
+
&& existing !== undefined
|
|
1214
|
+
&& sameStatFingerprint(existing, stats);
|
|
1215
|
+
const observe = existing === undefined
|
|
1216
|
+
|| (suspects === null ? !stableDuringCompleteCatchUp : touchesSuspicion(relativePath));
|
|
1217
|
+
if (!observe && suspects === null)
|
|
1218
|
+
evidence && (evidence.metadataReused += 1);
|
|
1128
1219
|
const repositoryPath = activeRepository
|
|
1129
1220
|
? slashPath(relative(activeRepository.root, absolutePath))
|
|
1130
1221
|
: null;
|
|
1131
1222
|
if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
|
|
1223
|
+
evidence && (evidence.gitInspections += 1);
|
|
1132
1224
|
try {
|
|
1133
1225
|
activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
|
|
1134
1226
|
}
|
|
@@ -1164,6 +1256,7 @@ function scanRoot(input) {
|
|
|
1164
1256
|
payloadVersion = indexed.payloadVersion;
|
|
1165
1257
|
}
|
|
1166
1258
|
else {
|
|
1259
|
+
evidence && (evidence.contentReads += 1);
|
|
1167
1260
|
const bytes = Buffer.from(readlinkSync(absolutePath), "utf8");
|
|
1168
1261
|
payloadVersion = sha256Hex(bytes);
|
|
1169
1262
|
if (!activeRepository)
|
|
@@ -1185,6 +1278,7 @@ function scanRoot(input) {
|
|
|
1185
1278
|
payloadVersion = indexed.payloadVersion;
|
|
1186
1279
|
}
|
|
1187
1280
|
else {
|
|
1281
|
+
evidence && (evidence.contentReads += 1);
|
|
1188
1282
|
const bytes = readFileSync(absolutePath);
|
|
1189
1283
|
type = fileType(bytes);
|
|
1190
1284
|
payloadVersion = sha256Hex(bytes);
|
|
@@ -1209,8 +1303,7 @@ function scanRoot(input) {
|
|
|
1209
1303
|
absolutePath,
|
|
1210
1304
|
payloadVersion,
|
|
1211
1305
|
transportVersion: null,
|
|
1212
|
-
|
|
1213
|
-
inode: stats.ino,
|
|
1306
|
+
...statFingerprint(stats),
|
|
1214
1307
|
});
|
|
1215
1308
|
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
1216
1309
|
visit(absolutePath, uuid, type, relativePath, type === "repo.git"
|
|
@@ -1258,6 +1351,10 @@ function scanRoot(input) {
|
|
|
1258
1351
|
absolutePath: entry.absolutePath,
|
|
1259
1352
|
deviceNumber: entry.deviceNumber,
|
|
1260
1353
|
inode: entry.inode,
|
|
1354
|
+
byteSize: entry.byteSize,
|
|
1355
|
+
modifiedTimeMs: entry.modifiedTimeMs,
|
|
1356
|
+
changedTimeMs: entry.changedTimeMs,
|
|
1357
|
+
filesystemMode: entry.filesystemMode,
|
|
1261
1358
|
};
|
|
1262
1359
|
});
|
|
1263
1360
|
const persistCurrentRows = () => {
|
|
@@ -1333,6 +1430,7 @@ function scanRoot(input) {
|
|
|
1333
1430
|
}))
|
|
1334
1431
|
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
1335
1432
|
: null;
|
|
1433
|
+
evidence && (evidence.repositoryCaptures += 1);
|
|
1336
1434
|
const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
|
|
1337
1435
|
repository.payloadVersion = captured.stateId;
|
|
1338
1436
|
repository.transportVersion = captured.transportVersion;
|
|
@@ -1611,14 +1709,111 @@ async function materializeRecords(input) {
|
|
|
1611
1709
|
record,
|
|
1612
1710
|
relativePath,
|
|
1613
1711
|
absolutePath,
|
|
1614
|
-
|
|
1615
|
-
inode: stats.ino,
|
|
1712
|
+
...statFingerprint(stats),
|
|
1616
1713
|
};
|
|
1617
1714
|
});
|
|
1618
1715
|
return { rows, repositories };
|
|
1619
1716
|
}
|
|
1717
|
+
/** Refresh the always-materialized portable reference inventory selected by
|
|
1718
|
+
* Live. Paths and bindings are local; UUIDs, parents, names, and versions are
|
|
1719
|
+
* the same cloud entities on every machine. */
|
|
1720
|
+
function installCloudWorkspaceReferences(input) {
|
|
1721
|
+
const { identity, userRoot, database, bindingDir, resourceId, workspace, references, } = input;
|
|
1722
|
+
const localRows = readRows(database);
|
|
1723
|
+
const rendered = [];
|
|
1724
|
+
try {
|
|
1725
|
+
for (const reference of references) {
|
|
1726
|
+
if (reference.type !== "reference"
|
|
1727
|
+
|| reference.status !== "active"
|
|
1728
|
+
|| !reference.payloadVersion
|
|
1729
|
+
|| reference.parentUUID === null) {
|
|
1730
|
+
throw new Error("cloud registry has an invalid durable reference");
|
|
1731
|
+
}
|
|
1732
|
+
const parent = localRows.find((row) => row.uuid === reference.parentUUID);
|
|
1733
|
+
if (!parent) {
|
|
1734
|
+
throw new Error(`cloud reference ${reference.uuid} parent is not materialized`);
|
|
1735
|
+
}
|
|
1736
|
+
const link = ensureWorkspaceReference({
|
|
1737
|
+
userRoot,
|
|
1738
|
+
bindingDir,
|
|
1739
|
+
workspaceId: reference.payloadVersion,
|
|
1740
|
+
workspaceName: reference.name,
|
|
1741
|
+
exactName: true,
|
|
1742
|
+
requireBinding: reference.payloadVersion === workspace.uuid,
|
|
1743
|
+
});
|
|
1744
|
+
const stats = lstatSync(link.referencePath);
|
|
1745
|
+
rendered.push({
|
|
1746
|
+
created: link.created,
|
|
1747
|
+
referencePath: link.referencePath,
|
|
1748
|
+
rootUUID: parent.rootUUID,
|
|
1749
|
+
row: {
|
|
1750
|
+
record: reference,
|
|
1751
|
+
relativePath: slashPath(relative(userRoot, link.referencePath)),
|
|
1752
|
+
absolutePath: link.referencePath,
|
|
1753
|
+
...statFingerprint(stats),
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
const rootIds = new Set(rendered.map(({ rootUUID }) => rootUUID));
|
|
1758
|
+
for (const rootUUID of rootIds) {
|
|
1759
|
+
const priorReferences = localRows.filter((row) => row.rootUUID === rootUUID && row.type === "reference");
|
|
1760
|
+
const currentReferences = rendered.filter((entry) => entry.rootUUID === rootUUID);
|
|
1761
|
+
const parentIds = new Set([
|
|
1762
|
+
...priorReferences.map((row) => row.parentUUID),
|
|
1763
|
+
...currentReferences.map(({ row }) => row.record.parentUUID),
|
|
1764
|
+
].filter((uuid) => uuid !== null));
|
|
1765
|
+
const priorParents = [...parentIds].map((parentId) => {
|
|
1766
|
+
const parent = localRows.find((row) => row.rootUUID === rootUUID && row.uuid === parentId);
|
|
1767
|
+
if (!parent || (parent.type !== "workspace" && parent.type !== "folder")) {
|
|
1768
|
+
throw new Error(`cloud reference parent ${parentId} is not a local container`);
|
|
1769
|
+
}
|
|
1770
|
+
return parent;
|
|
1771
|
+
});
|
|
1772
|
+
const parents = priorParents.map((parent) => {
|
|
1773
|
+
const parentId = parent.uuid;
|
|
1774
|
+
const stats = lstatSync(parent.absolutePath);
|
|
1775
|
+
const ordinaryChildren = localRows.filter((row) => row.rootUUID === rootUUID
|
|
1776
|
+
&& row.parentUUID === parentId
|
|
1777
|
+
&& row.type !== "reference"
|
|
1778
|
+
&& row.status === "active");
|
|
1779
|
+
const referenceChildren = currentReferences
|
|
1780
|
+
.filter(({ row }) => row.record.parentUUID === parentId)
|
|
1781
|
+
.map(({ row }) => row.record);
|
|
1782
|
+
const versionPayload = membershipHash([
|
|
1783
|
+
...ordinaryChildren,
|
|
1784
|
+
...referenceChildren,
|
|
1785
|
+
], sha256Hex);
|
|
1786
|
+
return {
|
|
1787
|
+
record: {
|
|
1788
|
+
...portableRecord(parent),
|
|
1789
|
+
version: canonicalVersion({
|
|
1790
|
+
type: parent.type,
|
|
1791
|
+
parentUUID: parent.parentUUID,
|
|
1792
|
+
name: parent.name,
|
|
1793
|
+
status: parent.status,
|
|
1794
|
+
payloadVersion: versionPayload,
|
|
1795
|
+
}, sha256Hex),
|
|
1796
|
+
},
|
|
1797
|
+
relativePath: parent.relativePath,
|
|
1798
|
+
absolutePath: parent.absolutePath,
|
|
1799
|
+
...statFingerprint(stats),
|
|
1800
|
+
};
|
|
1801
|
+
});
|
|
1802
|
+
persistRows(database, identity, resourceId, rootUUID, [...currentReferences.map(({ row }) => row), ...parents], [...priorReferences, ...priorParents]);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
catch (error) {
|
|
1806
|
+
for (const entry of rendered) {
|
|
1807
|
+
if (entry.created && pathExists(entry.referencePath)) {
|
|
1808
|
+
rmSync(entry.referencePath, { force: true });
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
throw error;
|
|
1812
|
+
}
|
|
1813
|
+
return references;
|
|
1814
|
+
}
|
|
1620
1815
|
async function installCloudWorkspace(input) {
|
|
1621
|
-
const { identity, database, cacheDir, bindingDir, resourceId, workspace, records, destinationParent, readContent, } = input;
|
|
1816
|
+
const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, readContent, } = input;
|
|
1622
1817
|
const existing = readRows(database).filter((row) => row.rootUUID === workspace.uuid);
|
|
1623
1818
|
if (existing.length > 0) {
|
|
1624
1819
|
const root = existing.find((row) => row.uuid === workspace.uuid);
|
|
@@ -1629,10 +1824,21 @@ async function installCloudWorkspace(input) {
|
|
|
1629
1824
|
if (!sameRecords(travelingRecords(localRecords), records)) {
|
|
1630
1825
|
throw new Error(`local workspace ${workspace.uuid} differs from its cloud graph`);
|
|
1631
1826
|
}
|
|
1827
|
+
const installedReferences = installCloudWorkspaceReferences({
|
|
1828
|
+
identity,
|
|
1829
|
+
userRoot,
|
|
1830
|
+
database,
|
|
1831
|
+
bindingDir,
|
|
1832
|
+
resourceId,
|
|
1833
|
+
workspace,
|
|
1834
|
+
references,
|
|
1835
|
+
});
|
|
1632
1836
|
return {
|
|
1633
1837
|
path: root.absolutePath,
|
|
1634
1838
|
rows: existing.length,
|
|
1635
1839
|
records: localRecords,
|
|
1840
|
+
reference,
|
|
1841
|
+
references: installedReferences,
|
|
1636
1842
|
alreadyMaterialized: true,
|
|
1637
1843
|
};
|
|
1638
1844
|
}
|
|
@@ -1656,14 +1862,26 @@ async function installCloudWorkspace(input) {
|
|
|
1656
1862
|
})), sha256Hex);
|
|
1657
1863
|
symlinkSync(destination, binding, "dir");
|
|
1658
1864
|
persistRows(database, identity, resourceId, workspace.uuid, rows);
|
|
1865
|
+
const installedReferences = installCloudWorkspaceReferences({
|
|
1866
|
+
identity,
|
|
1867
|
+
userRoot,
|
|
1868
|
+
database,
|
|
1869
|
+
bindingDir,
|
|
1870
|
+
resourceId,
|
|
1871
|
+
workspace,
|
|
1872
|
+
references,
|
|
1873
|
+
});
|
|
1659
1874
|
return {
|
|
1660
1875
|
path: destination,
|
|
1661
1876
|
rows: rows.length,
|
|
1662
1877
|
records: rows.map((row) => row.record),
|
|
1878
|
+
reference,
|
|
1879
|
+
references: installedReferences,
|
|
1663
1880
|
alreadyMaterialized: false,
|
|
1664
1881
|
};
|
|
1665
1882
|
}
|
|
1666
1883
|
catch (error) {
|
|
1884
|
+
deleteRootRows(database, resourceId, workspace.uuid);
|
|
1667
1885
|
if (pathExists(binding))
|
|
1668
1886
|
rmSync(binding, { force: true });
|
|
1669
1887
|
if (pathExists(destination))
|