Skip to content

Commit e6b5a5d

Browse files
committed
Merge remote-tracking branch 'origin/main' into kris/registry-deploy-auth-core
# Conflicts: # components/operationsValidation.js # unitTests/server/fastifyRoutes/operationsValidation.test.js
2 parents 4e90560 + 02dcf72 commit e6b5a5d

20 files changed

Lines changed: 281 additions & 62 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ harper-*.tgz
5050
# AI
5151
.antigravitycli/
5252
.claude/
53+
cache/
5354

5455
# YCSB benchmark results (generated)
5556
benchmarks/ycsb/results/

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ npx mocha unitTests/resources/mytest.js
4343

4444
TypeScript is stripped at runtime via `--conditions=typestrip` (Node.js native type stripping) — no compilation required for development. Use `npm run test:unit:typestrip` to run tests with this mode.
4545

46+
**Test timing:** prefer condition-waits over fixed `delay(N)` sleeps. `await delay(N); assert(sideEffectHappened)` races against loaded runners and is the root cause of a class of flakiness (#1138). Use the shared `waitFor(condition, timeout?, interval?)` helper in `unitTests/waitFor.js` to poll until the actual condition holds. Reserve fixed sleeps for genuinely modeling elapsed time (TTL/expiry windows) or asserting a non-event (that something has _not_ happened yet).
47+
4648
---
4749

4850
## Architecture

components/componentLoader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function loadComponentDirectories(loadedPluginModules?: Map<any, any>, lo
7777
}
7878
const hdbAppFolder = process.env.RUN_HDB_APP;
7979
if (hdbAppFolder) {
80+
if (getWorkerIndex() === 0) harperLogger.info?.('Loading application from ' + hdbAppFolder);
8081
cfsLoaded.push(
8182
loadComponent(hdbAppFolder, resources, hdbAppFolder, {
8283
isRoot: false,

components/operations.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ async function deployComponent(req) {
384384
allowInstallScripts: req.install_allow_scripts,
385385
};
386386
}
387+
if (req.urlPath !== undefined) applicationConfig.urlPath = req.urlPath;
387388
await configUtils.addConfig(req.project, applicationConfig);
388389
}
389390

components/operationsValidation.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ function deployComponentValidator(req) {
242242
install_timeout: Joi.number().optional(),
243243
install_allow_scripts: Joi.boolean().optional(),
244244
force: Joi.boolean().optional(),
245+
urlPath: Joi.string()
246+
.min(1)
247+
.custom((value, helpers) => {
248+
if (value.includes('..')) return helpers.error('any.invalid');
249+
return value;
250+
})
251+
.optional()
252+
.messages({ 'any.invalid': 'urlPath must not contain ".."' }),
245253
// Transient private-registry auth: never persisted, never replicated. Used only for this
246254
// node's npm pack/install during the deploy.
247255
registryAuth: Joi.array()
@@ -263,7 +271,7 @@ function deployComponentValidator(req) {
263271
})
264272
)
265273
.optional(),
266-
});
274+
}).with('urlPath', 'package');
267275

268276
return validator.validateBySchema(req, deployProjSchema);
269277
}

resources/graphql.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Resources } from './Resources.ts';
66
import type { NamedTypeNode, StringValueNode } from 'graphql';
77
import { once } from 'node:events';
88
import { ClientError } from '../utility/errors/hdbError.ts';
9+
import harperLogger from '../utility/logging/harper_logger.ts';
910

1011
const PRIMITIVE_TYPES = ['ID', 'Int', 'Float', 'Long', 'String', 'Boolean', 'Date', 'Bytes', 'Any', 'BigInt', 'Blob'];
1112

@@ -39,7 +40,13 @@ server.knownGraphQLDirectives.push(
3940
* @param resources
4041
*/
4142
export function handleApplication(scope: import('../components/Scope.ts').Scope) {
43+
let initialLoadComplete = false;
4244
const entryHandler = scope.handleEntry(async (entry) => {
45+
if (initialLoadComplete) {
46+
scope.requestRestart();
47+
return;
48+
}
49+
4350
if (entry.eventType === 'unlink') return;
4451
if (entry.entryType === 'directory') {
4552
scope.logger.warn?.('graphqlSchema currently does not handle directories. Specify file patterns only.');
@@ -48,7 +55,11 @@ export function handleApplication(scope: import('../components/Scope.ts').Scope)
4855

4956
await processGraphQLSchema((entry as any).contents, entry.urlPath, entry.absolutePath, scope.resources);
5057
});
51-
return once(entryHandler, 'initialLoadComplete');
58+
const initialLoadPromise = once(entryHandler, 'initialLoadComplete');
59+
initialLoadPromise.then(() => {
60+
initialLoadComplete = true;
61+
});
62+
return initialLoadPromise;
5263
}
5364

5465
async function processGraphQLSchema(gqlContent, urlPath, filePath, resources) {
@@ -251,6 +262,11 @@ async function processGraphQLSchema(gqlContent, urlPath, filePath, resources) {
251262
// with graphql database definitions, this is a declaration that the table should exist and that it
252263
// should be created if it does not exist
253264
typeDef.tableClass = table(typeDef);
265+
if (getWorkerIndex() === 0) {
266+
const pk = (typeDef.properties as any[])?.find((p) => p.isPrimaryKey)?.name ?? 'id';
267+
const schemaPart = typeDef.database ? `, schema: ${typeDef.database}` : '';
268+
harperLogger.info?.(`Initialized table "${typeDef.table}"${schemaPart}, primaryKey: ${pk}`);
269+
}
254270
if (typeDef.export) {
255271
// allow empty string to be used to declare a table on the root path
256272
if (typeDef.export.name === '') resources.set(dirname(urlPath), typeDef.tableClass);

resources/indexes/HierarchicalNavigableSmallWorld.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export class HierarchicalNavigableSmallWorld {
156156
distance: (a: number[], b: number[]) => number;
157157
int8 = false; // store vectors as int8-quantized bins (set via the `quantization` index option)
158158
efSearchConfigured = false; // whether the schema set an explicit search ef; if not, search ef auto-scales with N
159+
// Caches the Int8Array-converted clone of a frozen (decoded-from-disk) int8 node, keyed by the
160+
// frozen node the object store hands back. WeakMap so entries are collected when the store evicts
161+
// the frozen node — without it, every cache hit on a frozen node would re-slice and re-clone.
162+
private convertedNodes = new WeakMap<object, any>();
159163
constructor(indexStore: any, options: any) {
160164
this.indexStore = indexStore;
161165
if (indexStore) {
@@ -495,15 +499,27 @@ export class HierarchicalNavigableSmallWorld {
495499

496500
private safeGetSync(key: any, options?: any): any {
497501
try {
498-
const node = this.indexStore.getSync(key, options);
502+
let node = this.indexStore.getSync(key, options);
499503
// A quantized vector decodes as a bin (Uint8Array/Buffer) that is a view into the
500504
// store's read buffer, which may be reused on the next getSync — so copy the bytes
501505
// into a retained Int8Array (raw two's-complement reinterpret). The Int8Array guard
502506
// skips re-conversion when the object store (useObjectStore) hands back an
503507
// already-converted cached node. Float nodes (vector is a number[]) pass through.
504508
if (node && node.vector && !Array.isArray(node.vector) && !(node.vector instanceof Int8Array)) {
509+
// A node decoded from disk (a cache miss, common once the table outgrows the object
510+
// cache) is frozen — the index store sets freezeData — so assigning node.vector would
511+
// throw and the catch below would silently drop the node, fragmenting the graph (#1161).
512+
// Clone the frozen node, memoizing the clone against the frozen node so repeated cache
513+
// hits skip re-slicing/re-cloning. Mutate in place only the writable just-written object.
514+
const cached = this.convertedNodes.get(node);
515+
if (cached) return cached;
505516
const u8 = node.vector as Uint8Array;
506-
node.vector = new Int8Array(u8.buffer, u8.byteOffset, u8.byteLength).slice();
517+
const vector = new Int8Array(u8.buffer, u8.byteOffset, u8.byteLength).slice();
518+
if (Object.isFrozen(node)) {
519+
const converted = { ...node, vector };
520+
this.convertedNodes.set(node, converted);
521+
node = converted;
522+
} else node.vector = vector;
507523
}
508524
return node;
509525
} catch {

unitTests/components/EntryHandler.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { tmpdir } = require('node:os');
66
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = require('node:fs');
77
const { writeFile, mkdir } = require('node:fs/promises');
88
const { spy } = require('sinon');
9-
const { waitFor } = require('./waitFor.js');
9+
const { waitFor } = require('../waitFor.js');
1010

1111
function generateFixture(dirPath, fixture) {
1212
mkdirSync(dirPath, { recursive: true });

unitTests/components/Scope.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const { Resources } = require('#src/resources/Resources');
1212
const { EntryHandler } = require('#src/components/EntryHandler');
1313
const { restartNeeded, resetRestartNeeded } = require('#src/components/requestRestart');
1414
const { writeFile } = require('node:fs/promises');
15-
const { waitFor } = require('./waitFor.js');
15+
const { waitFor } = require('../waitFor.js');
1616
const { ApplicationScope } = require('#src/components/ApplicationScope');
1717
const { deployLifecycle, _resetForTests: resetDeployLifecycle } = require('#src/components/deployLifecycle');
1818

unitTests/components/waitFor.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)