+</code></pre><p>A crash during a write can lose its acknowledgement after committing. Follow <a href="#recovering-after-an-uncertain-write">uncertain-write reconciliation</a>, even when the error is a Worker failure instead of a storage code. Memory data is lost with its Worker.</p><p>Temporary OPFS owner handover is different: <code>LEADER_CHANGED</code> rejects in-flight operations, but the same <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a> reconnects and restores prepared statements for subsequent work. Reconcile interrupted writes; an interrupted transaction ends with <code>TRANSACTION_LOST</code>. The next section describes this recoverable lifecycle.</p><h3 id="tab-handover-and-subscriptions">Tab handover and subscriptions</h3><p>The browser's locks elect the next owner when the previous Worker closes or dies. New operations wait while that owner opens the database. Prepared statements are restored automatically when needed. Notifications fan out to every <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a>, including the one that wrote the data.</p><p>Operations already sent to a departing owner reject with <code>LEADER_CHANGED</code>. Their effects may already have committed; TinyJoin never silently repeats them. The <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a> reconnects for subsequent operations, so inspect the stored outcome before retrying a write. A callback transaction interrupted by owner loss cannot continue: subsequent transaction operations reject with <code>TRANSACTION_LOST</code>. Start a new transaction after reconciliation.</p><p>After handover, or when a page becomes visible or resumes, subscriptions may receive <code>{revision, tables: [], reset: true}</code>. Re-query on this notification even though the precise changed tables are unknown. Filtered subscriptions also receive it. A subscription is an invalidation signal, not a durable log of every commit.</p><p>Transactions exclude other Clients for the entire callback. Keep callbacks short and do not wait on work that needs another <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a> for the same name. If a client disappears with a transaction open, the owner rolls it back. A frozen but still live owner or transaction holder can delay other tabs until it resumes or closes. TinyJoin does not steal a live storage lock based on a timer, since doing so could let two engines write concurrently.</p><p>For offline reopening of the application itself, use the <a href="/guides/offline/">offline build integration</a>. OPFS stores the data; a service worker caches the application and database runtime files.</p><h3 id="recovering-after-an-uncertain-write">Recovering after an uncertain write</h3><p>A rejected write does not always prove that nothing committed. The following <a href="/api/tinyjoin/classes/errors/clienterror/"><code>ClientError</code></a> codes mean the current engine must no longer be used:</p><div class="table"><table><thead><tr><th>Code</th><th>Meaning</th></tr></thead><tbody><tr><td><code>RECOVERY_REQUIRED</code></td><td>The engine cannot safely continue after a storage publication failure; reopening must establish the stored state.</td></tr><tr><td><code>STORAGE_COMMIT_OUTCOME_UNKNOWN</code></td><td>A storage write or a result after a possible commit could not be confirmed. The requested change may have committed.</td></tr><tr><td><code>STORAGE_ENGINE_POISONED</code></td><td>A previous uncertain or fatal result already made this engine unusable.</td></tr></tbody></table></div><p>Stop accepting database work, keep the original error, and close the <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a>. For OPFS, open the <strong>same database name</strong> with a new <a href="/api/the-essentials/using-a-database/create/"><code>create</code></a>() call and inspect the recovered rows before deciding whether to repeat the operation. If open or inspection fails, keep the application in a recovery state and preserve the stored data; changing the name or deleting the database would hide the state you need to reconcile. A new memory database starts empty and cannot recover the old <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a>'s data.</p><p>Give an application operation a stable identifier before its first attempt and record that identifier in the same transaction as its effects. After reopening, query that record and compare the intended values. A matching record means the operation already happened; a missing record may allow a deliberate retry with the <strong>same identifier</strong>. An unexpected record needs application reconciliation. Generating a fresh identifier on every retry can apply an operation twice. TinyJoin has no automatic replay, so this policy belongs to the application. Recording the identifier with <code>ON CONFLICT DO NOTHING</code> skips an operation that is already recorded, but it does not compare the recorded values with the intended ones.</p><p>The <code>retryable</code> property only says that a later attempt or reopen may succeed. It is not a guarantee that replaying a write is safe, nor that the current <a href="/api/tinyjoin/classes/lifecycle/client/"><code>Client</code></a> remains usable. A rollback attempted after an uncertain commit cannot establish that the commit was absent. See also <a href="/guides/transactions-and-changes/#errors-and-cancellation">transaction error handling</a>.</p></section></article><aside aria-hidden="true"></aside></main><footer><nav><a id="gh" href="https://github.com/tinyplex/tinyjoin" target="_blank" rel="noreferrer">GitHub</a></nav><nav><a href="/">TinyJoin</a> © 2026 James Pearce. MIT License.</nav></footer><script>window.dataLayer=window.dataLayer||[];function g(){dataLayer.push(arguments);}g('js',new Date());g('config','G-40B96SPQX2');</script></body></html>
0 commit comments