Skip to content

Commit 763994e

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat-mcp-metadata-1095
# Conflicts: # resources/graphql.ts
2 parents 70fe0d4 + 88c94e6 commit 763994e

102 files changed

Lines changed: 16004 additions & 19255 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/integration-tests.yml

Lines changed: 0 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -74,194 +74,6 @@ jobs:
7474
package.json
7575
retention-days: 1
7676

77-
run-integration-apiTests:
78-
name: Integration API Tests (Node.js v${{ matrix.node-version }})
79-
needs: [generate-node-version-matrix, build]
80-
runs-on: ubuntu-latest
81-
timeout-minutes: 15
82-
strategy:
83-
fail-fast: false
84-
matrix:
85-
node-version: ${{ fromJSON(needs.generate-node-version-matrix.outputs.node-versions) }}
86-
87-
steps:
88-
- name: Checkout code
89-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
90-
91-
- name: Setup Node.js ${{ matrix.node-version }}
92-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
93-
with:
94-
node-version: ${{ matrix.node-version }}
95-
package-manager-cache: false
96-
97-
- name: Download build artifacts
98-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
99-
with:
100-
name: harper-build-artifacts-node-${{ matrix.node-version }}
101-
102-
- name: Setup Harper
103-
env:
104-
DEFAULTS_MODE: 'dev'
105-
HDB_ADMIN_USERNAME: 'admin'
106-
HDB_ADMIN_PASSWORD: 'password'
107-
ROOTPATH: '/tmp/hdb'
108-
OPERATIONSAPI_NETWORK_PORT: 9925
109-
LOGGING_LEVEL: 'debug'
110-
LOGGING_STDSTREAMS: true
111-
THREADS_COUNT: 1
112-
THREADS_DEBUG: false
113-
NODE_HOSTNAME: 'localhost'
114-
run: |
115-
rm -rf /tmp/hdb
116-
mkdir -p /tmp/hdb/log
117-
node ./dist/bin/harper.js install > /tmp/hdb/log/install-stdout.log 2> /tmp/hdb/log/install-stderr.log
118-
node ./dist/bin/harper.js start > /tmp/hdb/log/start-stdout.log 2> /tmp/hdb/log/start-stderr.log &
119-
echo "Waiting for Harper to be ready on port 9925..."
120-
timeout 120 bash -c 'until nc -z localhost 9925 2>/dev/null; do sleep 0.5; done'
121-
echo "Harper is ready."
122-
123-
- name: Run API Tests
124-
id: run-api-tests
125-
env:
126-
HDB_ADMIN_USERNAME: 'admin'
127-
HDB_ADMIN_PASSWORD: 'password'
128-
run: npm run test:integration:api-tests
129-
130-
- name: Upload Harper logs
131-
if: failure()
132-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
133-
with:
134-
name: harper-integration-api-test-logs-node-${{ matrix.node-version }}
135-
path: /tmp/hdb/log/
136-
retention-days: 3
137-
if-no-files-found: ignore
138-
139-
run-integration-apiTests-bun:
140-
name: Integration API Tests (Bun)
141-
if: false
142-
needs: [build]
143-
runs-on: ubuntu-latest
144-
timeout-minutes: 15
145-
146-
steps:
147-
- name: Checkout code
148-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
149-
150-
- name: Setup Node.js
151-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
152-
with:
153-
node-version: 24
154-
package-manager-cache: false
155-
156-
- name: Setup Bun
157-
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
158-
159-
- name: Download build artifacts
160-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
161-
with:
162-
name: harper-build-artifacts-node-24
163-
164-
- name: Setup Harper
165-
env:
166-
DEFAULTS_MODE: 'dev'
167-
HDB_ADMIN_USERNAME: 'admin'
168-
HDB_ADMIN_PASSWORD: 'password'
169-
ROOTPATH: '/tmp/hdb'
170-
OPERATIONSAPI_NETWORK_PORT: 9925
171-
LOGGING_LEVEL: 'debug'
172-
LOGGING_STDSTREAMS: true
173-
THREADS_COUNT: 1
174-
THREADS_DEBUG: false
175-
NODE_HOSTNAME: 'localhost'
176-
run: |
177-
rm -rf /tmp/hdb
178-
mkdir -p /tmp/hdb/log
179-
node ./dist/bin/harper.js install > /tmp/hdb/log/install-stdout.log 2> /tmp/hdb/log/install-stderr.log
180-
sleep 10
181-
bun ./dist/bin/harper.js start > /tmp/hdb/log/start-stdout.log 2> /tmp/hdb/log/start-stderr.log &
182-
sleep 10
183-
184-
- name: Run API Tests
185-
id: run-api-tests
186-
env:
187-
HDB_ADMIN_USERNAME: 'admin'
188-
HDB_ADMIN_PASSWORD: 'password'
189-
run: npm run test:integration:api-tests
190-
191-
- name: Upload Harper logs
192-
if: failure()
193-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
194-
with:
195-
name: harper-integration-api-test-logs-bun
196-
path: /tmp/hdb/log/
197-
retention-days: 3
198-
if-no-files-found: ignore
199-
200-
run-integration-apiTests-windows:
201-
name: Integration API Tests (Windows, Node.js v24)
202-
needs: [build-windows]
203-
runs-on: windows-latest
204-
timeout-minutes: 15
205-
206-
steps:
207-
- name: Checkout code
208-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
209-
210-
- name: Setup Node.js 24
211-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
212-
with:
213-
node-version: 24
214-
package-manager-cache: false
215-
216-
- name: Download build artifacts
217-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
218-
with:
219-
name: harper-build-artifacts-windows
220-
221-
- name: Setup Harper
222-
env:
223-
DEFAULTS_MODE: 'dev'
224-
HDB_ADMIN_USERNAME: 'admin'
225-
HDB_ADMIN_PASSWORD: 'password'
226-
ROOTPATH: ${{ runner.temp }}/hdb
227-
OPERATIONSAPI_NETWORK_PORT: 9925
228-
LOGGING_LEVEL: 'debug'
229-
LOGGING_STDSTREAMS: true
230-
THREADS_COUNT: 1
231-
THREADS_DEBUG: false
232-
NODE_HOSTNAME: 'localhost'
233-
run: |
234-
New-Item -ItemType Directory -Force -Path ${{ runner.temp }}/hdb/log
235-
node ./dist/bin/harper.js install > ${{ runner.temp }}/hdb/log/install-stdout.log 2> ${{ runner.temp }}/hdb/log/install-stderr.log
236-
Start-Process node -ArgumentList "./dist/bin/harper.js start" -RedirectStandardOutput "${{ runner.temp }}/hdb/log/start-stdout.log" -RedirectStandardError "${{ runner.temp }}/hdb/log/start-stderr.log" -PassThru
237-
Write-Host "Waiting for Harper to be ready on port 9925..."
238-
$deadline = (Get-Date).AddSeconds(120)
239-
do {
240-
Start-Sleep -Milliseconds 500
241-
$client = New-Object System.Net.Sockets.TcpClient
242-
$connected = $client.ConnectAsync('localhost', 9925).Wait(2000)
243-
if ($connected) { $client.Close(); $ready = $true }
244-
else { $client.Close() }
245-
} until ($ready -or (Get-Date) -gt $deadline)
246-
if (-not $ready) { Write-Error "Harper did not start within 120s"; exit 1 }
247-
Write-Host "Harper is ready."
248-
249-
- name: Run API Tests
250-
id: run-api-tests
251-
env:
252-
HDB_ADMIN_USERNAME: 'admin'
253-
HDB_ADMIN_PASSWORD: 'password'
254-
run: npm run test:integration:api-tests
255-
256-
- name: Upload Harper logs
257-
if: failure()
258-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
259-
with:
260-
name: harper-integration-api-test-logs-windows
261-
path: ${{ runner.temp }}/hdb/log/
262-
retention-days: 3
263-
if-no-files-found: ignore
264-
26577
run-integration-tests:
26678
name: Integration Tests ${{matrix.shard}}/4 (Node.js v${{ matrix.node-version }})
26779
runs-on: ubuntu-latest

.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/deploymentRecorder.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class DeploymentRecorder {
7272
private unsubscribe: (() => void) | null = null;
7373
private pendingPut: Promise<void> | null = null;
7474
private dirty = false;
75+
private sealed = false;
7576

7677
private constructor(deploymentId: string, initial: Record<string, any>) {
7778
this.deploymentId = deploymentId;
@@ -151,6 +152,12 @@ export class DeploymentRecorder {
151152
// the record dirty; the chained continuation issues a follow-up put once the prior one
152153
// settles. This keeps event_log writes O(1) puts per burst rather than O(N) per event.
153154
private scheduleFlush(): void {
155+
if (this.sealed) {
156+
// Sealed: accumulate state in memory but don't write. finish() does the single
157+
// terminal write. See seal() for why. The emitter still emits live SSE events.
158+
this.dirty = true;
159+
return;
160+
}
154161
if (this.pendingPut) {
155162
this.dirty = true;
156163
return;
@@ -264,6 +271,24 @@ export class DeploymentRecorder {
264271
for (const result of results) this.recordPeer(result);
265272
}
266273

274+
/**
275+
* Stop persisting intermediate row updates; accumulate them in memory so finish() writes
276+
* the terminal state in a single put. Called before the replicate phase, where the row
277+
* otherwise receives a tight burst of puts (replicate phase + per-peer + finish) within
278+
* a few ms. That burst can commit out of order on a loaded peer, where an older full
279+
* update reverts the terminal `success` write — the row stays stuck at `replicating` and
280+
* never converges (harperdb/harper#1170). Collapsing to one terminal write isolates it
281+
* from any concurrent same-key write so the receiver converges.
282+
*
283+
* Tradeoff: the origin's get_deployment *polling* view skips the transient `replicating`
284+
* status and incremental peer_results during the final phase; live SSE tailing is
285+
* unaffected (the emitter still emits in real time). Once #1170 lands this seal can be
286+
* removed to restore incremental peer_results persistence.
287+
*/
288+
seal(): void {
289+
this.sealed = true;
290+
}
291+
267292
async finish(status: 'success' | 'failed' | 'rolled_back', error?: unknown): Promise<void> {
268293
if (this.finished) return;
269294
// Send a terminal sentinel through the emitter (if any) BEFORE we unsubscribe and

components/operations.js

Lines changed: 6 additions & 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

@@ -507,6 +508,11 @@ async function deployComponent(req) {
507508
emit('peer', result);
508509
}
509510
: undefined;
511+
// Seal the recorder before the replicate phase so the row's terminal write (finish())
512+
// isn't part of the tight put burst that can commit out of order on a peer and revert
513+
// it (harperdb/harper#1170). onPeerResult/peer_results accumulate in memory and land in
514+
// finish()'s single write; live SSE 'peer' events still fire below.
515+
recorder?.seal();
510516
emit('phase', { phase: 'replicate', status: 'start' });
511517
let response = await server.replication.replicateOperation(req, { onPeerResult });
512518
emit('phase', { phase: 'replicate', status: 'done' });

components/operationsValidation.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,15 @@ function deployComponentValidator(req) {
242242
install_timeout: Joi.number().optional(),
243243
install_allow_scripts: Joi.boolean().optional(),
244244
force: Joi.boolean().optional(),
245-
});
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 ".."' }),
253+
}).with('urlPath', 'package');
246254

247255
return validator.validateBySchema(req, deployProjSchema);
248256
}

config-root.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@
460460
"description": "Max write queue time before rejecting (e.g. '45s')."
461461
},
462462
"noReadAhead": { "type": "boolean", "description": "Advise OS to not read ahead. Default: false" },
463+
"randomAccessFields": {
464+
"type": "boolean",
465+
"description": "Encode records as typed random-access structures, optimizing for fast field access and smaller records. Best for tables with stable, homogeneous field types; wide or variably-typed schemas should leave this off. Applies to each primary table on open, so changing it switches encoding for tables that have not pinned the choice via the @table(randomAccessFields:) directive (safe at runtime: existing records still decode, only new writes change). Default: false"
466+
},
463467
"prefetchWrites": { "type": "boolean", "description": "Load data prior to write transactions. Default: true" },
464468
"path": { "type": "string", "description": "Directory for all database files. Default: <rootPath>/database" },
465469
"blobPaths": {

integrationTests/apiTests/.env.example

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

0 commit comments

Comments
 (0)