@harperfast/harper 5.0.1 → 5.0.3
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/bin/cliOperations.js +6 -4
- package/bin/copyDb.ts +208 -0
- package/bin/restart.js +8 -7
- package/bin/run.js +2 -1
- package/components/Application.ts +24 -9
- package/components/ApplicationScope.ts +2 -3
- package/components/componentLoader.ts +13 -2
- package/config/harperConfigEnvVars.ts +34 -0
- package/dist/bin/cliOperations.js +6 -4
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +1 -0
- package/dist/bin/copyDb.js +197 -0
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/restart.js +8 -7
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +3 -1
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.js +15 -5
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +2 -2
- package/dist/components/ApplicationScope.js +2 -3
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/componentLoader.js +11 -2
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +33 -0
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/resources/DatabaseTransaction.js +17 -2
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +2 -2
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +1 -1
- package/dist/resources/RocksIndexStore.d.ts +14 -7
- package/dist/resources/RocksIndexStore.js +19 -12
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/Table.js +55 -29
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +7 -10
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/databases.js +18 -14
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +2 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +38 -19
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/security/certificateVerification/ocspVerification.js +1 -1
- package/dist/security/certificateVerification/ocspVerification.js.map +1 -1
- package/dist/security/jsLoader.js +54 -21
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +7 -7
- package/dist/security/keys.js.map +1 -1
- package/dist/security/user.js +9 -8
- package/dist/security/user.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +0 -4
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +1 -0
- package/dist/utility/hdbTerms.js +1 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +11 -8
- package/dist/utility/install/installer.js.map +1 -1
- package/package.json +3 -3
- package/resources/DatabaseTransaction.ts +19 -2
- package/resources/RecordEncoder.ts +2 -2
- package/resources/ResourceInterface.ts +1 -1
- package/resources/RocksIndexStore.ts +20 -15
- package/resources/Table.ts +50 -25
- package/resources/analytics/write.ts +7 -10
- package/resources/databases.ts +29 -14
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +67 -30
- package/security/certificateVerification/ocspVerification.ts +1 -1
- package/security/jsLoader.ts +68 -22
- package/security/keys.js +7 -7
- package/security/user.ts +10 -8
- package/server/itc/serverHandlers.js +0 -4
- package/static/defaultConfig.yaml +1 -1
- package/studio/web/assets/{index-C0iJWrnF.js → index-CxTavHFE.js} +5 -5
- package/studio/web/assets/{index-C0iJWrnF.js.map → index-CxTavHFE.js.map} +1 -1
- package/studio/web/assets/{index.lazy-C647wC7n.js → index.lazy-CfiR1tvq.js} +2 -2
- package/studio/web/assets/{index.lazy-C647wC7n.js.map → index.lazy-CfiR1tvq.js.map} +1 -1
- package/studio/web/assets/{profile-BTS_ZjxV.js → profile-C-uokAal.js} +2 -2
- package/studio/web/assets/{profile-BTS_ZjxV.js.map → profile-C-uokAal.js.map} +1 -1
- package/studio/web/assets/{status-Dc-S5M23.js → status-D6xeT4ss.js} +2 -2
- package/studio/web/assets/{status-Dc-S5M23.js.map → status-D6xeT4ss.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/hdbTerms.ts +1 -0
- package/utility/install/installer.js +14 -10
package/resources/Table.ts
CHANGED
|
@@ -1424,23 +1424,35 @@ export function makeTable(options) {
|
|
|
1424
1424
|
*/
|
|
1425
1425
|
static evict(id, existingRecord, existingVersion) {
|
|
1426
1426
|
let entry;
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1427
|
+
let transaction = txnForContext({ transaction: new DatabaseTransaction() }).getReadTxn();
|
|
1428
|
+
let options = { transaction };
|
|
1429
|
+
try {
|
|
1430
|
+
if (hasSourceGet || audit) {
|
|
1431
|
+
if (!existingRecord) return;
|
|
1432
|
+
entry = primaryStore.getEntry(id, options);
|
|
1433
|
+
if (!entry || !existingRecord) return;
|
|
1434
|
+
if (entry.version !== existingVersion) return;
|
|
1435
|
+
}
|
|
1436
|
+
if (hasSourceGet) {
|
|
1437
|
+
// if there is a resolution in-progress, abandon the eviction
|
|
1438
|
+
if (primaryStore.hasLock(id, entry.version)) return;
|
|
1439
|
+
}
|
|
1440
|
+
// evictions never go in the audit log, so we can not record a deletion entry for the eviction
|
|
1441
|
+
// as there is no corresponding audit entry and it would never get cleaned up. So we must simply
|
|
1442
|
+
// removed the entry entirely, but first cleanup indices
|
|
1443
|
+
if (primaryStore.ifVersion) {
|
|
1444
|
+
// lmdb
|
|
1445
|
+
primaryStore.ifVersion?.(id, existingVersion, () => {
|
|
1446
|
+
updateIndices(id, existingRecord, null);
|
|
1447
|
+
});
|
|
1448
|
+
return removeEntry(primaryStore, entry ?? primaryStore.getEntry(id), existingVersion);
|
|
1449
|
+
} else {
|
|
1450
|
+
updateIndices(id, existingRecord, null, options);
|
|
1451
|
+
return removeEntry(primaryStore, entry ?? primaryStore.getEntry(id), options);
|
|
1452
|
+
}
|
|
1453
|
+
} finally {
|
|
1454
|
+
return transaction.commit();
|
|
1436
1455
|
}
|
|
1437
|
-
primaryStore.ifVersion?.(id, existingVersion, () => {
|
|
1438
|
-
updateIndices(id, existingRecord, null);
|
|
1439
|
-
});
|
|
1440
|
-
// evictions never go in the audit log, so we can not record a deletion entry for the eviction
|
|
1441
|
-
// as there is no corresponding audit entry and it would never get cleaned up. So we must simply
|
|
1442
|
-
// removed the entry entirely
|
|
1443
|
-
return removeEntry(primaryStore, entry ?? primaryStore.getEntry(id), existingVersion);
|
|
1444
1456
|
}
|
|
1445
1457
|
/**
|
|
1446
1458
|
* This is intended to acquire a lock on a record from the whole cluster.
|
|
@@ -1951,9 +1963,10 @@ export function makeTable(options) {
|
|
|
1951
1963
|
context.lastModified = existingEntry.version;
|
|
1952
1964
|
TableResource._updateResource(this, existingEntry);
|
|
1953
1965
|
}
|
|
1954
|
-
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId)
|
|
1955
|
-
|
|
1956
|
-
|
|
1966
|
+
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId) < 0) {
|
|
1967
|
+
return;
|
|
1968
|
+
} // a newer record exists locally
|
|
1969
|
+
updateIndices(id, existingRecord, null, transaction && { transaction });
|
|
1957
1970
|
if (audit || trackDeletes) {
|
|
1958
1971
|
updateRecord(
|
|
1959
1972
|
id,
|
|
@@ -2262,7 +2275,9 @@ export function makeTable(options) {
|
|
|
2262
2275
|
return (entryA, entryB) => {
|
|
2263
2276
|
const a = getAttributeValue(entryA, order.attribute, context);
|
|
2264
2277
|
const b = getAttributeValue(entryB, order.attribute, context);
|
|
2265
|
-
const diff = descending
|
|
2278
|
+
const diff = descending
|
|
2279
|
+
? compareKeys(convertToComparableKeys(b), convertToComparableKeys(a))
|
|
2280
|
+
: compareKeys(convertToComparableKeys(a), convertToComparableKeys(b));
|
|
2266
2281
|
if (diff === 0) return nextComparator?.(entryA, entryB) || 0;
|
|
2267
2282
|
return diff;
|
|
2268
2283
|
};
|
|
@@ -3509,6 +3524,7 @@ export function makeTable(options) {
|
|
|
3509
3524
|
// determine what index values need to be removed and added
|
|
3510
3525
|
let valuesToAdd = getIndexedValues(value, indexNulls) as any[];
|
|
3511
3526
|
let valuesToRemove = getIndexedValues(existingValue, indexNulls) as any[];
|
|
3527
|
+
let isLMDB = !!index.prefetch;
|
|
3512
3528
|
if (valuesToRemove?.length > 0) {
|
|
3513
3529
|
// put this in a conditional so we can do a faster version for new records
|
|
3514
3530
|
// determine the changes/diff from new values and old values
|
|
@@ -3525,18 +3541,18 @@ export function makeTable(options) {
|
|
|
3525
3541
|
})
|
|
3526
3542
|
: [];
|
|
3527
3543
|
valuesToRemove = Array.from(setToRemove);
|
|
3528
|
-
if ((valuesToRemove.length > 0 || valuesToAdd.length > 0) && LMDB_PREFETCH_WRITES) {
|
|
3544
|
+
if (isLMDB && (valuesToRemove.length > 0 || valuesToAdd.length > 0) && LMDB_PREFETCH_WRITES) {
|
|
3529
3545
|
// prefetch any values that have been removed or added
|
|
3530
3546
|
const valuesToPrefetch = valuesToRemove.concat(valuesToAdd).map((v) => ({ key: v, value: id }));
|
|
3531
|
-
index.prefetch
|
|
3547
|
+
index.prefetch(valuesToPrefetch, noop);
|
|
3532
3548
|
}
|
|
3533
3549
|
//if the update cleared out the attribute value we need to delete it from the index
|
|
3534
3550
|
for (let i = 0, l = valuesToRemove.length; i < l; i++) {
|
|
3535
3551
|
index.remove(valuesToRemove[i], id, options);
|
|
3536
3552
|
}
|
|
3537
|
-
} else if (valuesToAdd?.length > 0 && LMDB_PREFETCH_WRITES) {
|
|
3553
|
+
} else if (isLMDB && valuesToAdd?.length > 0 && LMDB_PREFETCH_WRITES) {
|
|
3538
3554
|
// no old values, just new
|
|
3539
|
-
index.prefetch
|
|
3555
|
+
index.prefetch(
|
|
3540
3556
|
valuesToAdd.map((v) => ({ key: v, value: id })),
|
|
3541
3557
|
noop
|
|
3542
3558
|
);
|
|
@@ -4122,7 +4138,7 @@ export function makeTable(options) {
|
|
|
4122
4138
|
// don't do anything if the version has changed
|
|
4123
4139
|
return;
|
|
4124
4140
|
}
|
|
4125
|
-
updateIndices(id, existingRecord, updatedRecord);
|
|
4141
|
+
updateIndices(id, existingRecord, updatedRecord, transaction && { transaction });
|
|
4126
4142
|
if (updatedRecord) {
|
|
4127
4143
|
if (existingEntry) {
|
|
4128
4144
|
context.previousResidency = TableResource.getResidencyRecord(existingEntry.residencyId);
|
|
@@ -4572,3 +4588,12 @@ function hasOtherProcesses(store) {
|
|
|
4572
4588
|
return +line.match(/\d+/)?.[0] != pid;
|
|
4573
4589
|
});
|
|
4574
4590
|
}
|
|
4591
|
+
function convertToComparableKeys(a) {
|
|
4592
|
+
if (a instanceof Date) {
|
|
4593
|
+
return a.getTime();
|
|
4594
|
+
}
|
|
4595
|
+
if (Array.isArray(a)) {
|
|
4596
|
+
return a.map(convertToComparableKeys);
|
|
4597
|
+
}
|
|
4598
|
+
return a;
|
|
4599
|
+
}
|
|
@@ -219,15 +219,12 @@ export async function recordHostname() {
|
|
|
219
219
|
const nodeId = stableNodeId(hostname);
|
|
220
220
|
log.trace?.('recordHostname nodeId:', nodeId);
|
|
221
221
|
const hostnamesTable = getAnalyticsHostnameTable();
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
log.trace?.(`recordHostname storing hostname: ${JSON.stringify(hostnameRecord)}`);
|
|
229
|
-
await hostnamesTable.put(hostnameRecord.id, hostnameRecord);
|
|
230
|
-
}
|
|
222
|
+
const hostnameRecord = {
|
|
223
|
+
id: nodeId,
|
|
224
|
+
hostname,
|
|
225
|
+
};
|
|
226
|
+
log.trace?.(`recordHostname storing hostname: ${JSON.stringify(hostnameRecord)}`);
|
|
227
|
+
await hostnamesTable.put(hostnameRecord.id, hostnameRecord);
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
export interface Metric {
|
|
@@ -514,7 +511,7 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
514
511
|
await rest();
|
|
515
512
|
}
|
|
516
513
|
for (const entry of threadsToAverage) {
|
|
517
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
514
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
518
515
|
let { path, method, type, metric, count, total, distribution, threads, ...measures } = entry;
|
|
519
516
|
threads = threads.filter((thread) => thread);
|
|
520
517
|
for (const measureName in measures) {
|
package/resources/databases.ts
CHANGED
|
@@ -120,9 +120,13 @@ function openRocksDatabase(path: string, options: RocksDatabaseOptions & { dupSo
|
|
|
120
120
|
}
|
|
121
121
|
let db: RocksRootDatabase;
|
|
122
122
|
if (options.dupSort) {
|
|
123
|
-
db =
|
|
123
|
+
db = new RocksIndexStore(path, options).open() as RocksDatabaseEx;
|
|
124
124
|
} else {
|
|
125
125
|
db = RocksDatabase.open(path, options) as RocksDatabaseEx;
|
|
126
|
+
// the RocksDB put and remove return promises, which masks thrown errors in non-awaiting calls to put/remove,
|
|
127
|
+
// making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem
|
|
128
|
+
db.put = db.putSync;
|
|
129
|
+
db.remove = db.removeSync;
|
|
126
130
|
db.encoder.name = options.name;
|
|
127
131
|
}
|
|
128
132
|
db.env = {};
|
|
@@ -386,18 +390,18 @@ function initStores(
|
|
|
386
390
|
) {
|
|
387
391
|
const envInit = new OpenEnvironmentObject(path, false);
|
|
388
392
|
const internalDbiInit = createOpenDBIObject(false);
|
|
389
|
-
let
|
|
390
|
-
if (!
|
|
393
|
+
let attributesDbi = rootStore.dbisDb;
|
|
394
|
+
if (!attributesDbi) {
|
|
391
395
|
if (rootStore instanceof RocksDatabase) {
|
|
392
|
-
|
|
396
|
+
attributesDbi = openRocksDatabase(rootStore.path, {
|
|
393
397
|
...internalDbiInit,
|
|
394
398
|
disableWAL: false,
|
|
395
399
|
name: INTERNAL_DBIS_NAME,
|
|
396
400
|
}) as RocksDatabaseEx;
|
|
397
401
|
} else {
|
|
398
|
-
|
|
402
|
+
attributesDbi = rootStore.openDB(INTERNAL_DBIS_NAME, internalDbiInit);
|
|
399
403
|
}
|
|
400
|
-
rootStore.dbisDb =
|
|
404
|
+
rootStore.dbisDb = attributesDbi;
|
|
401
405
|
}
|
|
402
406
|
|
|
403
407
|
let auditStore = rootStore.auditStore;
|
|
@@ -428,7 +432,7 @@ function initStores(
|
|
|
428
432
|
definedTables.rootStore = rootStore;
|
|
429
433
|
const tablesToLoad = new Map<string, any>();
|
|
430
434
|
|
|
431
|
-
for (const result of
|
|
435
|
+
for (const result of attributesDbi.getRange({ start: false })) {
|
|
432
436
|
const { key, value } = result as { key: string; value: any };
|
|
433
437
|
let [tableName, attribute_name] = key.toString().split('/');
|
|
434
438
|
if (attribute_name === '') {
|
|
@@ -489,16 +493,16 @@ function initStores(
|
|
|
489
493
|
} else {
|
|
490
494
|
tableId = primaryAttribute.tableId;
|
|
491
495
|
if (tableId) {
|
|
492
|
-
if (tableId >= (
|
|
493
|
-
|
|
496
|
+
if (tableId >= (attributesDbi.getSync(NEXT_TABLE_ID) || 0)) {
|
|
497
|
+
attributesDbi.putSync(NEXT_TABLE_ID, tableId + 1);
|
|
494
498
|
logger.info(`Updating next table id (it was out of sync) to ${tableId + 1} for ${tableName}`);
|
|
495
499
|
}
|
|
496
500
|
} else {
|
|
497
|
-
primaryAttribute.tableId = tableId =
|
|
501
|
+
primaryAttribute.tableId = tableId = attributesDbi.getSync(NEXT_TABLE_ID);
|
|
498
502
|
if (!tableId) tableId = 1;
|
|
499
503
|
logger.debug(`Table {tableName} missing an id, assigning {tableId}`);
|
|
500
|
-
|
|
501
|
-
|
|
504
|
+
attributesDbi.putSync(NEXT_TABLE_ID, tableId + 1);
|
|
505
|
+
attributesDbi.putSync(primaryAttribute.key, primaryAttribute);
|
|
502
506
|
}
|
|
503
507
|
const dbiInit = createOpenDBIObject(!primaryAttribute.isPrimaryKey, primaryAttribute.isPrimaryKey);
|
|
504
508
|
dbiInit.compression = primaryAttribute.compression;
|
|
@@ -544,7 +548,18 @@ function initStores(
|
|
|
544
548
|
const attribute = attributes.find((attribute) => attribute.name === existingAttribute.name);
|
|
545
549
|
if (!attribute) {
|
|
546
550
|
if (existingAttribute.isPrimaryKey) {
|
|
547
|
-
logger.error(
|
|
551
|
+
logger.error(
|
|
552
|
+
new Error('Unable to remove existing primary key attribute'),
|
|
553
|
+
existingAttribute,
|
|
554
|
+
'from attributes',
|
|
555
|
+
existingAttributes,
|
|
556
|
+
'in',
|
|
557
|
+
tableName,
|
|
558
|
+
'requesting new attribute list',
|
|
559
|
+
attributes,
|
|
560
|
+
'full metadata list',
|
|
561
|
+
Array.from(attributesDbi.getRange({ start: false }))
|
|
562
|
+
);
|
|
548
563
|
continue;
|
|
549
564
|
}
|
|
550
565
|
if (existingAttribute.indexed) {
|
|
@@ -581,7 +596,7 @@ function initStores(
|
|
|
581
596
|
indices,
|
|
582
597
|
attributes,
|
|
583
598
|
schemaDefined: primaryAttribute.schemaDefined,
|
|
584
|
-
dbisDB:
|
|
599
|
+
dbisDB: attributesDbi,
|
|
585
600
|
})
|
|
586
601
|
);
|
|
587
602
|
table.schemaVersion = 1;
|
|
@@ -130,8 +130,8 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
130
130
|
// Generate random level for this new element
|
|
131
131
|
const level = oldNode.level ?? Math.min(Math.floor(-Math.log(Math.random()) * this.mL), MAX_LEVEL);
|
|
132
132
|
let currentLevel = entryPoint.level;
|
|
133
|
-
if (level
|
|
134
|
-
// if we are at
|
|
133
|
+
if (level > currentLevel) {
|
|
134
|
+
// if we are at a higher level, make this the new entry point
|
|
135
135
|
if (typeof nodeId !== 'number') {
|
|
136
136
|
throw new Error('Invalid nodeId: ' + nodeId);
|
|
137
137
|
}
|
|
@@ -142,7 +142,14 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
142
142
|
// For each level from top to bottom
|
|
143
143
|
while (currentLevel > level) {
|
|
144
144
|
// Search for closest neighbors at current level
|
|
145
|
-
const neighbors = this.searchLayer(
|
|
145
|
+
const neighbors = this.searchLayer(
|
|
146
|
+
vector,
|
|
147
|
+
entryPointId,
|
|
148
|
+
entryPoint,
|
|
149
|
+
this.efConstruction,
|
|
150
|
+
currentLevel,
|
|
151
|
+
options
|
|
152
|
+
);
|
|
146
153
|
|
|
147
154
|
if (neighbors.length > 0) {
|
|
148
155
|
entryPointId = neighbors[0].id; // closest neighbor becomes new entry point
|
|
@@ -157,7 +164,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
157
164
|
|
|
158
165
|
// Connect the new element to neighbors at its level and below
|
|
159
166
|
for (let l = Math.min(level, currentLevel); l >= 0; l--) {
|
|
160
|
-
let neighbors = this.searchLayer(vector, entryPointId, entryPoint, this.efConstruction, l);
|
|
167
|
+
let neighbors = this.searchLayer(vector, entryPointId, entryPoint, this.efConstruction, l, options);
|
|
161
168
|
neighbors = neighbors.slice(0, this.M << 1) as SearchResults;
|
|
162
169
|
|
|
163
170
|
if (neighbors.length === 0 && l === 0) {
|
|
@@ -232,6 +239,19 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
232
239
|
oldNode[l] = oldConnections;
|
|
233
240
|
}
|
|
234
241
|
oldConnections.splice(oldPosition, 1);
|
|
242
|
+
// update the distance in the reverse connection if the vector changed
|
|
243
|
+
if (oldConnection.distance !== distance) {
|
|
244
|
+
const neighborNode = updateNode(id, node);
|
|
245
|
+
if (neighborNode[l]) {
|
|
246
|
+
if (Object.isFrozen(neighborNode[l])) {
|
|
247
|
+
neighborNode[l] = neighborNode[l].slice();
|
|
248
|
+
}
|
|
249
|
+
const reverseIdx = neighborNode[l].findIndex(({ id: nid }) => nid === nodeId);
|
|
250
|
+
if (reverseIdx >= 0) {
|
|
251
|
+
neighborNode[l][reverseIdx] = { id: nodeId, distance };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
235
255
|
} else {
|
|
236
256
|
// add new connection since this is truly a new connection now
|
|
237
257
|
this.addConnection(id, updateNode(id, node), nodeId, l, distance, updateNode, options);
|
|
@@ -323,16 +343,16 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
323
343
|
this.indexStore.put(id, updatedNode, options);
|
|
324
344
|
}
|
|
325
345
|
for (const [key, vector] of needsReindexing) {
|
|
326
|
-
this.index(key, vector, vector);
|
|
346
|
+
this.index(key, vector, vector, options);
|
|
327
347
|
}
|
|
328
348
|
this.checkSymmetry(nodeId, this.indexStore.getSync(nodeId, options), options);
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
private getEntryPoint() {
|
|
351
|
+
private getEntryPoint(options: { transaction?: any } = {}) {
|
|
332
352
|
// Get entry point
|
|
333
|
-
const entryPointId = this.indexStore.getSync(ENTRY_POINT);
|
|
353
|
+
const entryPointId = this.indexStore.getSync(ENTRY_POINT, options);
|
|
334
354
|
if (entryPointId === undefined) return;
|
|
335
|
-
const node = this.indexStore.getSync(entryPointId);
|
|
355
|
+
const node = this.indexStore.getSync(entryPointId, options);
|
|
336
356
|
return { id: entryPointId, ...node };
|
|
337
357
|
}
|
|
338
358
|
|
|
@@ -346,6 +366,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
346
366
|
* @param ef
|
|
347
367
|
* @param level
|
|
348
368
|
* @param distanceFunction
|
|
369
|
+
* @param options
|
|
349
370
|
* @private
|
|
350
371
|
*/
|
|
351
372
|
private searchLayer(
|
|
@@ -354,13 +375,14 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
354
375
|
entryPoint: any,
|
|
355
376
|
ef: number,
|
|
356
377
|
level: number,
|
|
378
|
+
options: { transaction?: any } = {},
|
|
357
379
|
distanceFunction = this.distance
|
|
358
380
|
): SearchResults {
|
|
359
381
|
const visited = new Set([entryPointId]);
|
|
360
382
|
const candidates = [
|
|
361
383
|
{
|
|
362
384
|
id: entryPointId,
|
|
363
|
-
distance:
|
|
385
|
+
distance: distanceFunction(queryVector, entryPoint.vector),
|
|
364
386
|
node: entryPoint,
|
|
365
387
|
},
|
|
366
388
|
];
|
|
@@ -383,7 +405,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
383
405
|
if (visited.has(neighborId) || neighborId === undefined) continue;
|
|
384
406
|
visited.add(neighborId);
|
|
385
407
|
|
|
386
|
-
const neighbor = this.indexStore.getSync(neighborId);
|
|
408
|
+
const neighbor = this.indexStore.getSync(neighborId, options);
|
|
387
409
|
if (!neighbor) continue;
|
|
388
410
|
this.nodesVisitedCount++;
|
|
389
411
|
const distance = distanceFunction(queryVector, neighbor.vector);
|
|
@@ -416,19 +438,22 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
416
438
|
* @param comparator
|
|
417
439
|
* @param context
|
|
418
440
|
*/
|
|
419
|
-
search(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
441
|
+
search(
|
|
442
|
+
{
|
|
443
|
+
target,
|
|
444
|
+
value,
|
|
445
|
+
descending,
|
|
446
|
+
distance,
|
|
447
|
+
comparator,
|
|
448
|
+
}: {
|
|
449
|
+
target: number[];
|
|
450
|
+
value: number;
|
|
451
|
+
descending: boolean;
|
|
452
|
+
distance: string;
|
|
453
|
+
comparator: string;
|
|
454
|
+
},
|
|
455
|
+
context: any
|
|
456
|
+
) {
|
|
432
457
|
let limit = 0; // zero is ignored, only used if set below
|
|
433
458
|
switch (comparator) {
|
|
434
459
|
case 'lt':
|
|
@@ -449,14 +474,23 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
449
474
|
if (!target) throw new ClientError('A target vector must be provided for an HNSW query');
|
|
450
475
|
if (!Array.isArray(target)) throw new ClientError('The target vector must be an array');
|
|
451
476
|
|
|
452
|
-
|
|
477
|
+
const options = context.transaction; // should have a nested RocksDB transaction
|
|
478
|
+
let entryPoint = this.getEntryPoint(options);
|
|
453
479
|
if (!entryPoint) return [];
|
|
454
480
|
let entryPointId = entryPoint.id;
|
|
455
481
|
let results: Candidate[] = [];
|
|
456
482
|
// For each level from top to bottom
|
|
457
483
|
for (let l = entryPoint.level; l >= 0; l--) {
|
|
458
484
|
// Search for closest neighbors at current level
|
|
459
|
-
results = this.searchLayer(
|
|
485
|
+
results = this.searchLayer(
|
|
486
|
+
target,
|
|
487
|
+
entryPointId,
|
|
488
|
+
entryPoint,
|
|
489
|
+
this.efConstructionSearch,
|
|
490
|
+
l,
|
|
491
|
+
options,
|
|
492
|
+
distanceFunction
|
|
493
|
+
);
|
|
460
494
|
|
|
461
495
|
if (results.length > 0) {
|
|
462
496
|
const neighbor = results[0]; // closest neighbor becomes new entry point
|
|
@@ -487,7 +521,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
487
521
|
// verify that the connection is symmetrical
|
|
488
522
|
const symmetrical = neighborNode[l]?.find(({ id: nid }) => nid == id);
|
|
489
523
|
if (!symmetrical) {
|
|
490
|
-
logger.info?.('asymmetry detected', neighborNode[l]);
|
|
524
|
+
logger.info?.('asymmetry detected', neighborNode[l], 'does not have', id);
|
|
491
525
|
}
|
|
492
526
|
}
|
|
493
527
|
l++;
|
|
@@ -531,10 +565,13 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
531
565
|
if (removedNode) {
|
|
532
566
|
// Remove the reverse connection if it exists
|
|
533
567
|
if (removedNode[level]) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
logger.info?.('
|
|
568
|
+
const filtered = removedNode[level].filter(({ id }) => id !== fromId);
|
|
569
|
+
if (level === 0 && filtered.length === 0) {
|
|
570
|
+
// don't remove the last connection at level 0 — it would orphan this node
|
|
571
|
+
logger.info?.('skipping removal of last connection', fromId, toId);
|
|
572
|
+
} else {
|
|
573
|
+
removedNode = updateNode(removed.id, removedNode);
|
|
574
|
+
removedNode[level] = filtered;
|
|
538
575
|
}
|
|
539
576
|
}
|
|
540
577
|
}
|
|
@@ -95,7 +95,7 @@ export async function verifyOCSP(
|
|
|
95
95
|
method: cached.method || 'ocsp',
|
|
96
96
|
};
|
|
97
97
|
} catch (error) {
|
|
98
|
-
logger.error?.(`OCSP verification error
|
|
98
|
+
logger.error?.(`OCSP verification error:`, error);
|
|
99
99
|
|
|
100
100
|
// Check failure mode
|
|
101
101
|
if (config.failureMode === 'fail-closed') {
|
package/security/jsLoader.ts
CHANGED
|
@@ -14,7 +14,16 @@ import * as child_process from 'node:child_process';
|
|
|
14
14
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
15
15
|
import { contentTypes } from '../server/serverHelpers/contentTypes.ts';
|
|
16
16
|
import type { CompartmentOptions } from 'ses';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
openSync,
|
|
23
|
+
closeSync,
|
|
24
|
+
statSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
} from 'node:fs';
|
|
18
27
|
import { join } from 'node:path';
|
|
19
28
|
import { EventEmitter } from 'node:events';
|
|
20
29
|
import { whenComponentsLoaded } from '../server/threads/threadServer.js';
|
|
@@ -495,13 +504,13 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope, useC
|
|
|
495
504
|
}
|
|
496
505
|
|
|
497
506
|
if (url.startsWith('file://') && usePrivateGlobal) {
|
|
498
|
-
checkAllowedModulePath(url, scope.
|
|
507
|
+
checkAllowedModulePath(url, scope.allowedPath);
|
|
499
508
|
const source = readFileSync(new URL(url), { encoding: 'utf-8' });
|
|
500
509
|
return createModuleFromSource(url, source, usePrivateGlobal);
|
|
501
510
|
}
|
|
502
511
|
|
|
503
512
|
// For Node.js built-in modules (node:) and npm packages without application loader for dependency
|
|
504
|
-
const replacedModule = checkAllowedModulePath(url, scope.
|
|
513
|
+
const replacedModule = checkAllowedModulePath(url, scope.allowedPath);
|
|
505
514
|
if (replacedModule) {
|
|
506
515
|
return createSyntheticModule(url, normalizeImportedModule(replacedModule));
|
|
507
516
|
}
|
|
@@ -570,7 +579,7 @@ async function getCompartment(scope: ApplicationScope, globals) {
|
|
|
570
579
|
}
|
|
571
580
|
return new StaticModuleRecord(moduleText, moduleSpecifier);
|
|
572
581
|
} else {
|
|
573
|
-
checkAllowedModulePath(moduleSpecifier, scope.
|
|
582
|
+
checkAllowedModulePath(moduleSpecifier, scope.allowedPath);
|
|
574
583
|
const moduleExports = await import(moduleSpecifier);
|
|
575
584
|
return {
|
|
576
585
|
imports: [],
|
|
@@ -761,23 +770,54 @@ function isProcessRunning(pid: number): boolean {
|
|
|
761
770
|
* Acquires an exclusive lock using the PID file itself (synchronously with busy-wait)
|
|
762
771
|
* Returns 0 if lock was acquired (need to spawn new process), or the existing PID if process is running
|
|
763
772
|
*/
|
|
764
|
-
function
|
|
773
|
+
function parsePidFile(content: string): { pid: number; version: number } {
|
|
774
|
+
const lines = content.trim().split('\n');
|
|
775
|
+
const pid = Number.parseInt(lines[0], 10);
|
|
776
|
+
const version = lines.length > 1 ? parseInt(lines[1], 10) : 0;
|
|
777
|
+
return { pid, version };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function acquirePidFileLock(
|
|
781
|
+
pidFilePath: string,
|
|
782
|
+
requestedVersion?: number,
|
|
783
|
+
maxRetries = 100,
|
|
784
|
+
retryDelay = 5
|
|
785
|
+
): { pid: number; version: number } {
|
|
765
786
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
766
787
|
try {
|
|
767
788
|
// Try to open exclusively - 'wx' fails if file exists
|
|
768
789
|
const fd = openSync(pidFilePath, 'wx');
|
|
769
790
|
closeSync(fd);
|
|
770
|
-
return 0; // Successfully acquired lock (file created), caller should spawn process
|
|
791
|
+
return { pid: 0, version: 0 }; // Successfully acquired lock (file created), caller should spawn process
|
|
771
792
|
} catch (err) {
|
|
772
793
|
if (err.code === 'EEXIST') {
|
|
773
794
|
// File exists - check if it contains a valid running process
|
|
774
795
|
try {
|
|
775
796
|
const pidContent = readFileSync(pidFilePath, 'utf-8');
|
|
776
|
-
const existingPid =
|
|
797
|
+
const { pid: existingPid, version: existingVersion } = parsePidFile(pidContent);
|
|
777
798
|
|
|
778
799
|
if (!isNaN(existingPid) && isProcessRunning(existingPid)) {
|
|
779
|
-
//
|
|
780
|
-
|
|
800
|
+
// If the version isn't the one we want, kill the existing process and re-acquire
|
|
801
|
+
if (requestedVersion != null && requestedVersion !== existingVersion) {
|
|
802
|
+
try {
|
|
803
|
+
process.kill(existingPid);
|
|
804
|
+
} catch {
|
|
805
|
+
// Process may have already exited
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
unlinkSync(pidFilePath);
|
|
809
|
+
} catch {
|
|
810
|
+
// Another thread may have removed it
|
|
811
|
+
}
|
|
812
|
+
// Retry to acquire the lock for the new version
|
|
813
|
+
const start = Date.now();
|
|
814
|
+
while (Date.now() - start < retryDelay) {
|
|
815
|
+
// Busy wait for process cleanup
|
|
816
|
+
}
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
// Valid process is running at same or higher version, return its PID
|
|
820
|
+
return { pid: existingPid, version: existingVersion };
|
|
781
821
|
}
|
|
782
822
|
|
|
783
823
|
// Invalid/empty PID - check file age to determine if it's stale or being written
|
|
@@ -824,6 +864,7 @@ function createSpawn(spawnFunction: (...args: any) => child_process.ChildProcess
|
|
|
824
864
|
throw new Error(
|
|
825
865
|
`Calling ${spawnFunction.name} in Harper must have a process "name" in the options to ensure that a single process is started and reused`
|
|
826
866
|
);
|
|
867
|
+
const requestedVersion = options?.version;
|
|
827
868
|
|
|
828
869
|
// Ensure PID directory exists
|
|
829
870
|
const pidDir = join(basePath, 'pids');
|
|
@@ -831,20 +872,22 @@ function createSpawn(spawnFunction: (...args: any) => child_process.ChildProcess
|
|
|
831
872
|
|
|
832
873
|
const pidFilePath = join(pidDir, `${processName}.pid`);
|
|
833
874
|
|
|
834
|
-
// Try to acquire lock - returns 0 if acquired, or existing PID
|
|
835
|
-
const
|
|
875
|
+
// Try to acquire lock - returns pid: 0 if acquired, or existing PID/version
|
|
876
|
+
const existing = acquirePidFileLock(pidFilePath, requestedVersion);
|
|
836
877
|
|
|
837
|
-
if (
|
|
878
|
+
if (existing.pid !== 0) {
|
|
838
879
|
// Existing process is running, return wrapper
|
|
839
|
-
return new ExistingProcessWrapper(
|
|
880
|
+
return new ExistingProcessWrapper(existing.pid);
|
|
840
881
|
}
|
|
841
882
|
|
|
842
883
|
// We acquired the lock (file was created), spawn new process
|
|
843
884
|
const childProcess = spawnFunction(command, args, options, callback);
|
|
844
885
|
|
|
845
|
-
// Write PID to the file we just created
|
|
886
|
+
// Write PID (and version if provided) to the file we just created
|
|
887
|
+
const pidFileContent =
|
|
888
|
+
requestedVersion != null ? `${childProcess.pid}\n${requestedVersion}` : childProcess.pid.toString();
|
|
846
889
|
try {
|
|
847
|
-
writeFileSync(pidFilePath,
|
|
890
|
+
writeFileSync(pidFilePath, pidFileContent, 'utf-8');
|
|
848
891
|
} catch (err) {
|
|
849
892
|
// Failed to write PID, clean up
|
|
850
893
|
try {
|
|
@@ -869,21 +912,24 @@ function createSpawn(spawnFunction: (...args: any) => child_process.ChildProcess
|
|
|
869
912
|
|
|
870
913
|
/**
|
|
871
914
|
* Validates whether a module can be loaded based on security restrictions and returns the module path or replacement.
|
|
872
|
-
* For file URLs, ensures the module is within the
|
|
915
|
+
* For file URLs, ensures the module is within the allowed path.
|
|
873
916
|
* For node built-in modules, checks against an allowlist and returns any replacements.
|
|
874
917
|
*
|
|
875
918
|
* @param {string} moduleUrl - The URL or identifier of the module to be loaded, which may be a file: URL, node: URL, or bare module specifier.
|
|
876
|
-
* @param {string}
|
|
919
|
+
* @param {string} allowedPath - The absolute path that the module is allowed to load from.
|
|
877
920
|
* @return {any} Returns undefined for allowed file paths, or a replacement module identifier for allowed node built-in modules.
|
|
878
|
-
* @throws {Error} Throws an error if the module is outside the
|
|
921
|
+
* @throws {Error} Throws an error if the module is outside the allowed path or if the module is not in the allowed list.
|
|
879
922
|
*/
|
|
880
|
-
function checkAllowedModulePath(moduleUrl: string,
|
|
923
|
+
function checkAllowedModulePath(moduleUrl: string, allowedPath?: string): boolean {
|
|
881
924
|
if (moduleUrl.startsWith('file:')) {
|
|
882
|
-
|
|
883
|
-
|
|
925
|
+
let path = moduleUrl.slice(7);
|
|
926
|
+
try {
|
|
927
|
+
path = realpathSync(path);
|
|
928
|
+
} catch {}
|
|
929
|
+
if (!allowedPath || path.startsWith(allowedPath)) {
|
|
884
930
|
return;
|
|
885
931
|
}
|
|
886
|
-
throw new Error(`Can not load module outside of
|
|
932
|
+
throw new Error(`Can not load module outside of allowed path`);
|
|
887
933
|
}
|
|
888
934
|
let simpleName = moduleUrl.startsWith('node:') ? moduleUrl.slice(5) : moduleUrl;
|
|
889
935
|
simpleName = simpleName.split('/')[0];
|