Skip to content

Add HttpTabularProxyStorage for remote table operations#534

Merged
sroussey merged 8 commits into
mainfrom
claude/funny-sagan-FXYYk
May 24, 2026
Merged

Add HttpTabularProxyStorage for remote table operations#534
sroussey merged 8 commits into
mainfrom
claude/funny-sagan-FXYYk

Conversation

@sroussey
Copy link
Copy Markdown
Collaborator

Summary

Adds HttpTabularProxyStorage, a new storage adapter that forwards all ITabularStorage operations as HTTP POST requests to a remote server. This enables tabular storage to work across process boundaries (web, Electron, etc.) without requiring direct database access.

Key Changes

  • New HttpTabularProxyStorage class (packages/storage/src/tabular/HttpTabularProxyStorage.ts):

    • Extends BaseTabularStorage and implements the full ITabularStorage interface
    • Forwards all operations (put, get, delete, query, pagination, etc.) via an injected fetch implementation
    • Uses POST {basePath}/{table}/{op} URL pattern for all requests
    • Supports custom base path (defaults to /api/storage)
    • Implements polling-based change subscriptions via PollingSubscriptionManager
    • Uses fingerprinting to correctly handle compound primary keys in change detection (avoids naive string join collisions)
    • Lifecycle methods (setupDatabase, destroy) are no-ops for a proxy
  • Comprehensive test suite (packages/test/src/test/storage-tabular/HttpTabularProxyStorage.test.ts):

    • 617 lines of tests covering all storage operations
    • Fake server implementation that routes requests to an in-memory backing store
    • Tests for put/get/delete, bulk operations, query/getAll, pagination, change subscriptions
    • Regression test for compound key collision in polling (ensures both rows with colliding naive keys are reported)
    • Generic contract tests via runGenericTabularStorageTests with multiple schemas
  • Export in common.ts: Added export * from "./tabular/HttpTabularProxyStorage" to make the new class available from @workglow/storage

Notable Implementation Details

  • Fetch abstraction: The HttpTabularProxyFetch type matches DOM fetch signature, allowing real browser fetch, Hono app.fetch, or Electron IPC shims to work without adaptation
  • Error handling: Non-2xx responses throw with the server's { error } message propagated
  • Polling subscriptions: Uses fingerprinting (via makeFingerprint) on primary keys to correctly identify rows across polling cycles, preventing collisions when naive string joins would fail
  • Empty bulk optimization: getBulk([]) returns immediately without calling the server
  • Change detection: Tracks entity state via fingerprinted keys and emits INSERT/UPDATE/DELETE events based on diffs

https://claude.ai/code/session_01NRd4zupMz88R5LiyTEZW9H

claude added 5 commits May 24, 2026 19:14
…hods (Tasks 2-5)

put, putBulk, get, getBulk, delete, query, getAll, count, size,
deleteAll, deleteSearch — each with in-process fake-server tests.
getOffsetPage and pagination still stubbed (Task 6); subscribeToChanges
still stubbed (Task 8). All 15 new tests pass.

https://claude.ai/code/session_01NRd4zupMz88R5LiyTEZW9H
Implements getOffsetPage, getPage, and queryPage on the proxy so
pagination requests are forwarded to the server in one shot rather
than round-tripping twice through query(). Extends makeFakeServer
with the three corresponding op handlers and adds pagination tests.

Also adds local validatePageRequest() call before forwarding getPage
and queryPage so StorageValidationError is thrown client-side for
invalid orderBy columns/directions rather than being wrapped in a
generic Error from the server.

https://claude.ai/code/session_01NRd4zupMz88R5LiyTEZW9H
Comment thread packages/storage/src/tabular/HttpTabularProxyStorage.ts Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new HttpTabularProxyStorage implementation in @workglow/storage that forwards ITabularStorage operations over HTTP (POST {basePath}/{table}/{op}), plus a dedicated test suite validating behavior against an in-memory backing store.

Changes:

  • Introduce HttpTabularProxyStorage (HTTP-forwarding adapter) including polling-based subscribeToChanges.
  • Add a comprehensive HttpTabularProxyStorage test suite and run existing generic tabular-storage contracts against the proxy.
  • Export the new storage from @workglow/storage via packages/storage/src/common.ts.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
packages/storage/src/tabular/HttpTabularProxyStorage.ts Implements the HTTP proxy tabular storage (CRUD/query/paging/queryIndex + polling subscriptions).
packages/test/src/test/storage-tabular/HttpTabularProxyStorage.test.ts Adds tests and generic contract coverage for the new HTTP proxy storage.
packages/storage/src/common.ts Re-exports HttpTabularProxyStorage from the package public surface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +99 to +116
protected async call<T>(op: string, body: unknown): Promise<T> {
const res = await this.fetchImpl(this.url(op), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body ?? {}),
});
if (!res.ok) {
let message: string;
try {
const data = (await res.json()) as { error?: string };
message = data?.error ?? `HTTP ${res.status}`;
} catch {
message = `HTTP ${res.status}`;
}
throw new Error(`HttpTabularProxyStorage(${this.table}/${op}): ${message}`);
}
return (await res.json()) as T;
}
Comment on lines +139 to +140
await this.call<{ ok: true }>("delete", { key });
this.events.emit("delete", key as keyof Entity);
Comment on lines +215 to +223
override async queryIndex<K extends keyof Entity & string>(
criteria: SearchCriteria<Entity>,
options: CoveringIndexQueryOptions<Entity, K>
): Promise<Pick<Entity, K>[]> {
const { entities } = await this.call<{ entities: Pick<Entity, K>[] }>("queryIndex", {
criteria,
options,
});
return entities;
Comment on lines +45 to +53
readonly indexes?: readonly (
| keyof Schema["properties"]
| readonly (keyof Schema["properties"])[]
)[];
/** Optional base path. Defaults to `/api/storage`. Must NOT have a trailing slash. */
readonly basePath?: string;
/** Forwarded to BaseTabularStorage. Defaults to "if-missing". */
readonly clientProvidedKeys?: ClientProvidedKeysOption;
}
// row1 -> name="x|y", type="z" -> identifier "x|y|z" (name + "|" + type)
// row2 -> name="x", type="y|z" -> identifier "x|y|z" (same string! that's the bug)
// So we assert by checking both rows' `option` values are seen:
const options = inserts.map((_, i) => i); // placeholder — we check via a second listener
Comment on lines +508 to +513
// Both distinct rows must be reported — if one shadowed the other only one INSERT fires.
expect(inserts).toContain("x|y|z");
expect(inserts).toContain("x|y|z");
// More specific assertion: both composite identifiers present
expect(inserts.some((s) => s === "x|y|z")).toBe(true);
// The two rows have DIFFERENT composite keys under fingerprinting:
} as const;
const TestPrimaryKey = ["id"] as const;

function makeFakeServer<Schema extends typeof TestSchema, PK extends typeof TestPrimaryKey>(
The full `bun run build` (@workglow/test build-types via tsgo) failed
where the storage-package-only type build passed:
- makeFakeServer was generic over <Schema, PK>, causing excessively-deep
  type instantiation (TS2589) when matched against the proxy's generics.
  Retyped its param to AnyTabularStorage — the permissive supertype — so
  it isn't re-instantiated per caller.
- Removed leftover placeholder code in the compound-key collision test
  (unused 'options' var → TS6133) and the redundant first listener; the
  test now asserts both rows via their unique 'option' values directly.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 24, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 62.11% 24561 / 39540
🔵 Statements 61.97% 25414 / 41006
🔵 Functions 63.03% 4656 / 7386
🔵 Branches 50.73% 12024 / 23698
File CoverageNo changed files found.
Generated in workflow #2433 for commit e58a8fd by the Vitest Coverage Report Action

sroussey and others added 2 commits May 24, 2026 22:35
…dling in HttpTabularProxyStorage

Introduces a new utility function to trim trailing slashes from the base path in HttpTabularProxyStorage. Updates error handling in the fake server to provide a more user-friendly error message for internal exceptions. Adds tests to verify the new functionality, including the correct handling of trailing slashes and error messages.
- regex-free trailing-slash strip on basePath (clears CodeQL polynomial-regex alert) + doc fix
- document JSON-safe schema constraint (no Uint8Array/bigint round-trip)
- normalize emitted key in delete() to the primary key (parity with other backends)
- add validateSelect/validateQueryParams to queryIndex() for fail-fast parity
@sroussey sroussey merged commit b96cfd9 into main May 24, 2026
13 checks passed
@sroussey sroussey deleted the claude/funny-sagan-FXYYk branch May 24, 2026 22:54
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.

4 participants