Skip to content

Commit 990a770

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/schemadefined-on-replicated-tables-rebase
# Conflicts: # resources/databases.ts # unitTests/resources/databases.test.js
2 parents d81fa85 + f847836 commit 990a770

142 files changed

Lines changed: 19419 additions & 19455 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/claude-issue-to-pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ concurrency:
2222

2323
jobs:
2424
work:
25-
uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@6463b3da6326f0ca4646fb6b5139c2ecf92130f6 # main 2026-05-06 (post #11/#12/#14)
25+
uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@441d2a310cb360adf419d3546dd27566164f1ec2 # main 2026-06-09 (post #58: Opus for :bug/:test, efficiency guidance)
2626
with:
2727
# Same SHA as the `uses:` ref above. See the comment in
2828
# claude-mention.yml for why the duplication is unavoidable.
29-
ai-review-prompts-ref: 6463b3da6326f0ca4646fb6b5139c2ecf92130f6
29+
ai-review-prompts-ref: 441d2a310cb360adf419d3546dd27566164f1ec2
3030
repo-specific-conventions: |
3131
## Harper core notes
3232

.github/workflows/claude-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
# Note: the `claude-review` label name is matched there too —
3434
# `_claude-review.yml`'s authorize `if:`, not in this caller.
3535
if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' }}
36-
uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@2be0f702b114e39b90eed90962922ef64df58482 # main 2026-06-03 (post #52 — week-of-05-25 calibration: Harper-aware best-practices wiring, skills-ref v1.7.0, surface-findings rule, access-control blocker guidance, public-skill dedup)
36+
uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@3f2300a39910a0983eb74967dedeebd8c3c98adc # main 2026-06-08 (post #57 — week-of-06-01 calibration: severity-discipline non-blocker bullets, async-commit-ordering check, non-critical-work-on-critical-path guard)
3737
with:
3838
# Same SHA as the `uses:` ref above. The reusable uses this to
3939
# check out HarperFast/ai-review-prompts (layer files + bash
@@ -44,7 +44,7 @@ jobs:
4444
# introspect their own ref (`github.workflow_ref` resolves to the
4545
# CALLER's ref in `workflow_call` context), and `uses: …@<ref>`
4646
# is parsed literally so we can't interpolate a variable.
47-
ai-review-prompts-ref: 2be0f702b114e39b90eed90962922ef64df58482
47+
ai-review-prompts-ref: 3f2300a39910a0983eb74967dedeebd8c3c98adc
4848
review-layers: |
4949
universal
5050
harper/common

.github/workflows/gemini-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
# Note: the `gemini-review` label name is matched there too —
5353
# `_gemini-review.yml`'s authorize `if:`, not in this caller.
5454
if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' }}
55-
uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@2be0f702b114e39b90eed90962922ef64df58482 # main 2026-06-03 (post #52 — calibration: Harper-aware wiring for the Gemini leg, skills-ref v1.7.0, surface-findings rule, access-control blocker guidance, public-skill dedup)
55+
uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@3f2300a39910a0983eb74967dedeebd8c3c98adc # main 2026-06-08 (post #57week-of-06-01 calibration: severity-discipline non-blocker bullets, async-commit-ordering check, non-critical-work-on-critical-path guard)
5656
# Caller-side permissions at the calling-job level (NOT workflow-
5757
# level — that placement caps the reusable's per-job grants below
5858
# what they need and breaks the workflow at startup; see
@@ -70,7 +70,7 @@ jobs:
7070
# duplication is unavoidable: reusable workflows can't introspect
7171
# their own ref (`github.workflow_ref` resolves to the CALLER's
7272
# ref in workflow_call context), and `uses: …@<ref>` is literal.
73-
ai-review-prompts-ref: 2be0f702b114e39b90eed90962922ef64df58482
73+
ai-review-prompts-ref: 3f2300a39910a0983eb74967dedeebd8c3c98adc
7474
review-layers: |
7575
universal
7676
harper/common

.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

DESIGN.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ Records stored in tables are plain objects given a `RecordObject` prototype, whi
1313
- To give a plain JS object the RecordObject prototype without copying it (preserving mutability), use `Object.setPrototypeOf(obj, primaryStore.encoder.structPrototype)` then `entryMap.set(obj, entry)`.
1414
- Do **not** copy the object (e.g. via `Object.assign` into a new instance) if any code still holds a reference to the original and expects to mutate it — see below.
1515

16+
## Struct mode is gated to primary DBIs (downgrade compatibility)
17+
18+
`RecordEncoder` extends a structon encoder, whose random-access "struct" encoding uses header bytes in `0x20–0x3f`. That range overlaps msgpack positive-fixints, so a reader without struct support (msgpackr v1, i.e. harperdb v4) decodes a struct header as an integer. harperdb v4 only enabled struct mode (its `randomAccessStructure` option) on **primary** DBIs, so non-primary DBIs — notably the `__dbis__` metadata store (table/attribute defs) — were plain records mode. If v5 writes `__dbis__` in struct mode, a v5→v4 downgrade can't decode the metadata, silently treats the instance as a fresh pre-3.0 install, and refuses to boot.
19+
20+
To match v4: `OpenDBIObject` sets `randomAccessStructure = isPrimary`, and `RecordEncoder`, when `randomAccessStructure` is false, makes the struct **write** hook bail (`this._writeStruct = () => 0`) so objects are written in records mode. Two subtleties:
21+
22+
- We **bail** the hook (return 0) rather than clear it (`undefined`). Clearing it shifts msgpackr's positive-fixint boundary from `0x20` to `0x40`, so top-level integers `0x20–0x3f` (e.g. a `NEXT_TABLE_ID` ≥ 32 stored in `__dbis__`) would be written as bare fixints — which the still-installed struct **read** hook misreads as struct headers. Bailing keeps the boundary at `0x20`, so those integers are written as `uint8`.
23+
- The struct **read** hook is left intact, so records a prior v5 already wrote in struct mode still decode (read-compat); only new writes switch to records mode.
24+
- Companion change in `structon`: `prepareStructures` saves the shared structures in the legacy plain-array form when there are no typed structs (instead of the `{named, typed}` Map), so the `Symbol.for('structures')` buffer for a records-mode `__dbis__` is also v4-decodable.
25+
1626
## getFromSource() timing: promise resolves before commit runs
1727

1828
In `getFromSource()` (`Table.ts`), the promise that callers await resolves with the entry **before** the `dbTxn.addWrite` commit callback runs. The commit callback mutates `updatedRecord` in-place to set fields like `createdAt` and `updatedAt`. Since the resolved entry's `.value` is the same reference as `updatedRecord`, those mutations are visible to the caller after resolution.

components/Scope.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,14 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
271271
if (key[0] === 'files' || key[0] === 'urlPath') {
272272
// TODO: validate options
273273

274-
// If not entry handler exists then likely the config did not have `files` initially
275-
// Now, it does, so create a default entry handler.
274+
// If no entry handler exists yet, the plugin's handleApplication has not called
275+
// handleEntry() yet — or it hasn't been called at all for this scope. Either way,
276+
// when handleEntry() is eventually called it will read the current config via
277+
// getFilesOption() and create the handler with the correct, up-to-date values.
278+
// Eagerly creating an entry handler here would start chokidar's initial scan
279+
// before the plugin's callback is attached, causing the initial `add` events to
280+
// be missed (RE-8).
276281
if (!scope.#entryHandler) {
277-
scope.#entryHandler = scope.#createEntryHandler(config as FileAndURLPathConfig);
278282
return;
279283
}
280284

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

0 commit comments

Comments
 (0)