Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cee96ae
Add Unix Domain Socket mirrors for TLS ports with metadata export
kriszyp Apr 15, 2026
b84e9f8
Formatting
kriszyp Apr 15, 2026
98ff66f
Use rocksdb-js's currentThreadId for thread id
kriszyp Apr 16, 2026
156ffea
Don't bypass auth on (by default) on secured port mirroring UDS
kriszyp Apr 16, 2026
52b9844
Parse PROXY protocol v1 header on UDS mirror servers
kriszyp Apr 16, 2026
bd5df94
Upgrade rocksdb-js
kriszyp Apr 17, 2026
f8dcfc1
Add Bun runtime support
kriszyp Apr 18, 2026
b6400a6
Pin oven-sh/setup-bun to full commit SHA
kriszyp Apr 18, 2026
3d033c1
Guard killHarper/teardownHarper when ctx.harper is unset
kriszyp Apr 18, 2026
558a4bb
Fix Bun server never binding to port
kriszyp Apr 18, 2026
252001e
Fix 401 on unauthenticated loopback requests to operations API on Bun
kriszyp Apr 18, 2026
27bb70e
Fix static file serving on Bun via Writable shim + finished: false
kriszyp Apr 18, 2026
98e21ff
Clean up, test fixes
kriszyp Apr 18, 2026
730fc88
Fixes and debugging
kriszyp Apr 18, 2026
6875e54
Revert runtime param and just skip upgrade test for bun
kriszyp Apr 18, 2026
fd81fc1
Propagate Connection: close
kriszyp Apr 19, 2026
d2123c1
Send port removal message because Bun doesn't fire a close event for …
kriszyp Apr 19, 2026
2885b81
Handle `worker.terminate()` NAPI segfault on Bun by implementing `FOR…
kriszyp Apr 19, 2026
be48406
Add benchmark
kriszyp Apr 19, 2026
137bcb0
Update `enableProxyProtocol` to ensure race-free handling of PROXY v1…
kriszyp Apr 19, 2026
7117367
Testing updates
kriszyp Apr 20, 2026
731488d
Register `unhandledRejection` listener to prevent worker crashes; imp…
kriszyp Apr 20, 2026
bcb10b7
Update `enableProxyProtocol` to ensure race-free handling of PROXY v1…
kriszyp Apr 19, 2026
c23b70f
Merge remote-tracking branch 'origin/main' into symphony-prep
kriszyp Apr 21, 2026
ceb85c4
Merge remote-tracking branch 'origin/main' into bun-runtime-support
kriszyp Apr 21, 2026
4e38737
Remove socket routing and session affinity; simplify Windows support …
kriszyp Apr 21, 2026
2a78e83
Merge branch 'symphony-prep' into bun-runtime-support
kriszyp Apr 21, 2026
3d934b9
Maybe give Windows more time
kriszyp Apr 21, 2026
3a09f4c
Skip API tests for Bun for now
kriszyp Apr 21, 2026
a018ae5
Add Unix Domain Socket mirrors for TLS ports with metadata export
kriszyp Apr 15, 2026
d48808f
Formatting
kriszyp Apr 15, 2026
1536a33
Use rocksdb-js's currentThreadId for thread id
kriszyp Apr 16, 2026
1fe04cd
Don't bypass auth on (by default) on secured port mirroring UDS
kriszyp Apr 16, 2026
56d6b30
Parse PROXY protocol v1 header on UDS mirror servers
kriszyp Apr 16, 2026
329a89e
Update `enableProxyProtocol` to ensure race-free handling of PROXY v1…
kriszyp Apr 19, 2026
cf6f6a6
Formatting
kriszyp Apr 23, 2026
2d46a4e
Remove unintended Bun changes
kriszyp Apr 23, 2026
a8a6b5c
Merge branch 'symphony-prep' into bun-runtime-support
kriszyp Apr 23, 2026
4ee9327
Improve static file handling, URL path derivation, and add MQTT port …
kriszyp Apr 24, 2026
e9ac677
Properly set hostname on non-HTTP ports for Bun
kriszyp Apr 24, 2026
4120e56
Skip Windows upgrade test
kriszyp Apr 24, 2026
8853340
Format, cleanup/lint
kriszyp Apr 24, 2026
ec7d315
More lint
kriszyp Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,66 @@ jobs:
retention-days: 3
if-no-files-found: ignore

run-integration-apiTests-bun:
name: Integration API Tests (Bun)
if: false
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
package-manager-cache: false

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2

- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: harper-build-artifacts-node-24

- name: Setup Harper
env:
DEFAULTS_MODE: 'dev'
HDB_ADMIN_USERNAME: 'admin'
HDB_ADMIN_PASSWORD: 'password'
ROOTPATH: '/tmp/hdb'
OPERATIONSAPI_NETWORK_PORT: 9925
LOGGING_LEVEL: 'debug'
LOGGING_STDSTREAMS: true
THREADS_COUNT: 1
THREADS_DEBUG: false
NODE_HOSTNAME: 'localhost'
run: |
mkdir -p /tmp/hdb/log
node ./dist/bin/harper.js install > /tmp/hdb/log/install-stdout.log 2> /tmp/hdb/log/install-stderr.log
sleep 10
bun ./dist/bin/harper.js start > /tmp/hdb/log/start-stdout.log 2> /tmp/hdb/log/start-stderr.log &
sleep 10

- name: Run API Tests
id: run-api-tests
env:
HDB_ADMIN_USERNAME: 'admin'
HDB_ADMIN_PASSWORD: 'password'
run: npm run test:integration:api-tests

- name: Upload Harper logs
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: harper-integration-api-test-logs-bun
path: /tmp/hdb/log/
retention-days: 3
if-no-files-found: ignore

run-integration-tests:
name: Integration Tests ${{matrix.shard}}/4 (Node.js v${{ matrix.node-version }})
runs-on: ubuntu-latest
Expand Down Expand Up @@ -181,3 +241,120 @@ jobs:
path: /tmp/harper-integration-test-logs/
retention-days: 3
if-no-files-found: ignore

build-windows:
name: Build Harper (Windows, Node.js v22)
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js 22
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
package-manager-cache: false

- name: Install dependencies
run: npm install --ignore-scripts

- name: Build
run: npm run build
continue-on-error: true # we currently have type errors so just ignore that

- name: Upload build artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: harper-build-artifacts-windows
path: |
dist/
static/
node_modules/
package.json
retention-days: 1

run-integration-tests-windows:
name: Integration Tests ${{matrix.shard}}/4 (Windows, Node.js v22)
runs-on: windows-latest
needs: [build-windows]
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js 22
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
package-manager-cache: false

- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: harper-build-artifacts-windows

- name: Install harperdb@4 for legacy tests
run: npm install --ignore-scripts --prefix "${{ runner.temp }}/harperdb-legacy" harperdb@4

- name: Run Integration Test Shard ${{ matrix.shard }}
env:
HARPER_INTEGRATION_TEST_LOG_DIR: ${{ runner.temp }}/harper-integration-test-logs
HARPER_LEGACY_VERSION_PATH: ${{ runner.temp }}/harperdb-legacy/node_modules/harperdb
run: npm run test:integration -- --shard=${{ matrix.shard }}/4

- name: Upload Harper server logs
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: harper-server-logs-windows-shard-${{ matrix.shard }}
path: ${{ runner.temp }}/harper-integration-test-logs/
retention-days: 3
if-no-files-found: ignore

run-integration-tests-bun:
name: Integration Tests ${{matrix.shard}}/4 (Bun)
runs-on: ubuntu-latest
needs: [build]
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
package-manager-cache: false

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2

- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: harper-build-artifacts-node-24

- name: Run Integration Test Shard ${{ matrix.shard }}
env:
HARPER_INTEGRATION_TEST_LOG_DIR: /tmp/harper-integration-test-logs
HARPER_RUNTIME: bun
run: |
npm run test:integration -- --shard=${{ matrix.shard }}/4

- name: Upload Harper server logs
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: harper-server-logs-bun-shard-${{ matrix.shard }}
path: /tmp/harper-integration-test-logs/
retention-days: 3
if-no-files-found: ignore
3 changes: 2 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"threads": "readonly",
"Resource": "readonly",
"headersTest": "readonly",
"logger": "readonly"
"logger": "readonly",
"Bun": "readonly"
},
"ignorePatterns": ["unitTests/security/jsLoader/fixtures"]
}
4 changes: 3 additions & 1 deletion bin/harper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const { packageJson } = require('../utility/packageUtils.js');
const checkNode = require('../launchServiceScripts/utility/checkNodeVersion.js');
const hdbTerms = require('../utility/hdbTerms.ts');
const { SERVICE_ACTIONS_ENUM } = hdbTerms;
process.setSourceMapsEnabled(true); // this is necessary for source maps to work, at least on the main thread.
if (typeof process.setSourceMapsEnabled === 'function') {
process.setSourceMapsEnabled(true); // this is necessary for source maps to work, at least on the main thread.
}

const HELP = `
Usage: harperdb [command]
Expand Down
6 changes: 2 additions & 4 deletions bin/lite.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const { startHTTPThreads, startSocketServer } = require('../server/threads/socketRouter.ts');
const { startHTTPThreads } = require('../server/threads/socketRouter.ts');

startHTTPThreads(0, true);
startSocketServer(9925);
startSocketServer(9926);
startHTTPThreads(1);
21 changes: 2 additions & 19 deletions components/componentLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@
import { server } from '../server/Server.ts';
import { Resources } from '../resources/Resources.ts';
import { table } from '../resources/databases.ts';
import { startSocketServer } from '../server/threads/socketRouter.ts';
import { getHdbBasePath } from '../utility/environment/environmentManager.js';
import * as operationsServer from '../server/operationsServer.ts';
import * as auth from '../security/auth.ts';
import * as mqtt from '../server/mqtt.ts';
import { getConfigObj, getConfigPath } from '../config/configUtils.js';
import { createReuseportFd } from '../server/serverHelpers/Request.ts';
import { ErrorResource } from '../resources/ErrorResource.ts';
import { Scope } from './Scope.ts';
import { ApplicationScope } from './ApplicationScope.ts';
Expand Down Expand Up @@ -82,7 +80,7 @@
});
}

export const TRUSTED_RESOURCE_PLUGINS = {

Check failure on line 83 in components/componentLoader.ts

View workflow job for this annotation

GitHub Actions / Unit Test (Node.js v20)

Exported variable 'TRUSTED_RESOURCE_PLUGINS' has or is using name 'AuthAuditLog' from external module "/home/runner/work/harper/harper/utility/logging/harper_logger" but cannot be named.
REST, // for backwards compatibility with older configs
rest: REST,
graphql: graphqlQueryHandler,
Expand Down Expand Up @@ -112,7 +110,6 @@

const BUILT_INS = Object.keys(TRUSTED_RESOURCE_PLUGINS);

const portsStarted = [];
export const loadedPaths = new Map();
let errorReporter;
export function setErrorReporter(reporter) {
Expand Down Expand Up @@ -453,22 +450,8 @@
...componentConfig,
})) || extensionModule;
if (isRoot && network) {
for (const possiblePort of [port, securePort]) {
try {
if (+possiblePort && !portsStarted.includes(possiblePort)) {
const sessionAffinity = env.get(CONFIG_PARAMS.HTTP_SESSIONAFFINITY);
if (sessionAffinity)
harperLogger.warn('Session affinity is not recommended and may cause memory leaks');
if (sessionAffinity || !createReuseportFd) {
// if there is a TCP port associated with the plugin, we set up the routing on the main thread for it
portsStarted.push(possiblePort);
startSocketServer(possiblePort, sessionAffinity);
}
}
} catch (error) {
console.error('Error listening on socket', possiblePort, error, componentName);
}
}
if (env.get(CONFIG_PARAMS.HTTP_SESSIONAFFINITY))
harperLogger.warn('Session affinity is not supported and will be ignored');
}
}
if (resources.isWorker)
Expand Down
4 changes: 2 additions & 2 deletions components/deriveURLPath.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { join } from 'node:path';
import type { Component } from './Component.js';
import type { ComponentV1 } from './ComponentV1.js';

Expand All @@ -8,6 +7,7 @@ function pathStartsWithBase(base: string, path: string) {
}

export function deriveURLPath(component: Component | ComponentV1, path: string, type: 'file' | 'directory'): string {
path = path.replace(/\\/g, '/'); // converting from potential windows path to URL paths
if (path.startsWith('./')) {
path = path.slice(2); // remove leading './'
}
Expand Down Expand Up @@ -53,5 +53,5 @@ export function deriveURLPath(component: Component | ComponentV1, path: string,
}
}

return join(component.baseURLPath, path);
return component.baseURLPath + path; // note, do NOT use join here, this is not a file system path, this is a URL path
}
2 changes: 1 addition & 1 deletion integrationTests/apiTests/config/envConfig.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function createHeaders(username, password) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
'Content-Type': 'application/json',
'Connection': 'keep-alive',
'Connection': 'close',
};
return headers;
}
Expand Down
82 changes: 82 additions & 0 deletions integrationTests/deploy/deploy-from-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,86 @@ suite('Local application deployment', (ctx: ContextWithHarper) => {
const body = await response.text();
ok(body.includes('<h1>Hello, Harper!</h1>'));
});

test('throughput benchmark: Simple table PUT/GET', async (t) => {
const base = `${ctx.harper.httpURL}/Simple`;
const auth = `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`;
const jsonHeaders = { 'Content-Type': 'application/json', 'Authorization': auth };
const readHeaders = { Authorization: auth };

const CONCURRENCY = 25;
const TOTAL_REQUESTS = 20000;
const READ_WRITE_RATIO = 20; // reads per write

// Pre-populate records so GETs have data to hit
const SEED_COUNT = 200;
await Promise.all(
Array.from({ length: SEED_COUNT }, (_, i) =>
fetch(`${base}/${i}`, {
method: 'PUT',
headers: jsonHeaders,
body: JSON.stringify({ name: `seed-${i}` }),
}).then((r) => r.body?.cancel())
)
);

const latencies: number[] = [];
let errors = 0;
let nextWriteId = SEED_COUNT;

async function sendRequest(index: number): Promise<void> {
const isWrite = index % (READ_WRITE_RATIO + 1) === 0;
const t0 = performance.now();
try {
let res: Response;
if (isWrite) {
const id = nextWriteId++;
res = await fetch(`${base}/${id}`, {
method: 'PUT',
headers: jsonHeaders,
body: JSON.stringify({ name: `bench-${id}` }),
});
} else {
const id = Math.floor(Math.random() * SEED_COUNT);
res = await fetch(`${base}/${id}`, { headers: readHeaders });
}
if (!res.ok) errors++;
await res.body?.cancel();
} catch {
errors++;
}
latencies.push(performance.now() - t0);
}

// Bounded concurrency dispatcher
const startTime = performance.now();
let dispatched = 0;
const inflight = new Set<Promise<void>>();

while (dispatched < TOTAL_REQUESTS || inflight.size > 0) {
while (inflight.size < CONCURRENCY && dispatched < TOTAL_REQUESTS) {
const p: Promise<void> = sendRequest(dispatched++).then(() => {
inflight.delete(p);
});
inflight.add(p);
}
if (inflight.size > 0) await Promise.race(inflight);
}

const totalMs = performance.now() - startTime;

latencies.sort((a, b) => a - b);
const pct = (p: number) => latencies[Math.floor(latencies.length * p)].toFixed(1);
const throughput = ((latencies.length / totalMs) * 1000).toFixed(1);

t.diagnostic(`Benchmark: ${TOTAL_REQUESTS} requests, concurrency=${CONCURRENCY}, R:W=${READ_WRITE_RATIO}:1`);
t.diagnostic(` Throughput : ${throughput} req/s`);
t.diagnostic(` Total time : ${(totalMs / 1000).toFixed(2)}s`);
t.diagnostic(` Errors : ${errors}`);
t.diagnostic(` Latency p50: ${pct(0.5)}ms`);
t.diagnostic(` Latency p95: ${pct(0.95)}ms`);
t.diagnostic(` Latency p99: ${pct(0.99)}ms`);

strictEqual(errors, 0, `Expected 0 errors, got ${errors}`);
});
});
3 changes: 3 additions & 0 deletions integrationTests/deploy/fixture/config.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
static:
files: 'web'
graphqlSchema: # These reads GraphQL schemas to define the schema of database/tables/attributes.
files: '*.graphql' # looks for these files
rest: true
4 changes: 4 additions & 0 deletions integrationTests/deploy/fixture/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type Simple @table @export {
id: Long @primaryKey
name: String
}
Loading
Loading