Changelog¶
All notable changes to this project are recorded in this file. The format follows Keep a Changelog, and version numbers follow the compatibility policy.
Unreleased¶
Breaking¶
Added¶
Changed¶
Deprecated¶
Removed¶
Fixed¶
[0.1.0] - 2026-10-12¶
The first release.
Breaking¶
- The library is a supabase-kt plugin. Install it with
install(Offline) { … }increateSupabaseClientand reach it assupabase.offline, wherecreateSupabaseOfflineClient(…)returned a client; the settings ofSupabaseOfflineConfigare the plugin'sOffline.Config(offlineStoragePathisdataDirectory, aString). The old names are gone; see Removed. supabase-kt closes the plugin with the client, once it has begun closing its HTTP client: a call made in between, or one whose request had not gone out, is still sent (Ktor can send while its client finishes closing), or ends as it would offline (a read the cache can answer is answered from it, a write the mutation queue holds isQueued) or with the closed error, never with a cancellation; a request already on the wire still answers. Callsupabase.offline.close()first to let the calls not yet sent go out. - A flow being collected when the client closes (
selectAsFlow,pendingMutations,pendingMutationCountFlow,rejectedMutations) completes, where it failed with the client's closed error, so a screen torn down at logout sees its flow end, not an error. A flow collected afterclose()still fails with the closed error, as every other call does. - Logging goes through supabase-kt's own: the plugin logs to the client's logging processor
(
defaultLoggingFactory) with the tagSupabase-Offline, atOffline.Config.logLevelor the client'sdefaultLogLevel.OfflineLogger,OfflineLogLeveland theloggerandminLogLevelsettings are gone: configure logging once, increateSupabaseClient. MutationOutcome.Queuedis a class that carries the queued write as aPendingMutation: match it withis MutationOutcome.Queued, notMutationOutcome.Queued ->.MutationOutcome.AppliedandQueuedhave internal constructors.OfflinePostgrestQueryBuilderis a final class, as supabase-kt's builder is, andOfflineandOfflineBucketApineed theOfflineInternalopt-in to implement. A test double comes fromsupabase-offline-kt-testing.RefreshState.InFlightisRefreshState.Refreshing, andRefreshState.Pausedis new: an exhaustivewhenoverRefreshStateneeds a branch for it.ReadStrategy.NetworkFirst,CacheFirstandCacheOnlytakemaxAge, notcacheDuration, and are no longer data classes.FlowReadStrategyandMutationStrategyare sealed interfaces: their cases are written as before, butentries,values()andvalueOfare gone.- A
CacheOnlyread that finds nothing it may serve throwsCacheMissException, notNoSuchElementException. AQueueIfOfflinewrite the queue cannot hold (a dry run, or one whose request could not be inspected to tell; a filter it cannot record; an unknown user; another schema; a write on its way into the queue while local data was cleared) that cannot be sent for want of a connection throwsMutationNotQueueableException, with that failure as its cause, instead of the failure itself or anIOException.isNetworkOrConnectivityFailure()is stilltruefor it. - The testing hook's opt-in marker
InternalOfflineTestingApiisOfflineInternal. syncPendingMutations(table: String? = null)returns aSyncReport(what it sent, what the server refused, what is still queued) in place of a count, and replacessyncAllPendingMutations(). It no longer throws a failure that stopped the pass: the report says what is still queued andsyncStatus.value.lastSyncErrorwhy. When the library is sending the same writes already, it waits for that and then sends what is left.pendingMutationCountandpendingMutationCountFloweach take onetable: String? = nullin place of two overloads: calls compile as before, compiled callers must be rebuilt. They, and the new queue and rejected-list members, show the current user's writes only, and none while the stored session cannot be read (they showed every user's then).selectAsFlowemits aQuerySnapshotin place ofOfflineReadResult, withisFromCache,fetchedAtandhasPendingMutationsbesideresultandrefresh; it is no longer constructed by an app.autoSyncis on by default and does what it says: the library sends queued writes by itself when the session settles into a user with writes queued, and when the network comes back. SetautoSync = falseto leave those passes to the app; a write queued behind an earlier one is sent by itself either way.- On Android the library's manifest declares
android.permission.ACCESS_NETWORK_STATE, for its connectivity monitor: it appears in the merged manifest of every app that uses the library. - A test builds its client as an app does, with
install(Offline) { inMemory(clock = …) }fromsupabase-offline-kt-testing;buildInMemorySupabaseOfflineClientis gone (see Removed). The client no longer runs in the test's own scope: its background work runs on the library's, and a failure there is logged, never thrown into the test. ReadStrategy.NetworkFirst,CacheFirstandCacheOnlytake aDurationonly: the constructors taking milliseconds are gone, andNetworkFirst(null)now compiles.- The Storage file helpers extend
OfflineBucketApi, the bucketoffline.storage.from(…)returns, in place of supabase-kt'sBucketApi: on AndroidensureLocalFilereturns aFileandensureLocalUriaUri, on iOSensureLocalPathaString, none of them nullable any more, and each takespathas the bucket's own members do.localUriIfCachedandlocalPathIfCachedarenullonly when nothing is cached. On supabase-kt's own bucket they no longer compile, where they returnednull. - A read, an update or a delete with two top-level filters on one column, such as
filter { gte("age", 1); lte("age", 10) }, throwsIllegalArgumentExceptionbefore anything is sent or queued. supabase-kt sends only the first of them, so the read showed rows outside the window and the write reached them, and a queued write replayed that way later. Write the two as oneand { }group, which sends both. - A read, an update or a delete whose pattern list (
likeAll,likeAny,ilikeAll,ilikeAny) is empty, or holds a pattern that is empty,NULL, edged with white space or holding a brace (and none of,.:()), throwsIllegalArgumentExceptionbefore anything is sent or queued, at the top level or in a group. supabase-kt sends such a pattern unquoted, so the server read another list:likeAll("code", listOf(""))went out aslike(all).{}, which matches every row. - On iOS the database and the file cache live in
Library/Application Supportby default (supabase_offline.dbandsupabase_offline/), not inDocuments, which an app with file sharing on shows in the Files app. A client that finds a database inDocumentsmoves it, with its unsent writes and its cached files, when it opens; while another client holds it, it stays there and that client's claim fails the open, and when both places hold a database the one inDocumentsis left and a warning logged. - The iOS framework carries no Swift-only helpers and no SKIE-generated Swift (see Removed). The Swift package reaches it through a bridge of its own.
- On Android, an interface member with a body compiles to a JVM default method only, so the
$DefaultImplsclasses are gone (-jvm-default=no-compatibility). SyncStatus.lastSyncErroris aSyncFailedException?, which is all it ever held: read itshttpStatusandcodewithout a cast. The testing artifact'ssyncStatus(lastSyncError = …)takes one.syncPendingMutations(table)no longer sends a table's writes ahead of an earlier write of the same user to another table: it stops at the first write with an earlier one still queued elsewhere, andSyncReport.heldBehindOtherTablesCountcounts what it held back. Sync every table to send them in order.- On Swift, the requirement of
OfflineFilterValueisofflineFilterText, where it wasfilterText, a name that showed in completion onString,Int,Date,UUIDand every other standard type that conforms, and could collide with one an app declares. A type of your own that conforms declaresvar offlineFilterText: String.
Added¶
- Built and tested against supabase-kt 3.6 (
supabase-bomin the version catalog), with Ktor 3.4.3 and kotlinx.coroutines 1.10.2, the versions it was released with. Not 3.8: supabase-kt 3.7.0 and 3.8.0 carry a Realtime crash and a protocol-V2 leave defect, fixed upstream but not yet released, and this library's pin sets an app's Realtime too; it moves to the first release with both fixes. On 3.6 there is nodryRun(),maybeSingle(),isdistinctfilter orpurgeCache(write a dry run by addingPrefer: tx=rollbackto the request), and a Storage object name with%,?or#is downloaded as supabase-kt downloads it but never cached, since supabase-kt 3.6 puts it in the URL unencoded and can fetch a different object (see the compatibility page). Each minor release of this library names exactly one supabase-kt minor: the artifact compiles supabase-kt's inline functions into itself and implements itsBucketApi, which gains members in minor releases, so a range cannot be promised. When an app resolves another supabase-kt minor, the plugin logs one warning when it is installed, naming both versions, and carries on; a patch difference is no warning. - The
Offlineplugin for supabase-kt:install(Offline) { … }andsupabase.offline, with its settings inOffline.Config: the two cache sizes,dataDirectory,primaryKeys(each table's key,idunless set;primaryKey(table, columns)),responseTrust(ResponseTrust, with the earlier rule asAuthenticatedSessionOnly),logLevel,networkAvailability, the app's own connectivity signal in place of the library's monitor, andautoSync. On Android the plugin needs noContext: androidx.startup gives it the application's. The configuration is read once, when the client is created. - Offline-first PostgREST reads through supabase-kt.
supabase.offline.from(table).select { }takes supabase-kt's own request builder and aReadStrategy(NetworkFirst,CacheFirst,CacheOnlyorNetworkOnly).selectAsFlowemits aQuerySnapshoteach time the cache or the network answers, under aFlowReadStrategy. OfflinePostgrestQueryBuilderhas supabase-kt's verbs and overloads under its names (insert,upsertandupdateof JSON, of a value and of a list, theupdate { set(…) }DSL,delete) and itstable,schemaandpostgrest; the offline choice is a named parameter before the trailing lambda. Most calls written against supabase-kt compile with only the receiver changed andio.github.nesfeder.supabase.offline.offlineimported. These need an edit: a write whose result is read (resultOrNull()); a suspending call or an earlyreturninside a request lambda, since the verbs are notinline(make the call before the verb); a request lambda passed inside the parentheses (name itrequest = { … }or put it after them); the typedrpc(importio.github.nesfeder.supabase.offline.rpcin place of supabase-kt's); and realtime'sselectAsFlowand bucket management, which stay onsupabase.fromandsupabase.storage. Typed writes encode throughpostgrest.serializer, as supabase-kt's do.MutationOutcome.Queued.pendingMutation(PendingMutation: id, table,MutationOperationINSERT,UPSERT,UPDATEorDELETE, createdAt, row keys, body) andMutationOutcome.isQueued.rpcon the plugin, network only, with a typed overload;offline.storage.from(bucket)andoffline.storage[bucket]; a typedapplyServerRecord, whoseprimaryKeysdefault to the table's configured key.applyServerRecordandapplyServerDeletetake aschema, for a table of a schema other than the client's default.OfflineException, open for new subclasses, withCacheMissException,MutationNotQueueableExceptionandNetworkDisabledException;httpStatusOrNull(), which reads supabase-kt's and Ktor's errors, Storage's included.- The opt-in markers
OfflineExperimentalandOfflineInternal, both at error level. API under either is left out of the API records and the compatibility checks: experimental API may change in a minor release with no changelog line. - Writes that work offline.
insert,update,upsertanddelete, and their JSON forms, return aMutationOutcome:Appliedwith the server's response, orQueuedwhen the write was recorded in the mutation queue to be sent later (MutationStrategy.QueueIfOffline, the default). Queued writes show in cached reads until they are sent. - The mutation queue, observed and steered:
pendingMutations(table?)(a flow ofPendingMutation),pendingMutationCount(table?)andpendingMutationCountFlow(table?), anddiscardPendingMutation(id), the way out of a write the server keeps failing (it waits for a send in flight, and what waited behind the write is then sent).syncPendingMutations(table?)replays the current user's queued writes in the order they were made and returns aSyncReport(syncedCount,rejected,remainingCount, andalreadyOnServerCountfor an insert whose earlier send had in fact arrived); it returns promptly when offline. - Refused writes:
rejectedMutations(), a flow of the current user's persisted rejected list (RejectedMutation, with the server'sServerErrorandisRetryable), withdismissRejectedMutation(id)andretryRejectedMutation(id); one user can never see, dismiss or retry another's. A write sent at once that the server refuses still throws the server's error to its caller: the list is for refusals found on replay. syncStatus, aStateFlow<SyncStatus>:network(NetworkStatus:UNKNOWN,ONLINE,OFFLINE,DISABLED), the current user's pending and rejected counts,isSyncing,lastSyncedAt, andlastSyncError, aSyncFailedExceptionthat carries the status and code but no row, filter value or token. The network is current as soon as the switch below returns; the two counts follow the queue shortly after each change, read together. It never throws: atclose()it takes one last value, withisSyncingfalse, and changes no more.disableNetwork()andenableNetwork(): while access is off, nothing is sent, reads are answered from the cache, writes that may queue are queued, reactive reads arePaused, and requests that must reach the server throwNetworkDisabledException, whichisNetworkOrConnectivityFailure()counts as offline. A new client starts with access on.- A connectivity monitor, on by default: Android's
ConnectivityManager(a validated network with internet) and the iOS path monitor. It is a hint, never a gate: it decides when the library tries by itself and whatsyncStatus.networksays, and a request the app makes is tried whatever it says. It starts with the client and stops withclose(). RefreshState.Paused: a refresh that cannot reach a network known to be down waits for it, instead of reading asStalled, and runs again when the network comes back.- Provenance on one-shot reads:
PostgrestResult.isFromCache,fetchedAtandhasPendingMutations, asQuerySnapshothas them, carried in response headers PostgREST never sends; a result from supabase-kt itself has none of them set. - Storage downloads cached on the device.
storage.from(bucket)returns anOfflineBucketApiwithcachedFileandensureCachedFile, and Android and iOS functions return the cached file's location. applyServerRecordandapplyServerDeleteapply a Realtime change to the cached reads.- Size limits for the cached rows and the cached files, in
Offline.Config. clearLocalData()deletes the cached rows, the queued mutations, which are never sent, and the cached files;close()releases the plugin's database.buildInMemorySupabaseOfflineClientin thesupabase-offline-kt-testingartifact: a client that keeps everything in memory, for tests.- A record of the public API of both artifacts, checked on every change, so the API cannot change without a reviewer seeing it.
supabase-offline-kt-testing:Offline.Config.inMemory(clock)keeps the plugin's cache, mutation queue and Storage file cache in memory, writing nothing to disk, with every other setting applied as in an app; the clock is the time the client sees. It watches no connectivity monitor, so its network status isUNKNOWNuntil a test setsnetworkAvailabilityor callsdisableNetwork(), which is how a test takes it offline. Factories build the value types whose constructors are internal, for an app's own test doubles:appliedOutcome,queuedOutcome,cachedFile,pendingMutation,stalledRefresh,querySnapshot,syncReport,syncStatus,rejectedMutation,serverError, and the exceptionscacheMissException,mutationNotQueueableException,networkDisabledExceptionandsyncFailedException.- Published on Maven Central as
io.github.nesfeder.supabase-offline:supabase-offline-ktandio.github.nesfeder.supabase-offline:supabase-offline-kt-testing, for Android,iosArm64andiosSimulatorArm64, each with its sources, an HTML API reference and a signature on every file. - The Swift package
SupabaseOffline(andSupabaseOfflineTestingfor tests), published by each release tohttps://github.com/nesfeder/supabase-offline-swiftunder the same version. It needs iOS 16 and Xcode 26 or later, and runs on arm64 simulators only: a simulator build that includes x86_64 (such as Any iOS Simulator Device) fails to link, so build for a specific simulator, or withARCHS=arm64. Its framework is an asset of this repository's GitHub Release, and carries a privacy manifest, as theSupabaseOfflinetarget does, declaring the file-timestamp and user-defaults APIs it references. It is not code-signed in 0.1.0. SyncReport.heldBehindOtherTablesCount, and the testing artifact'ssyncReport(heldBehindOtherTablesCount = …), defaulting to0.- The Swift package's API reference, built with DocC and published with the documentation site
under
/swift/, with articles on getting started, coming from supabase-swift, offline behavior, handling errors and testing an app; every code sample in it is compiled with the package's tests. The package's public interface is recorded inswift/api/, and a change to it is rated as a change to the Kotlin API is.
Changed¶
- Every public
suspendfunction declares@Throws(Throwable::class), so a framework that exports the library hands Swift a failure as an error instead of ending the process.close(), which overrides supabase-kt's, and thesyncStatusproperty cannot declare it. - A reactive read's refresh that fails for a reason other than the network is logged at WARNING, by type, once for each kind of failure in a row; it was retried silently.
- A reactive read's refresh that waits for the session to settle (an answer it may not cache yet)
reports
RefreshState.Refreshing, neverStalledwith an exception of the library's own. - Expected offline conditions on the read side log at INFO: a
CacheOnlyread with nothing cached, a coldNetworkFirstread offline, a read answered from the cache because the network failed, and a download offline. A download the server refuses logs at WARNING. - A
NetworkFirstThenCacheread fetches again when its cached rows may no longer be the server's: after a write the cache could not follow, a change of the cache's owner, orclearLocalData(). - okio is no longer an
apidependency of the library: no public declaration names one of its types. It still reaches an app, at the same version, through supabase-kt's Auth. - The Android artifact depends on
androidx.startup:startup-runtimeitself, at the version supabase-kt's Auth already brings, instead of reaching it through Auth. - A locally written row shows only in the cached lists whose filters it matches, leaves a list it
stops matching, and joins one it starts matching when another cached list read with the same
columns holds it.
eq,neq,inandisare evaluated with Postgres's null rules; a condition the library cannot evaluate admits the row, and a pending update or delete filtered by one changes nothing shown until it is sent. A queued row is still appended past a list's order and limit. - An upsert is shown merged by its conflict columns, or by the table's key (
idunless configured otherwise), and every row of a multi-row upsert shows once. - Without the Auth plugin, with the app's own token provider, or signed out, an empty answer is cached and reactive reads settle on it. A response received while a stored session restores or refreshes is never cached.
- With the Auth plugin, the cached rows and cached files belong to one identity at a time: when the session settles into another, they are dropped. Queued mutations are kept for their owners.
single(),maybeSingle()(from supabase-kt 3.8),head, counts (a list with a count included),csv(),geojson(),stripNulls()andexplain()are never cached: they go to the network, and underCacheOnlythrowCacheMissExceptionwithout sending anything.- A table in another schema is cached apart. A write to it with the default strategy is sent at
once and never queued; offline it throws
MutationNotQueueableException. - Typed writes (
insert(value),upsert(values),update(value)) encode through the app's own PostgREST serializer, so a customJson, contextual serializers andencodeDefaultsreach the server, and the mutation queue, as supabase-kt sends them; they used the library's ownJson. - Offline, a
NetworkFirstread with a warm cache answers at once instead of after supabase-kt's retries;NetworkFirstThenCachewith a warm cache shows it asStalled(Pausedwhen the network is known to be down) and keeps fetching instead of failing; aCacheFirstThenNetworkrefresh reportsStalledafter about 3.6 s, orPausedat once when the network is known to be down. - After a sync pass stopped on a server's
Retry-After, the library's own next pass waits at least that long (at most an hour); a sync the app asks for is sent at once. - The testing artifact pins the library to its own version with a strict constraint in its published metadata. Beside a newer library, an older testing artifact fails dependency resolution instead of a test failing with a missing method; a newer testing artifact raises the library in the test classpath to its own version without a word. Keep both on one version.
- A slow collector of a
CacheFirstThenNetworkread is handed the latest snapshot, not each one in turn, and never holds back the query's other collectors. - The request lambda passed to a read or a write may run more than once, so it must configure the same request every time.
- The on-device database is schema version 1, the one this release ships: each queued write keeps its owner, its replay order, its schema-cache refusals and whether a send of it may have reached the server in its own row, and each cached query the replay order it has caught up with.
- Replayed writes are kept for the overlay until every cached query of their table has been fetched again since, or until they fall beyond the newest 1,000 of the table, whatever the device clock says; a table nothing caches keeps none.
- A write PostgREST refuses because its schema cache does not know the table or a column yet
(
PGRST205,PGRST204), as happens just after a migration, stays queued, online as well as offline, and its user's later writes wait behind it. It is rejected only at the tenth such refusal, each counted at least 30 seconds after the last while the app runs (the first after it starts counts at once), so a table that does not exist, or a misspelled column, cannot hold the queue for ever. close()lets a replay pass finish the request it has on the wire, and record the answer, before it stops, so the next client does not send the write again. A request the server does not answer holdsclose()for up to its 10-second bound.- The library's internal code is renamed and grouped into packages under
io.github.nesfeder.supabase.offline.internal. No public declaration, stored name or stored format changed. The exported Room schema records one field's new Kotlin name,cacheKey; its column is stillsignature, and the schema's identity hash is unchanged. - The engine's internal store is the set of components the client builds itself. No public declaration, stored name or stored format changed: the exported Room schema and its identity hash are unchanged.
syncStatusfollows the queue from the moment the client opens, so its counts include what an earlier run left queued before the app first reads it; read for the first time afterclose(), it holds the last value it had.- A reconnection, or the session settling, ends at once the wait a failed pass left before the
library's next pass, and starts it again from one second; a server's
Retry-Afteris still waited out. While the monitor says offline, the library makes one failed attempt by itself per offline period and grows no wait. A wrong monitor costs nothing while requests reach the server; after one fails for want of a connection, queued writes wait for the monitor, the session or the app's sync. WithautoSyncon, a client without Auth sends what an earlier run left queued when it opens. An app's connectivity signal that ends while saying offline counts as unknown, as one that fails does. - Keeping the rows
clearLocalData()deleted out of the database's files is best effort, and never holds the clear: it waits at most a second for an open read holding the write-ahead log, then empties the log in the background within a few seconds of the read ending, and once more atclose(). It waited for up to about a minute, holding every send, and then only logged a warning. - Being offline is logged at INFO at most: a write the queue cannot hold that fails for want of a
connection was logged at ERROR, and a replay pass stopped by the network at WARNING. A write that
asked for a rollback (
Prefer: tx=rollback, what a dry run sends) and was committed, as hosted projects do, is logged once per client at WARNING. close()never throws: a failure closing the database is logged, and the database is released.
Removed¶
- The ways in before the plugin, and the names the plugin's API replaced, with nothing deprecated
left behind:
createSupabaseOfflineClient(Android and iOS) andSupabaseOfflineConfig(install the plugin and usesupabase.offline; it opens the same database, on iOS once it has moved fromDocumentstoLibrary/Application Support, so the cache and unsent writes carry over); theSupabaseOfflineClienttype (Offline);insertJson,upsertJsonandupdateJson(insert,upsertandupdate);insertWith,upsertWithandupdateWith, and the reifiedinsert,upsertandupdateextensions (the typed members, which encode throughpostgrest.serializer);storage(bucket)(storage.from(bucket));clearAllCache()(clearLocalData());supabase(supabaseClient);OfflineLoggerandOfflineLogLevel, with the iOS factory'sloggerandminLogLevel(supabase-kt's logging, atlogLevel);syncAllPendingMutations()andsyncPendingMutations(table): Int(syncPendingMutations(table?): SyncReport); the two-overloadpendingMutationCount()/pendingMutationCount(table)andpendingMutationCountFlow()/pendingMutationCountFlow(table)(one member each,table: String? = null); andOfflineReadResult(QuerySnapshot). buildInMemorySupabaseOfflineClient(install(Offline) { inMemory() }), and the millisecond constructors ofReadStrategy.NetworkFirst,CacheFirstandCacheOnly(passmaxAgeas aDuration).- What the iOS framework carried for the old Swift layer:
SupabaseFactoryand itsofflineClientKt,SwiftJson, theencodeToJsonString,encodeToJsonElement,encodeListToJsonStringandencodeListToJsonArrayhelpers,singleElementArray, the fifteen…Ktquery and mutation helpers, the threeapplySwiftOptions,ensureLocalFileUrlandlocalFileUrlIfCached(ensureLocalPathandlocalPathIfCachedstay), and SKIE with the Swift it generated. Nothing has consumed the framework, so no published user is affected; the Swift package will be built on a bridge module of its own. NumericDeltaColumn, and thedeltaColumnsparameter of the update verbs. A queued write replays exactly what was sent; a value that several devices change concurrently, such as a counter or a running total, is computed on the server from the facts clients write.
Fixed¶
- A write is a dry run when any of its
Prefervalues carriestx=rollback, not only the first: supabase-kt writes its ownPreferfirst and appends one the request adds, so a write that addedPrefer: tx=rollbackitself was queued offline and would have been replayed, and committed, as a real write. - Rows a write returns (
select()) are cached with the columns the caller wrote, matched by key; an update that asks for rows back changes only the rows returned. - A write the cache cannot follow marks the cached lists it may reach stale, and they are fetched again, instead of showing values the server no longer holds.
- A cached list a live reader shows is never evicted, for up to 500 live readers.
- A patched list keeps the time it was fetched, and a list made only of local writes is stale.
- A queued write is sent with its owner's session token or not at all: a user signing in while another's writes replay no longer has them sent as theirs.
- A write that may queue, made when the session's token has expired and cannot be refreshed
(offline, or refused by the Auth server), is queued instead of throwing
TokenExpiredException, and replays as never sent once the session is good again. - A replay refused because its stored filter can no longer be read says so, instead of saying the write has no filter.
- The in-memory client of the testing artifact is safe across threads: a live read while another
thread writes no longer fails with
ConcurrentModificationException. - Each emission of a live read is one view of the cache, the queue and its refresh. An insert the
server applied no longer shows twice for an emission (a list keyed by id crashed on it), a
replayed insert no longer drops out of a list opened right after the sync, and a
CacheFirstThenNetworkread no longer reportsIdleone emission before the rows its refresh fetched. - A channel download Storage answers with an error (
ensureCachedFile, the offline bucket'sdownloadAuthenticatedanddownloadPublicto a channel, and supabase-kt's…Tohelpers through it) throws what the bytes download throws. It cached the error body as the file and served it from then on. A redirect from https to plain http is refused too, even where Ktor'sallowHttpsDowngradelets the bytes download follow it. - A live read fetches again after
clearLocalData(), as after a change of owner, instead of showing the emptied cache until something else fetched; a refresh request made while a read's refresh ran is no longer lost. - A live read fetches again after a change of owner whose last step, deleting the file cache's folders, failed: it showed the emptied cache until something else fetched, and the error logged said the cache had not been dropped.
- A read made while the stored session restores waits for it at most 10 seconds instead of waiting
on a session store that hangs. Past that the cache is not served and the read goes out on the anon
key: a one-shot read returns that answer uncached, which under row-level security can hold fewer
rows, or none, and a live read shows none of the cached rows, only the queued writes it admits
(
Refreshing), until the session settles. - A read the cache cannot answer, made while the connectivity monitor says the network is down, is sent once instead of after supabase-kt's retries.
- One failed read of the database no longer turns automatic behavior off for the client's life, or fails a write the server answered: a later sign-in sends the user's queued writes, the sync status follows the queue again, and a write the server applied (or refused), or a discard that happened, is reported as it happened rather than as the failed read.
- A sync that
clearLocalData()cut short reports the writes it had sent before. - A write made as a client without Auth opens, with
autoSyncon, is sent at once and returnsApplied. The pass that client starts for writes an earlier run left could find the new write in the queue and send it itself, so the write returnedQueuedwith the network up and nothing ahead of it; it was still sent once, and never twice. - While a stored session restores, reads and writes read it once between them, not once each.
- A Storage call naming an object whose name holds a
%that does not begin a percent-escape (100%.png) throwsIllegalArgumentExceptionbefore anything is sent. supabase-kt 3.6 puts the name in the URL as written, the HTTP client cannot build that URL, and the failure read as a connectivity failure, so an app took itself to be offline. - A Storage "not found" answered while the session restores, or after its refresh failed, keeps the user's cached copy and is answered from it. The download went out on the anon key, and Storage answers an object that key may not read as "not found", so the copy was dropped and an app opening a private file at launch lost it until the next download.
- A client that opens with
dataDirectorynewly set, and moves the database from the default location, moves the downloaded files with it; one that cannot move is deleted, to be downloaded again. They stayed in the default directory, whereclearLocalData()no longer reached them and, on Android, Auto Backup still included them. SyncStatus.lastSyncErrorfor a pass stopped on the device, by neither the server nor the network, says so in a fixed sentence instead of naming the failure's class. On iOS a pass stopped by a request header the HTTP client refuses now says so, and what to check, instead of that general sentence.