Skip to content

feat(workspace): add bulkSet/bulkDelete to TableHelper, fix API OAuth cookie handling#1653

Merged
braden-w merged 10 commits intomainfrom
pr/5-bulk-ops-and-api-fixes
Apr 12, 2026
Merged

feat(workspace): add bulkSet/bulkDelete to TableHelper, fix API OAuth cookie handling#1653
braden-w merged 10 commits intomainfrom
pr/5-bulk-ops-and-api-fixes

Conversation

@braden-w
Copy link
Copy Markdown
Member

@braden-w braden-w commented Apr 12, 2026

This is the last PR in the 5-PR stack, and it only makes sense on top of #1652: the earlier workspace work opened the door for batch-friendly table operations, and this pass finishes the path so bulk imports and destructive cleanup stop behaving like a thousand tiny edits. The goal is simple—make batch work cheap and predictable without changing how single-row writes feel day to day.

First, TableHelper finally has bulk write and bulk delete paths that match the way people actually use it. Instead of looping set() or delete() and paying for one observer tick per row, callers can stay inside a single transaction and let observers see one coherent batch. set() is still the right tool for one-off edits; bulkSet() and bulkDelete() are for imports, migrations, and table resets.

await workspace.tables.posts.bulkSet([
  { id: '1', title: 'A', _v: 1 },
  { id: '2', title: 'B', _v: 1 },
]);

await workspace.tables.posts.bulkDelete(['1', '2']);

That performance story is documented too. The new YKeyValueLww architecture notes spell out why the bulk path is cheaper, and the JSDoc updates make the eager-vs-deferred observer behavior explicit so future changes don't have to be rediscovered in the debugger.


Second, the API fixes are the boring kind of necessary. kv.get() now fails loudly when async schema validation shows up where the API only supports sync reads, instead of quietly pretending the value was missing. The auth server now sets the OAuth/session cookies the way a cross-origin hub actually needs them—sameSite: 'none' plus secure: true—and the noisy oauthProvider discovery warnings are silenced because those endpoints are already mounted by hand. generateGuid was inlined so the API package stops depending on @epicenter/workspace for a single utility.

10 commits, 13 files, +586/-93. Last in the stack, and it stacks on #1652.

braden-w added 10 commits April 12, 2026 17:07
New methods at three levels:
- YKeyValueLww: bulkSet (skips deleteEntryByKey, observer batches conflicts)
  and bulkDelete (one-pass scan, right-to-left batch delete)
- EncryptedYKeyValueLww: delegates to inner with encryption on bulkSet
- TableHelper: async chunked API with onProgress callback and event loop
  yielding between chunks (default chunk size: 1000)

bulkSet is O(n) for batch updates vs O(n²) when calling set() in a loop.
bulkDelete is O(n) for batch deletes vs O(n²) when calling delete() in a loop.

Includes tests for both YKV-level and TableHelper-level operations.
Mark all waves complete with actual implementation details and deviations.
Key deviation: set() is unchanged — O(n²) fix delivered via new bulkSet()
method instead of modifying existing set() behavior.
Add detailed JSDoc to set(), bulkSet(), delete(), and bulkDelete()
explaining why set/delete eagerly scan while bulk methods defer to the
observer. Update spec with the design rationale and performance analysis.
…allback

Async schema validation in kv.get() returned defaultValue, masking a
programmer misconfiguration. Matches create-table.ts and schema-union.ts
which both throw TypeError for async schemas—the correct behavior for
direct API calls vs. reactive observers.
…Delete JSDoc

- set() diagram now shows both deleteEntryByKey + push in same transaction,
  single observer fire, no DEDUP_ORIGIN needed
- bulkSet() diagram shows deferred cleanup with 2nd observer fire (skipped)
- bulkDelete() JSDoc explicitly notes it does NOT use DEDUP_ORIGIN and why
  (direct deletions, no deferred work, single observer fire)
Module-level JSDoc now explains the full single-vs-bulk design: why set()
eagerly deletes but bulkSet() defers, why bulkDelete doesn't need
DEDUP_ORIGIN, and how the observer's conflict resolution serves both
bulkSet and multi-device sync.

DEDUP_ORIGIN JSDoc expanded with: when it fires, what triggers conflicts,
which methods DO and DON'T need it, and why the re-entrant call is a no-op.
The plugin warns on every request because basePath is /auth (not /),
so it can't auto-mount /.well-known endpoints at the root. Both
discovery endpoints are already mounted manually in app.ts.
…ndency

The Cloudflare Worker bundle can't resolve @epicenter/workspace (Yjs
dependency tree). Inlined the nanoid-based ID generator directly with
the same spec (15-char alphanumeric, same alphabet).
Client apps on different origins (localhost, Tauri, subdomains) hit
state_mismatch during Google OAuth because SameSite=lax cookies set
via cross-origin POST weren't persisting through the redirect flow.

Sets SameSite=none + Secure for cross-origin support, and enables
crossSubDomainCookies for *.epicenter.so production apps.
Base automatically changed from pr/4-kv-improvements to main April 12, 2026 08:57
@braden-w braden-w merged commit f5bda98 into main Apr 12, 2026
1 of 9 checks passed
@braden-w braden-w deleted the pr/5-bulk-ops-and-api-fixes branch April 12, 2026 08:57
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