Skip to content

Releases: koolbase/koolbase-flutter

v11.3.0

Choose a tag to compare

@github-actions github-actions released this 24 Aug 11:12
KoolbaseError: one canonical error type across the whole SDK

Catch anything, call KoolbaseError.from(e), branch on code. The 44 typed
exceptions are unchanged; this sits above them so an app can branch on what a
failure MEANS without knowing the SDK's internal taxonomy.

Found by measuring: across 14,732 lines, four places in the entire SDK catch a
transport failure. A database read that loses connection has been throwing a
raw SocketException straight through to the caller — on a phone with patchy
signal, the most common failure there is, and the one with no typed home.

Normalization is total: an unmapped server code, a bare string, an integer all
become a KoolbaseError rather than escaping. This runs in error paths, where a
second failure has nowhere to go.

Eleven codes. The bar for adding one is that an application could reasonably do
something DIFFERENT because of it — rateLimited because waiting is not
retrying, contactNotVerified because it routes to "resend verification" not
"check your password". retryable is derived from code, never stored, so the two
cannot disagree. details carries structured context through rather than
flattening it, including the server's current record on a revision mismatch.

message is developer diagnostic data and must never reach an end user. Written
into the doc comment, not just the changelog.

Also fixes the README's first auth example, which called a method that does not
exist (Koolbase.auth.register). signUp appeared nowhere in 1382 lines, so the
front page had no correct path to creating a user — including no mention of
SignUpResult.verificationRequired, where the account exists but no session was
issued.

Additive only. Nothing throws differently, no signature moved.

v11.2.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 01:55
chore: update koolbase_flutter dependency to version 11.2.0

v11.1.1

Choose a tag to compare

@github-actions github-actions released this 16 Aug 16:32
fix: offline writes refuse signed-out — never enqueue under a null owner

A ticket parked on-device while the SDK session was dead was enqueued
with userId null. Every per-user surface then did its job correctly —
pendingWrites() throws signed-out, reads and replay filter by owner — so
the null-owner row matched nothing, forever: physically present,
invisible, unreplayable. The write neither failed nor succeeded; it
vanished into a bucket nobody owns.

All three offline enqueue sites (insert fallback, queued update, queued
delete) now throw KoolbaseUnauthenticatedException signed-out. Signed-in
behavior byte-for-byte unchanged. Mutation-pinned: removing the insert
guard fails 'writes to NO bucket'. Mirrors the RN SDK's Aug 4 fix of the
same bug family. v11.1.1

v11.1.0

Choose a tag to compare

@github-actions github-actions released this 16 Aug 09:39
feat: get(fresh: true) — cache-skipping network read

SWR cannot express read-after-write: a cached answer is the state before
your write, and awaiting the background-refresh stream proved racy
on-device (a local projection tracked the server exactly one sale
behind). get(fresh: true) skips the cache and returns the network's
answer; the read still updates the cache, so SWR callers benefit.
Plain get() unchanged. Fake queries in tests updated to the new
signature. v11.1.0

v11.0.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 12:15
feat(auth)!: SignUpResult + ContactNotVerifiedException (11.0.0)

BREAKING: signUp returns SignUpResult, not KoolbaseUser.

The server can now withhold a session at registration when a project requires
a verified contact channel. The old return type could not express 'account
created, not signed in' — the session-less 201 reached AuthSession.fromJson
and threw 'type Null is not a subtype of type String' on the absent
access_token. Observed on device before this fix.

SignUpResult carries the user in both cases and a verificationRequired flag,
so consumers must branch. Deliberately not a nullable user or nullable
session on the public surface: that would push the same null-check onto every
caller and reproduce the crash one layer up.

The session is persisted only when one was issued. Calling _setSession with
null would leave the SDK half-authenticated — reporting a signed-in user with
no token.

signUp no longer routes through _parseSession. That parser assumes tokens
exist, which is correct for login and refresh and wrong for registration
under this policy; loosening it would have weakened login's contract for no
reason.

ContactNotVerifiedException maps the server's contact_not_verified code on
login. Credentials were correct and the policy refused, so it is distinct
from InvalidCredentialsException — apps route to resend-verification rather
than telling the user to re-check a password that was right.

Added only to _checkError, not to the Apple/Google parsers: OAuth satisfies
the requirement by definition (a provider attestation IS a verified contact),
so the server never emits this code on those paths.

Enforcement is off for every project that existed before the server-side
release, so verificationRequired is false and behaviour is unchanged unless a
customer opts in.

v10.5.0

Choose a tag to compare

@github-actions github-actions released this 11 Aug 18:58
feat: KoolbaseCollectionGrid (10.5.0)

The same collection laid out as a grid. Deliberately thin: it shares
KoolbaseCollectionController with KoolbaseCollectionList, so
stale-while-revalidate, pull-to-refresh and the loading/empty/error
slots behave identically — the two differ only in layout, and sharing
the controller is what keeps them from drifting on the hard parts.

Fixed crossAxisCount rather than responsive reflow: a caller who needs
reflow can vary it from a LayoutBuilder, and building it in would have
meant guessing at tile sizing for everyone else.

Tests are narrow on purpose — the controller's properties are already
covered by the list's tests, so re-testing them here would test the
same code twice. The query fake moves to test/support so two copies
can't drift.

README's 'for grids, drive the controller directly' is no longer true
and now documents the widget instead.

v10.4.0

Choose a tag to compare

@github-actions github-actions released this 07 Aug 16:07
10.4.0: streams fetch on listen, and writes refresh them

Two gaps that both surfaced as "the UI just does not update" — nothing
errored, nothing logged, the data was simply absent. Both were found by
building an app against the SDK rather than by reading it.

.stream was a bare relay off a broadcast controller: get() performed the
fetch and pushed refreshes into it, so a stream-only listener waited
forever on a collection nothing else had read. It now fetches on first
listen, cache-first exactly as get() is.

Every write invalidated the collection cache and stopped, which only
affects the NEXT query. A listener already watching sat unchanged until
something happened to re-fetch — a message sent into a chat thread did
not appear in that thread. insert, upsert, deleteWhere, batch, and
conflict resolution now refresh open queries on the collection.

Each query re-runs ITSELF, so a stream only receives records matching its
own filters. The refresh is a registered closure per stream rather than
one rebuilt from the stream key: a key carries collection, filters, and
user but not ordering, limit, or populated fields, so a reconstructed
query would push the wrong records into a stream that never asked.

Mutation-verified: dropping the collection filter refreshes every query
in the process, failing three tests.

README gains a Live queries section — .stream was undocumented — and its
where() example is corrected to the real named-argument signature.

v10.2.0

Choose a tag to compare

@github-actions github-actions released this 04 Aug 10:44
database: insert-conflicts are real, and resolvable (10.2.0)

The Flutter twin of RN 94d8a21, closing the cross-SDK batch. Unique
constraints made insert-conflicts a genuine third kind: a queued insert
refused as a duplicate is held like any terminal refusal — but the client
coerced its operation to update (ConflictOperation admitted only two members),
and resolving one issued a PATCH against a record id that exists nowhere.
Storage never lied; only the mapper did.

ConflictOperation gains insert. Resolving a rejected insert IS the insert,
retried: resolveWithMerge carries amended data (the fix-the-colliding-title
path), unconditional — no record, no revision to be conditional against — with
the conflict's id as the idempotency key, so a resolution whose response is
lost returns the original on retry rather than duplicating. Wire-proven on the
exact route. resolveWithServer means the colliding row stands: clears with
zero requests, asserted by request count.

Seeded through the production path (enqueue → moveToRejected). Mutation-
verified: deleting the insert branch resurfaces the pre-fix wrong-verb PATCH,
caught by name by a scripted client that refuses unknown routes loudly.

README documents the insert-conflict resolution semantics in the same change.

v10.1.2

Choose a tag to compare

@github-actions github-actions released this 03 Aug 22:19
database: a refused resolution teaches the stored conflict (10.1.2)

The Flutter twin of RN fb4480d, live in a published SDK until now. Resolution
was conditional on the revision the ORIGINAL refusal reported; when the record
moved again mid-decision, the 409 — carrying current_revision and the record —
was correctly refused and entirely discarded by _resolveWrite. Every retry
replayed the stale condition; abandon was the only exit. Device-proven on RN
(three identical refusals against an unchanged server).

Flutter's factory already parsed the 409 fully into
KoolbaseRevisionMismatchException — the information died one method later. The
fix is one catch: refreshConflict absorbs currentRevision and currentRecord
into the Drift row (honoring serverState's own documented contract — 'as
returned with the refusal, so resolving does not need a fetch' — which the
CREATING refusal honored and the resolution refusal violated), then rethrows
with review-and-retry.

Proven by the revision sequence [8, 9]: the second attempt is conditional
against the LEARNED revision and succeeds without the server moving again.
Seeded through the production path (enqueue → moveToConflict). Mutation-
verified: removing the refreshConflict call fails the storage assertion.

The resolution HTTP path gained an injectable client (httpClient param) —
an unmockable resolution path is why no test ever caught this.

v10.1.1

Choose a tag to compare

@github-actions github-actions released this 02 Aug 20:28
database: signed out is a refusal, not an empty list (10.1.1)

The Flutter half of tonight's device finding. A signed-out pendingWrites()
filtered on a null identity — silently empty, indistinguishable from 'all
synced', and worse: null == null matches legacy no-owner writes, so a
signed-out display could show someone else's rows. The RN session watched the
RN variant of this lie for an evening ('pending: 0', restart-proven, two
writes unsent).

Both methods now throw KoolbaseUnauthenticatedException without a user; the
stream emits it as an error event per emission, so a sign-out under a live
badge becomes an error the moment the session dies. The prior test expecting
isEmpty is updated to the new contract with a note explaining why it changed.
Mutation-verified.

RN shipped the same contract tonight (signed-out refusal + enqueue guard).
Both SDKs now refuse to fake a zero.