winche_storage 5.1.1
winche_storage: ^5.1.1 copied to clipboard
Dart SDK for the WincheStorage backend - a reference-based API with resumable, multipart-aware upload and download tasks for any storage backend.
CHANGELOG #
5.1.1 #
Fixed: a facade registered while signed in was unbound for that turn #
onSessionChanged opened with an unconditional await _teardown(). A freshly
registered facade has nothing to tear down, but the await suspends anyway — an
async body runs synchronously only up to its first one, and awaiting an
already-completed future still costs a microtask. So _bind was never reached
before control returned to the caller, and WincheStorage.instance handed back
an unbound facade even when an identity had been signed in for minutes.
Every session-bound call made in that turn threw WincheUnboundException — and
because storage is usually first obtained from whatever widget needs it, that
turn is typically a build, where the throw tears down the widget tree rather
than reaching an error branch. It was also a permanent failure in practice: a
provider or notifier that caches its creation error never retries, so the state
did not recover on the next frame when the bind landed.
The teardown is now skipped when nothing is bound, so the first bind completes
synchronously — which is what the config setter already documented ("a session
core bound synchronously during WincheStorage.instance"), and what
winche_database has always done by guarding the same await in its _bind.
Registration order still does not matter, and a real user switch is unaffected: anything bound is still fully torn down before the incoming session is built.
Fixed: transferEvents threw instead of returning a stream #
It went through _require(), so reading it while unbound threw rather than
returning a stream — at a call site that is typically inside build(), which is
exactly the shape core's WincheUnboundException documentation says a
stream-returning member must not take. It also marked the facade as used,
locking config, for what is only an observation.
It is now a plain getter over the facade-owned controller, matching
winche_database's treatment of connectionStates and syncEvents. The
controller outlives every session, so a listener attached while nobody is signed
in keeps working and starts receiving events once an identity arrives — there is
nothing to re-attach on a user switch.
child() and transferEvents are now the two members safe to call at any time.
5.1.0 #
Added: ChildReference.cachedFiles() #
Future<List<CachedFile>> — the cached files directly under a path, sorted by
path, each verified against the disk so every localPath opens.
5.0.0 removed offlineChildren() on the grounds that a server listing
annotated with isCached covers the multi-file case. It does, but only while
the server is reachable: listChildren() throws when offline, so at the moment
the cache matters most there was no way to ask what was in it. This is that
read. It is not a listing — it reports what this device holds, makes no claim
about what exists on the server, and never calls listDirectory.
One level only, like a listing. Rows whose bytes are absent or incomplete are omitted rather than returned in a degraded form.
for (final f in await dir.cachedFiles()) {
print('${f.path} → ${f.localPath}');
}
Fixed: cache operations on a CachedFile read from the cache #
The reference carried by a CachedFile that was read from the cache —
cachedFile(), cachedFiles(), or keepCached() when the bytes were already
complete — was built without the catalog. So clearCache(), refreshCache(),
keepCached(), cachedFile() and checkForUpdate() all threw StateError on
an object obtained from the cache, and delete() through one deleted the file
on the server and then silently skipped its local cleanup, leaving the bytes
and the catalog row behind as an orphan that still reported as cached. A
CachedFile returned by a download carries the reference you called it on and
was never affected.
It now carries the catalog, and the live-task registry with it: a transfer
started through such a reference is aborted on sign-out, appears on
transferEvents, and is findable via downloadFor / uploadFor.
uploadPath(..., cache: true) through one now works too, where it previously
threw StateError.
It still carries no upload queue, so resumeUpload() throws on it and
delete() will not cancel a queued upload for that path — use
storage.child(path) when either matters.
Behaviour change: delete() through a cache-read reference now evicts the
local copy, where before it left the bytes behind. If you were relying on that,
copy the bytes out before deleting — every route to delete() now cleans up.
5.0.0 #
Built on winche_core. Storage is now bound to whichever identity is signed
in, rather than being handed a token, a namespace and a directory by the app.
Breaking: this release discards every existing local store. The on-disk
layout moved from <dir>/winche_storage_<namespace>/ to
<root>/winche/<storageKey>/storage/, and no migration is performed. On first
launch under 5.0 every user starts from an empty cache and any queued upload
that had not yet reached the server is lost — silently, with nothing in the UI
to notice it. That is worse here than in a cache-only package: a queued upload
exists nowhere else. Drain the queue before upgrading if that matters — check
pendingUploads() and wait for it to empty while 4.x is still installed.
Requires winche_core ^0.2.0 #
Also raises the Dart SDK floor from ^3.0.0 to ^3.10.0 to match core.
Breaking: the offline layer is now a file cache #
The package cached file content but presented it as though it cached the
storage index: getSnapshot/listChildren (server) sat beside
offlineSnapshot/offlineChildren (cache), same return types, distinguished by
a fromCache boolean -- implying the same question answered from two places. It
wasn't. The catalog only ever held files someone explicitly cached, so the
"cache read" was a different question wearing matching clothes.
The scope is now explicit: this package caches bytes, not the index. Listings
and metadata are always live. For offline-capable structured data, use
winche_database.
| removed | replacement |
|---|---|
offlineSnapshot() |
cachedFile() -> CachedFile? |
offlineChildren() |
none -- listings are annotated instead |
makeAvailableOffline() |
keepCached() -> CachedFile |
refreshOfflineCopy() |
refreshCache() |
removeOfflineCopy() |
clearCache() |
offlineCopyStatus() |
checkForUpdate() |
OfflineCopyStatus |
CacheStatus |
WincheStorage.clearOfflineCache() |
WincheStorage.clearCache() |
pendingTransfers({kind}) |
pendingUploads() |
resumeTransfers() |
resumeUploads() |
ChildReference.resumeTransfer() |
resumeUpload() |
FileSnapshot.fromCache / DirectorySnapshot.fromCache |
removed |
FileData.isCached / FileData.localPath |
moved to FileSnapshot |
-
Listings carry cache state. Every
FileSnapshotfromlistChildren()andgetSnapshot()now hasisCachedandlocalPath. This replaces the documented recipe of calling bothlistChildren()andofflineChildren()and hand-building aSetof paths to cross-reference. -
cachedFile()returns null, not a "missing" snapshot. "I don't have these bytes" and "this file doesn't exist" used to look identical. A returnedCachedFileis verified against the disk, so itslocalPathalways opens. -
keepCached()is file-only and idempotent. It no longer caches a whole directory when the path has no file record -- that made one call fetch either one file or hundreds depending on a server round-trip the caller couldn't see, and it was the cache layer's only use of the storage index. It also returns the existing copy instead of silently re-downloading; userefreshCache()to force. -
FileDatais a pure wire model. Everything on it came from the server.
Breaking: downloads are no longer durable #
download(saveTo, enqueue: true) is gone; enqueue leaves download()
entirely. An upload is the only copy of something -- lose the queue and the work
is gone. A download is a cache fill whose bytes stay authoritative on the server,
so losing one costs bandwidth and never data. The durable queue is now an upload
outbox.
Range resume is not lost: the resume offset comes from the file on disk, not
from the record, so keepCached() picks up a partial left by an app exit.
In-session retry for downloads is unchanged.
transferEventsno longer reports durable download progress -- because downloads are no longer durable. The replacement isdownloadFor(path)plus the task'sstateStream, not a renamed API.transferEventsnow covers more than before: one-shot transfers used to emit nothing at all, since only the queue emitted. Every transfer is now visible.downloadFor(path)is in-session only.uploadFor(path)still survives a restart.TransferRecord.kindis gone (records are all uploads).TransferKindremains, describing events.- Download rows written by an earlier version are purged on first launch. Left in place they would deserialize under the upload-only record shape into an upload whose source is the download destination -- uploading a partially fetched file over the server's copy.
Fixed #
-
A cache fill interrupted by process death could never complete. The
downloading->readytransition lived in an awaiting stack frame, and download records carried nopinnedflag, so after a restart the bytes landed but the row stayeddownloadingforever -- the file occupying disk, invisible, and re-downloaded on every attempt. Removing durable downloads eliminates the cross-process gap, andcachedFile()now verifies bytes rather than trusting the row, so any row already stuck self-corrects. -
A resumed download could silently corrupt the file. Bytes were appended to whatever partial was on disk with no check that the server's content was unchanged, producing an old-prefix/new-suffix splice that passes a length check. Now guarded by comparing
contentHashbefore resuming, withIf-Rangeas a second layer. The guard is exact rather than best-effort: a download only starts whenuploadStatus == complete, which implies acontentHash. -
keepCached()reported server conditions asStateError. A file not being on the server is an ordinary runtime condition, not a bug to fix. It is nowStorageNotFoundException, and a record whose bytes have not been uploaded yet reportsStorageFailedPreconditionException-- distinguishing "still uploading, try later" from "the upload failed, it must be re-uploaded" -- instead of falling through to an obscure signed-URL error. -
CacheStatus.unknownmeant two unrelated things: "the server is unreachable" and "there is no fingerprint to compare". The second is nowremoteIncomplete, which is what the server actually reports viauploadStatus.notPinnedleaves the enum: it iscachedFile() == null, answerable locally without a round-trip.
Changed #
-
Breaking:
WincheStorageis aWincheStorageService. Construct the app instead, and reach storage throughWincheStorage.instance(orinstanceFor(app)).winche_corebuilds a session for whichever identity is signed in and disposes it on sign-out. -
Breaking: four fields leave
WincheStorageConfig. Each was a way to get it wrong; all four now come from core, where they cannot disagree with the rest of the stack:4.x 5.0 uriWincheOptions.storageEndpointtokenProvidersession.token()namespaceResolveridentity.storageKeydirectoryResolverWincheOptions.directoryResolverWhat remains is what storage tunes for itself:
multipartThreshold,inMemory, and the fourretry*knobs. It is set on the instance (WincheStorage.instance.config = ...) and throws aStateErroronce storage has been used.Stateless mode survives unchanged: no
directoryResolveron the app,inMemoryoff, native. Durable and offline operations still throwStateError, now naming both knobs that would enable them. -
Breaking:
close()is deleted. Teardown is core's job — a sign-out tears the session down, anddispose()releases the service. The order and the guarantees are unchanged (one-shot transfers, then the queue, then the store, never waiting on the network).isClosedis deleted with it.A user switch no longer needs sequencing by the app. Core awaits storage's teardown before dispatching the next session, so the outgoing identity's store is always fully closed before the incoming one opens.
-
Breaking:
resumeDownloads()is deleted, along with durable downloads themselves (see above).resumeUploads()is the single manual nudge, and its purpose has changed: sign-in and token rotation now re-drive the queue automatically, so it is only for what the SDK cannot observe — the OS reporting the network is back, or the app returning to the foreground. -
Breaking:
WincheStorage.withStore(api, store)is replaced bydebugBindStore(api, store), a@visibleForTestingmethod on the instance. -
Breaking:
WincheStorageExceptionextendsWincheException, core's root for the whole stack.status,message,detailsandstatusCodeare unchanged; only the supertype is new.on WincheStorageExceptionstill asks the narrow question, andon WincheExceptionnow catches anything from any Winche package. -
Breaking:
child()no longer throws while unbound. It returns a reference that resolves its api and store when used, so building one is always safe — including in a widget field or abuildmethod, where a throw tears down the tree instead of reaching an error branch. The operation you attempt is what rejects withWincheUnboundException.A reference is therefore always about whoever is signed in at the moment you use it. One built before sign-in starts working when an identity arrives; one built under a previous user reports unbound after they sign out, instead of quietly reading a torn-down store.
-
WincheSessionExpirednow pauses a transfer instead of retrying it. It previously fell into the "not aWincheStorageException" default and counted an attempt. A transfer that straddles a user switch is about to be aborted by teardown anyway, so spending retry budget on a session that no longer exists only loses work.
Removed #
validateNamespaceand the namespace concept. Core validates the identity, andstorageKeyis a digest, so the check became unreachable by construction — and a second implementation of a rule core owns is one more place to update.
4.0.1 #
Packaging only — the library code is byte-for-byte identical to 4.0.0.
- The published archive no longer contains
docs/superpowers/, the design specs and implementation plans. They are internal working documents that made up roughly half the archive and help nobody consuming the package. They remain in the repository. Archive size drops from 382 KB to 341 KB.
4.0.0 #
Identity-scoped local state, a close that can't race the store, and a transfer
queue that survives an expired token. Ports the fixes from winche_database
5.0.0, which apply here in a more damaging form because storage persists file
bytes alongside its index.
Added #
WincheStorageConfig.namespaceResolver— required for a persistent store; scopes all local state to one identity. Local state is single-tenant: the offline catalog, the cached bytes and the durable transfer queue carry no identity, so a shared store let a second user on the same device read the previous user's pinned files and replay their queued uploads under the new token. Each identity now gets its own directory,<dir>/winche_storage_<namespace>/, holding the sembast index,cache/andstaging/(on the web, the IndexedDB database name). Switching users isawait storage.close()plus a newWincheStorage; the previous user's queued transfers stay on disk and resume when they sign back in. Resolved lazily and cached, likedirectoryResolver— it pins the identity for the lifetime of the instance. Validated against[A-Za-z0-9._-]+rather than sanitised: rewriting a user id would collapse two identities onto one store.WincheStorage.resumeUploads()— re-drives every upload halted by a pause. Call it after refreshing an auth token instead of waiting out theretryPollIntervalbackstop. Thewinche_databasereconnect()analogue.WincheStorage.isClosed,TransferStatus.paused,TransferEventType.paused, andTransferEvent.record.
Fixed #
- An expired token no longer destroys queued work. Every failure was treated
the same — count an attempt, back off, and after
retryMaxAttemptsfail permanently and drop the handle. A 401 therefore burned five attempts in about two minutes and silently discarded an un-synced upload. Failures are now split three ways:unauthenticatedandunavailablepause (record and handle survive, no attempt counted,TransferEventType.pausedemitted, probing continues on the usual backoff so a brief blip recovers in about a second);internal/deadlineExceeded/unknownand non-WincheStorageExceptionerrors retry as before;permissionDenied/notFound/invalidArgument/failedPreconditionare terminal. - A durable transfer started offline is no longer dropped after ~2.5 minutes.
unavailablenow pauses, which makes the 3.0.0 contract — "retries until it succeeds, so it can be started while offline" — actually true. - A terminally failed transfer is recoverable.
TransferEventcarries the droppedTransferRecordinrecord; a path and an error alone lost the sourcelocalPath,mimeTypeandmetadata. close()no longer races the local store.dispose()cancelled only the poll timer: in-flight drive loops and every scheduled retryTimeroutlived it and wrote into a sembast that had already closed — an uncatchableBad state: database is closed. Teardown now runs in dependency order — one-shot transfers, then the controller (timers cancelled, in-flight HTTP aborted,runningrecords reset topending), then the store.LazyStorageLocalStoredegrades to no-ops after close so a straggling callback cannot surface a store error, and itsclose()is idempotent.- In-flight transfers are aborted on close rather than cancelled:
cancel()on an upload deletes the remote file, which closing the SDK should never imply. - A retry backoff of many doublings no longer overflows its shift (reachable now that a paused transfer probes indefinitely).
- A completed transfer is no longer briefly still in the queue. The durable
record was dropped just after the handle completed, so
await task.whenDonefollowed bypendingUploads()could still see the finished transfer, and thecompletedevent could arrive after it. Both now happen in an awaitedonBeforeCompletehook that runs before the handle completes — the same contractcache: trueuploads already had for their pin. - A store that cannot open — an unusable namespace, an uncreatable directory — no longer escapes as an unhandled async error from the constructor's fire-and-forget rehydrate. The failure resurfaces, catchably, on the next store access.
Changed #
- Breaking:
WincheStorage.dispose()is renamedclose(). No deprecated alias. It staysFuture<void>and is now idempotent, and it resolves only once the store is really closed — await it before opening anotherWincheStorageover the same files. Every other member throwsStateErrorafterwards. - Breaking: a persistent
WincheStoragenow requiresnamespaceResolver, and it must be omitted wheninMemory: true. Both areArgumentErrorat construction. Required in stateless native mode too (nodirectoryResolver, not web), where it is unused — one flat rule rather than a matrix. - Breaking: cached files move from
<dir>/<fileId><.ext>to<dir>/winche_storage_<ns>/cache/<fileId><.ext>, staging from<dir>/.staging/to<dir>/winche_storage_<ns>/staging/, and the sembast database from<dir>/winche_storage.dbto<dir>/winche_storage_<ns>/index.db. There is no migration. Pinned files rebuild themselves on next use, but the old cache and database are left on disk unreferenced, and un-synced uploads queued in the legacy store are lost — drain the queue before shipping this upgrade if that matters. - Breaking:
TransferStatusgainedpausedandTransferEventTypegainedpaused— exhaustiveswitches over either need a new arm. staging/is a sibling ofcache/rather than a hidden.staging/beside the cached files, soclearOfflineCache()can empty the cache without disturbing an upload that is still in flight.
3.0.0 #
Robust per-call transfer mechanics, simplified config.
- Breaking — config: removed
enableOfflineCacheandenableAutoResume. The durable transfer queue and offline cache now exist whenever a store is configured — adirectoryResolver(native),inMemory: true, or web (IndexedDB). With none configured on native, the client is stateless and durable/offline operations throwStateErrorat call time (no constructionArgumentError). - Breaking — upload/download API: the
makeAvailableOffline:parameter onuploadPath/uploadBytesis replaced bycache:, anduploadPath/downloadgainenqueue::enqueue: true— durable: the transfer joins the queue, is deduped by path, survives a restart, and retries until it succeeds (so it can start offline).downloadand file-backeduploadPathonly;uploadBytesis not durable.cache: true— stage-first keep-offline (the upload-time pin).- Requesting a flag without its subsystem now throws
StateError(was a silent no-op for the oldmakeAvailableOffline:parameter). download()is a one-shot by default; passenqueue: truefor durable.
- Transfers gained a
queuedstate and a stable-handle model: a tracked transfer is a single handle whosewhenDoneresolves only on the terminal outcome and that survives retries/restart — so you can start an upload while offline and justawaitit. Pause/resume works on tracked handles. updateMetadata()now also refreshes a pinned file's cached metadata after the server write succeeds, so offline reads (offlineSnapshot/offlineChildren) stay current. Only the metadata is synced — the cached content fingerprint is preserved, soofflineCopyStatus()still detects stale cached bytes.makeAvailableOffline()on a directory path now pins every file directly under it (one level — the server lists a single level), instead of throwing a misleading "not found on server". A genuinely missing path still throws.- A pinned (
cache: true) tracked (enqueue: true) upload now finalizes its offline copy beforewhenDoneresolves — the same contract as a direct pinned upload — so a completed upload guarantees the file is cached. (Previously the controller committed the pin just afterwhenDone, so an immediate cache read could miss it.) - Added
WincheStorage.uploadFor(path)/downloadFor(path)to reattach a progress UI to a tracked transfer after a restart. - Breaking — retry config flattened: the
TransferRetryConfigobject is no longer part of the public API. Its knobs are now top-level fields onWincheStorageConfig(andWincheStorage.withStore):retryBaseDelay,retryMaxDelay,retryMaxAttempts,retryPollInterval. - Breaking —
ChildReferencerenames for self-describing names:get()→getSnapshot(),list()→listChildren(),refresh()→refreshOfflineCopy(),evict()→removeOfflineCopy(),resume()→resumeTransfer().makeAvailableOffline()is unchanged. - Offline staleness is now content-aware: the old
isStale()bool is replaced byofflineCopyStatus()returningOfflineCopyStatus(upToDate/contentChanged/remoteDeleted/notPinned/unknown), driven by a new server content fingerprint exposed asFileData.contentHash. - Breaking — reads split into server vs cache.
getSnapshot()andlistChildren()are now server-only (they no longer fall back to the cache or annotate results withisCached/localPath, and throwStorageUnavailableExceptionwhen offline). NewofflineSnapshot()andofflineChildren()read the local cache only (fromCache: true). Compose them for the old remote-first-with-fallback behavior. - Breaking:
ChildReference.list()now returns aDirectorySnapshotinstead ofList<FileSnapshot>. Read the files via.files. The snapshot adds directory-level metadata (fromCache,name,length,isEmpty). list()is now offline-aware: when the server is unreachable it returns the locally pinned files directly under the path withfromCache: true(a partial, pinned-only view) instead of throwing. With offline cache off it still throws.- Upload-time pinning:
uploadPath/uploadBytesacceptmakeAvailableOffline: trueto place the uploaded bytes straight into the offline cache — no download roundtrip. Best-effort: a caching failure leaves the upload successful and records a stale pin for a laterrefresh(). isStale()now returnsfalsewhen the server is unreachable (offline) rather than throwing; other API errors still propagate.delete()now cleans up local state after a successful server delete: it evicts any offline copy (local file + catalog entry) and drops any queued or in-flight transfer for the path, so a deleted file leaves no orphan behind.- Added
WincheStorage.pendingTransfers({TransferKind? kind})— a snapshot of the durable queue (pending/running/failed records), optionally filtered by kind (e.g. uploads only).
2.0.0 #
- Added the opt-in offline cache: pin files for offline use, remote-first reads with a local cache fallback, and on-demand freshness checks.
- Added the opt-in auto-resume layer: a durable transfer queue that survives app restarts and self-retries failed transfers with exponential backoff.
1.1.0 #
- Uploading to an existing path now overwrites a completed file when the size or MIME type differs, and discards an interrupted attempt for different content instead of throwing — a previously failed upload no longer blocks the path.
- Files at or below
multipartThresholdnow upload in a single request via the backend's single-shot upload endpoint; only larger files use multipart. This also fixes empty (0-byte) uploads, which previously failed. - Downloads now verify the written byte count against the remote record size and fail on a truncated transfer, deleting the partial file before reporting.
1.0.0 #
- Initial Release