Skip to content

Store: declared non-overlapping intervals and retry-safe host transactions (#902) - #925

Merged
jimhoyd merged 1 commit into
mainfrom
claude/store-intervals-and-txn-retries
Sep 29, 2026
Merged

jimhoyd merged 1 commit into
mainfrom
claude/store-intervals-and-txn-retries

Conversation

@jimhoyd

@jimhoyd jimhoyd commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Implements #902 items 1 and 3. Items 2, 4, 5 and 6 stay on #902; STORE.md's "What is not covered" points there.

Item 1: collections.<c>.intervals: {start, end, within?, scope?, when?}

  • Rule: no two constrained records in the same scope, with equal within values, may overlap as half-open [start, end). Intervals that only touch are allowed.
    • Checked inside the write transaction for create, PUT, PATCH and transitions.
    • A clash refuses with 409 interval_conflict and writes nothing: no record, no retry claim, no audit event.
    • A refused move keeps the old slot.
  • when: an equality filter such as {status: booked}, so cancelling frees a slot. Reopening into a taken slot is refused.
  • scope: collection (the default) blocks across owners; owner constrains each owner's records alone.
  • Privacy: conflict.id appears only when the caller may read the conflicting record. Another owner's booking blocks the slot and nothing about it is returned.
  • Bounds:
    • start, end and within properties must be required and not increments.
    • Bounds are both numeric, or both date-time in UTC …Z (offsets are refused).
    • Date-times compare as epoch milliseconds in SQL, checked against JavaScript on 5,000 random values.
  • Index:
    • A partial expression index per declaration makes the check one indexed step; a test asserts the query plan.
    • Activation refuses stored overlaps, naming both ids, and drops stale indexes.
    • reassign and ownerless-assign refuse moves that would overlap under scope: owner.

Item 3: StoreExports.transaction(work, {idempotencyKey, fingerprint?})

  • Migration 3→4 adds store_transaction_results, which stores the key and fingerprint as SHA-256 only.
  • Same fingerprint: returns the stored result without running work.
  • Different fingerprint: 422 idempotency_key_reused.
  • First run: stores the result in the same transaction. The result must be exact-round-trip JSON of at most 16 KiB. The newest 1000 keys are kept.

Measurements (npm run bench:store -- --intervals, M4 Pro, Node 26.10, SQLite 3.53.4)

p50 1k 10k
Check query through the index 1.8 µs 2.4 µs
Same query, index dropped 267 µs 3.2 ms
Previous host-transaction scan 23 µs 197 µs
Accepted create with / without intervals 81 / 65 µs 184 / 180 µs

Evidence

  • intervals.test.ts (13 tests) covers:
    • adjacency, and the same instant written two ways;
    • the UTC and ordering 422s;
    • a refused move keeping its slot, cancel/reopen, privacy on owned collections, and scope: owner;
    • the reassign refusal, and rollback on an injected failure;
    • an in-process race and a worker-thread race;
    • activation refusals, the query plan and stale-index drop;
    • the OpenAPI description.
  • transaction-retries.test.ts (5 tests) covers replay, a mismatched fingerprint, nothing kept on failure, restart, hashing, eviction and races.
  • npm run verify and npm run test:package pass, based on main after Store collections name project schemas; defaults and readOnlyProperties on the collection (#908) #924.
  • Store budget raised to 116 / 438 KiB (measured 114,883 / 445,013 bytes).

Limits

🤖 Generated with Claude Code

…ransactions (#902)

- intervals: {start, end, within?, scope?, when?} on a collection refuses any
  create, PUT, PATCH or transition whose half-open [start, end) overlaps another
  constrained record's in its scope with 409 interval_conflict, inside the
  write transaction, through a partial expression index (one descending index
  step; 2.4 us median at 10k records vs 3.2 ms without the index).
  error.conflict.id only for a record the caller may read, so another owner's
  booking blocks without being named. Bounds are numbers or UTC (Z) date-times
  with at most millisecond precision. Activation refuses stored overlaps and
  drops stale interval indexes; reassign and ownerless-assign refuse moves that
  would overlap under scope: owner.
- StoreExports.transaction(work, {idempotencyKey, fingerprint}) keeps hashed
  key/fingerprint and the JSON result (<= 16 KiB) in the same transaction and
  replays it without running work; different fingerprint is 422
  idempotency_key_reused. Schema version 4 adds store_transaction_results.
- bench:store --intervals, OpenAPI 409/conflict, STORE.md, README, llms,
  CHANGELOG; STORE.md "What is not covered" points at #902 for items 2, 4-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jimhoyd
jimhoyd enabled auto-merge (squash) September 29, 2026 15:46
@jimhoyd
jimhoyd merged commit 4e88193 into main Sep 29, 2026
25 checks passed
@jimhoyd
jimhoyd deleted the claude/store-intervals-and-txn-retries branch September 29, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant