Skip to content

fix: preserve Temporal values in offline transactions#1579

Open
cdemetriou-prod wants to merge 4 commits into
TanStack:mainfrom
cdemetriou-prod:fix/support-temporal-offline-transactions
Open

fix: preserve Temporal values in offline transactions#1579
cdemetriou-prod wants to merge 4 commits into
TanStack:mainfrom
cdemetriou-prod:fix/support-temporal-offline-transactions

Conversation

@cdemetriou-prod

@cdemetriou-prod cdemetriou-prod commented Jun 9, 2026

Copy link
Copy Markdown

🎯 Changes

Preserves TC39 Temporal values when offline transactions are serialized to and restored from the outbox.

Previously, Temporal objects such as Temporal.PlainDate and Temporal.Instant could round-trip as {} because they do not expose enumerable fields. This updates the offline transaction serializer to store Temporal values with explicit type markers and restore them through globalThis.Temporal, covering mutation data and transaction metadata.

Adds serializer tests for Temporal values in mutation modified / original / changes data and transaction metadata.

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Offline transactions now preserve Temporal date/time values when serialized and restored (including transaction metadata).
  • New Features

    • Optional Temporal support can be supplied at runtime; missing support is surfaced as an explicit error so callers can handle it.
  • Tests

    • Added tests covering Temporal serialization/deserialization across mutations and transaction metadata.
  • Chores

    • Added release entry and a development polyfill dependency.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bae4eb86-2c0d-454c-8226-9ec9b1d5e620

📥 Commits

Reviewing files that changed from the base of the PR and between fa8bef9 and 6eb9678.

📒 Files selected for processing (1)
  • packages/offline-transactions/tests/OfflineExecutor.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/offline-transactions/tests/OfflineExecutor.test.ts

📝 Walkthrough

Walkthrough

TransactionSerializer now encodes Temporal-like values as markers and reconstructs them via injected or global Temporal constructors; metadata is included in the round-trip. OutboxManager and OfflineExecutor accept optional temporal constructors and propagate MissingTemporalConstructorError. Types, tests, and a devDependency/changeset were added.

Changes

Temporal value preservation in offline transactions

Layer / File(s) Summary
Temporal detection and serialization handlers
packages/offline-transactions/src/outbox/TransactionSerializer.ts
Implements detection of Temporal-like objects via Symbol.toStringTag, encodes them as { __type: 'Temporal', type, value } using toString(), and reconstructs them with constructor .from(value). Adds MissingTemporalConstructorError.
Transaction metadata serialization
packages/offline-transactions/src/outbox/TransactionSerializer.ts
serialize() and deserialize() now include transaction.metadata processed through serializeValue/deserializeValue, enabling Temporal values in metadata to round-trip.
OutboxManager and OfflineExecutor wiring & error handling
packages/offline-transactions/src/outbox/OutboxManager.ts, packages/offline-transactions/src/OfflineExecutor.ts
OutboxManager constructor accepts optional temporal and passes it to TransactionSerializer; OfflineExecutor forwards config.temporal. get/getAll and leader replay now re-throw MissingTemporalConstructorError instead of swallowing it; other deserialization errors remain logged and ignored.
Types and config
packages/offline-transactions/src/types.ts
Adds TemporalConstructorName, TemporalConstructor, TemporalConstructors types and an optional temporal?: TemporalConstructors field to OfflineConfig.
Tests: Temporal handling and helpers
packages/offline-transactions/tests/TransactionSerializer.test.ts, packages/offline-transactions/tests/OfflineExecutor.test.ts
Adds temporal-polyfill import, passes Temporal into TransactionSerializer, adds createTransaction helper, introduces Temporal handling tests for mutation payloads and metadata, and adds a test asserting MissingTemporalConstructorError is thrown when global Temporal is absent.
Release notes and devDependency
.changeset/quiet-temporal-dragons.md, packages/offline-transactions/package.json
Adds a changeset documenting Temporal preservation and adds temporal-polyfill (^0.3.0) to devDependencies for tests.

Sequence Diagram

sequenceDiagram
  participant Client
  participant OutboxManager
  participant TransactionSerializer
  participant TemporalConstructors
  Client->>OutboxManager: request transaction (get/getAll)
  OutboxManager->>TransactionSerializer: deserialize(serializedTransaction)
  TransactionSerializer->>TransactionSerializer: detect Temporal marker in value
  TransactionSerializer->>TemporalConstructors: resolve constructor for `type` (injected or globalThis.Temporal)
  TransactionSerializer->>TemporalConstructors: call constructor.from(value)
  TemporalConstructors->>TransactionSerializer: return Temporal instance
  TransactionSerializer->>OutboxManager: return reconstructed OfflineTransaction
  OutboxManager->>Client: return transaction / re-throw MissingTemporalConstructorError
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • samwillis

Poem

🐰 Temporal threads stitched through storage and time,
I sniffed out markers and stitched them back fine.
From serialize's burrow to deserialize's light,
Offline transactions now keep moments tight—
A rabbit applauds: time returns just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: preserve Temporal values in offline transactions' clearly summarizes the main change: adding support for preserving TC39 Temporal values during offline transaction serialization and deserialization.
Description check ✅ Passed The PR description covers the main changes with clear motivation, includes relevant test additions, confirms local testing via 'pnpm test', and indicates a changeset has been generated for release tracking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/offline-transactions/src/outbox/TransactionSerializer.ts`:
- Around line 50-65: TransactionSerializer currently reaches for
globalThis.Temporal via getTemporalConstructor (used by deserializeValue),
causing silent failures in OutboxManager.get; refactor to make the Temporal
dependency explicit by adding an injectable Temporal namespace or a mapping of
TemporalConstructorName->TemporalConstructor to TransactionSerializer (e.g., a
constructor parameter on TransactionSerializer) and update
getTemporalConstructor to use that injected object instead of
globalThis.Temporal; also update OutboxManager.get to either construct
TransactionSerializer with the injected Temporal (propagate the dependency from
wherever OutboxManager is created) or, if no Temporal is supplied, throw a clear
runtime error (fail fast) rather than swallowing the deserialization error and
returning null so persisted outbox items remain readable.

In `@packages/offline-transactions/tests/TransactionSerializer.test.ts`:
- Around line 323-345: The test currently seeds changes.temporalValues but only
asserts deserialized.mutations[0].modified and .original; add assertions that
inspect deserialized.changes.temporalValues (the same path used when creating
the transaction) and verify each entry's Symbol.toStringTag and toString() match
the original temporalValues entries; locate the test around
serializer.serialize/serializer.deserialize and the transaction object creation
and add checks analogous to the modified/original assertions but against
deserialized.changes.temporalValues to prevent regressions in the changes
branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60e28cff-5dc9-4353-a259-c675f299e6c9

📥 Commits

Reviewing files that changed from the base of the PR and between 4d1abde and 993bb12.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .changeset/quiet-temporal-dragons.md
  • packages/offline-transactions/package.json
  • packages/offline-transactions/src/outbox/TransactionSerializer.ts
  • packages/offline-transactions/tests/TransactionSerializer.test.ts

Comment thread packages/offline-transactions/src/outbox/TransactionSerializer.ts Outdated
Comment thread packages/offline-transactions/tests/TransactionSerializer.test.ts
@cdemetriou-prod cdemetriou-prod marked this pull request as draft June 9, 2026 21:27
@cdemetriou-prod

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/offline-transactions/tests/TransactionSerializer.test.ts (1)

281-371: ⚡ Quick win

Add a negative-path test for missing Temporal constructors

Current Temporal tests only verify successful round-trips. Please add one assertion that deserializing a Temporal marker without injected constructors throws MissingTemporalConstructorError, so this runtime contract doesn’t regress.

➕ Minimal test addition
-import { TransactionSerializer } from '../src/outbox/TransactionSerializer'
+import {
+  MissingTemporalConstructorError,
+  TransactionSerializer,
+} from '../src/outbox/TransactionSerializer'
@@
   describe(`Temporal handling`, () => {
+    it(`should throw when Temporal constructors are unavailable`, () => {
+      const serializerWithTemporal = createSerializer()
+      const serializerWithoutTemporal = new TransactionSerializer({
+        'test-collection': mockCollection as any,
+      })
+
+      const transaction = createTransaction({
+        modified: {
+          id: `1`,
+          dueDate: Temporal.PlainDate.from(`2024-01-15`),
+        },
+      })
+
+      const serialized = serializerWithTemporal.serialize(transaction)
+      expect(() => serializerWithoutTemporal.deserialize(serialized)).toThrow(
+        MissingTemporalConstructorError,
+      )
+    })
+
     it(`should restore Temporal values in mutation data`, () => {

As per coding guidelines, "Always add unit tests that reproduce a bug before fixing it to ensure the bug is fixed and prevent regression" and "Test corner cases including ... undefined vs null values ...".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/offline-transactions/tests/TransactionSerializer.test.ts` around
lines 281 - 371, Add a negative-path test that ensures deserializing Temporal
markers without injected constructors throws MissingTemporalConstructorError:
create a serializer (via createSerializer) that does not have Temporal
constructors injected, produce or reuse a serialized transaction containing
Temporal markers (e.g., from serialize or a minimal prepared payload), and
assert that calling serializer.deserialize(...) throws
MissingTemporalConstructorError; reference serializer.deserialize,
createSerializer, and MissingTemporalConstructorError in the test so it fails
when the runtime contract is not met.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/offline-transactions/src/OfflineExecutor.ts`:
- Around line 283-287: When replaying persisted outbox entries in
loadAndReplayTransactions() the broad catch currently swallows
MissingTemporalConstructorError thrown by OutboxManager.getAll(), so update
loadAndReplayTransactions() to explicitly catch MissingTemporalConstructorError
(the error class by name) and rethrow it (or propagate it) instead of treating
it as a recoverable warning; keep the existing generic catch for other errors
but ensure MissingTemporalConstructorError is not swallowed so startup can fail
fast when temporals are missing.

---

Nitpick comments:
In `@packages/offline-transactions/tests/TransactionSerializer.test.ts`:
- Around line 281-371: Add a negative-path test that ensures deserializing
Temporal markers without injected constructors throws
MissingTemporalConstructorError: create a serializer (via createSerializer) that
does not have Temporal constructors injected, produce or reuse a serialized
transaction containing Temporal markers (e.g., from serialize or a minimal
prepared payload), and assert that calling serializer.deserialize(...) throws
MissingTemporalConstructorError; reference serializer.deserialize,
createSerializer, and MissingTemporalConstructorError in the test so it fails
when the runtime contract is not met.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddb558b6-5105-4d68-8fab-0711c3364227

📥 Commits

Reviewing files that changed from the base of the PR and between 993bb12 and ecbe679.

📒 Files selected for processing (5)
  • packages/offline-transactions/src/OfflineExecutor.ts
  • packages/offline-transactions/src/outbox/OutboxManager.ts
  • packages/offline-transactions/src/outbox/TransactionSerializer.ts
  • packages/offline-transactions/src/types.ts
  • packages/offline-transactions/tests/TransactionSerializer.test.ts

Comment thread packages/offline-transactions/src/OfflineExecutor.ts
@cdemetriou-prod

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/offline-transactions/tests/OfflineExecutor.test.ts`:
- Around line 108-135: The test currently calls executor.dispose() only on the
success path, risking leaked resources if expect(...waitForInit()) rejects; move
disposal into the finally block so the executor is always disposed. After
creating the executor via startOfflineExecutor(...) and before restoring
Temporal (originalTemporal/globalWithTemporal), ensure executor.dispose() is
invoked in the finally branch (guarding for executor being defined) so
startOfflineExecutor, executor.dispose, waitForInit, and
MissingTemporalConstructorError are handled safely and no listeners leak between
tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 408f1c0f-8b48-46bc-bb16-d31e71c99e97

📥 Commits

Reviewing files that changed from the base of the PR and between ecbe679 and fa8bef9.

📒 Files selected for processing (2)
  • packages/offline-transactions/src/OfflineExecutor.ts
  • packages/offline-transactions/tests/OfflineExecutor.test.ts

Comment thread packages/offline-transactions/tests/OfflineExecutor.test.ts
@cdemetriou-prod

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cdemetriou-prod cdemetriou-prod marked this pull request as ready for review June 9, 2026 22:28
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