at_persistence_secondary_server
A Dart library with the implementation classes for the persistence layer of the secondary server.
5.2.1
17K downloads/mo
#2369 most downloaded on pub.dev
atsign-foundation/at_server
What this package is like to depend on
Last release 13 days ago
11 Aug 2026
Release timing varies
gaps range from 3 weeks to 7 months
Nearly every release is documented
notes for 95 of 95 stable releases
2 versions withdrawn
withdrawn after publishing
6 years old
97 releases · first in 2020
9 releases in the last 12 months
see the full history below
Release timeline
97 releases · Oct 2020 to Aug 2026Releases
latest 60 of 97-
5.2.111 Aug 2026Release notes
Open source →-
feat:
AtCommitLog.iterateacceptsskipDeletesUntil/latestCommitId, pushing sync's delete-skip into the query. Previously sync fetched every commit entry fromfromCommitIdand discarded below-watermark DELETE entries in a Dart filter; now the backend excludes them at the source — SQLite in the SQLWHERE, so those rows are never read or deserialised — while always yielding the single latest entry so the client can still advance its watermark. Legacy callers (noskipDeletesUntil) are unaffected. -
perf: the SQLite skip-deletes query is now an index-driven range scan over a new partial index of live entries (
commit_log_live), not aNOT (operation = '-' ...)filter over the fullcommit_idindex. The filter form is correct but its plan walks every row fromfromCommitIdto the tail in SQLite's C layer, so a client near the head of a delete-dominated log still pays a full-log scan; the partial index seeks straight to the live rows. Measured on a 1M-entry log holding one live entry, the skip-deletes scan drops from ~253ms to ~0.2ms. The index is created byCREATE INDEX IF NOT EXISTSon open, with no contract-version bump — an existing database acquires it on next start. -
perf:
SqliteAtCommitLog.iteratestreams rows through a prepared cursor rather than an eagerselect()that materialised the whole result set before the caller saw a row. Callers stop early (sync breaks after one page), so an ordinarysyncreturning 25 entries against a 1M-entry log drops from ~500ms to sub-millisecond. -
fix:
getSize()on the SQLite commit-log, access-log and notification stores returned raw bytes; the spec and the Hive stores return KB. All three now return KB (they had over-reported by 1024x). -
fix:
PersistenceMigratornow drops ORPHANED commit entries rather than copying them into the target — a non-delete commit entry whose key is absent from the keystore. Hive accumulates these (its commit log's box and in-memory cache can disagree, and the TTL-expiry purge is cache-driven, so it can miss the box row); SQLite purges by atKey directly against storage and cannot produce one. Migrating them forward would make them permanent residents of a backend that can never generate one, andPersistenceSnapshotfilters them from both sides so the post-migration verify cannot see them. Migration is where both stores are already walked, so reconciling costs nothing. DELETE tombstones always survive — they legitimately have no keystore row, and dropping them would break delete propagation to clients. Keys that are expired but still present are NOT orphans and migrate verbatim. The dropped count is reported asMigrationReport.orphanedCommitEntries(a new required field on that class) and logged per entry. -
fix: corrected the 5.2.0 entry below, which described a commit-log orphan as "sync-benign". It was not — an orphan failed the whole sync request with
AT0015until the fix in at_secondary_server 3.15.0. -
fix: resolve sqlite migration deadlocks and OOMs by enforcing TRUNCATE mode and disabling true MVCC snapshots
-
-
5.2.008 Jul 2026Release notes
Open source →- feat: dual-write persistence layer (
lib/dual.dart) — serves reads from a primary backend while mirroring every write byte-exactly into a secondary (viarestore/replay, not operation-replay, so server-stamped timestamps and commit ids stay identical). Run a real workload once, compare after. - feat:
bin/compare_persistence.dart— compares two DB sets (backend + storage root) for one atSign across all four stores and reports every inconsistency; exit 0 identical / 1 differences / 2 error. Backed byPersistenceSnapshot.differencesFrom(all differences, not just the first). - feat: SQLite persistence backend (
lib/sqlite.dart) implementing the same spec interfaces as Hive —SqliteAtKeyValueStore(with full- fidelitysupportsSnapshots/supportsPathQueries/ true transactions),SqliteAtCommitLog(dense/gaplesscounters-based commit ids),SqliteAtNotificationKeystore,SqliteAtAccessLog,SqliteAtPersistenceFactory. Oneatsign.dbper atSign; the schema is a stable per-atSign interchange contract. - feat: migrator-only verbatim primitives —
AtKeyValueStore.restoreandAtAccessLog.replay(symmetric toAtCommitLog.replay) — plusPersistenceMigrator(backend-agnostic Hive↔SQLite copy) andPersistenceSnapshot(canonical bundle comparator).AtPersistenceBackendIdgainssqlite. Covered by a conversion-integrity gate: hive→sqlite→hive→sqlite→hive→sqlite round-trips byte-identically. - fix:
PersistenceSnapshottolerates Hive's non-deterministic commit-log orphans — a non-delete commit entry whose key is absent or expired is skipped. After a TTL-expiryskipCommitsweep, Hive can leave a stale commit entry in its box (its cache-basedgetLatestCommitEntrypurge misses the box entry — a cache/box inconsistency); a faithful SQLite mirror carries no such entry. Such an entry is excluded from both snapshots. (Corrected in 5.2.1: this entry originally described the orphan as "sync-benign". It was not — until the fix in at_secondary_server 3.15.0 an orphan failed the entire sync request withAT0015. It is benign to the SNAPSHOT COMPARISON, which is what this change is about.) DELETE entries and live unexpired-key entries are always compared, so real mirror data loss still fails. Makes the dual-write Hive-vs-SQLite DB-set comparison deterministic under a real functional workload, so it can gate CI.
- feat: dual-write persistence layer (
-
5.1.017 Jun 2026Release notes
Open source →- feat: new persistence field
AtMetaData.appMetadatacarrying the provider-ownedAppMetadatafrom at_commons (opaque to the server). Stored byAtMetaDataAdapteras a JSON-encoded string (hive field 26); records written before the field existed read back withappMetadata == null. Mapped infromCommonsMetadata/toCommonsMetadataandtoJson/fromJson(the latter accepts both the Map and base64 wire forms).
- feat: new persistence field
-
5.0.016 Jun 2026Release notes
Open source →Major release: persistence-overhaul. Themes:
- Bootstrap is now factory + bundle, not singletons.
AtPersistenceFactory/AtPersistenceBundlereplace every legacygetInstance()shim. Backend-pluggable by design. - Hive concretes renamed; abstract interfaces freed.
HiveAtCommitLog/HiveAtAccessLog/HiveAtNotificationKeystore/HiveAtKeyValueStoreimplement abstractAtCommitLog/AtAccessLog/AtNotificationKeystore/AtKeyValueStore. Bundles type at the abstracts. - Slim bundle + capability toggles. The bundle exposes
keyValueStore(core) plusaccessLog?andnotificationKeystore?(optional capabilities gated byAtPersistenceConfigtoggles).serverDefaultsopts into every capability;clientDefaultsopts into the keystore only. - First-class min/max/floor query surface.
KeyValueStoregainsnextExpiresAt/peekExpired(expiry wake-up + bounded ascending-order drain, on both the main and notification keystores);AtKeyValueStoregainsnextAvailableAt/peekNewlyAvailable(TTB wake-up + a caller-side-watermark sweep window);AtCommitLoggainsfirstCommittedSequenceNumber(the log's floor, pairing withlastCommittedSequenceNumberso sync clients can detect "server no longer retains my delta"). The Hive notification keystore now maintains an in-memory effective-expiry index (foldingexpiresAt, thenotificationDateTime + maxTtlguard, and already-expired-by-shape entries into one comparable instant), which also backsgetExpiredKeyswithout a full-box deserialise. Fixed: bulk commit-log removal left stale entries in the commit-log cache; cache eviction is now guarded so deleting an older duplicate never evicts the newer live entry. AtKeyValueStorewidened with ten new primitives for at_client adoption:exists,scanKeys(withKeyPattern+ ordering + pagination),getMany,removeMany,changesstream,transaction,queryByPath(+supportsPathQueries),snapshot(+supportsSnapshots), andstats. Capability flags let a future SQL backend light up native indexed query / MVCC paths while Hive falls back gracefully.- Keystore type hierarchy collapsed.
SecondaryKeyStorerenamed toAtKeyValueStore;HiveSecondaryKeyStorerenamed toHiveAtKeyValueStore. Bundle fieldkeyStorerenamed tokeyValueStore.Keystore(read-only),WritableKeystore, andSynchronizableKeyStorecollapsed into a singleKeyValueStore<K, V>interface that holds the merged CRUD + rich surface (scan, bulk, expire, change stream, transaction, snapshot, stats).AtKeyValueStore<K, V, T>extendsKeyValueStoreand adds the sync-coupled surface: the (nullable)commitLog,putMeta/putAll/getMeta, andqueryByPath/supportsPathQueries. scanKeys/KeyPatternlive onAtKeyValueStore, notKeyValueStore. Structured filtering (KeyPattern'ssharedBy/sharedWith/namespace/idPrefix) is atKey-shaped, so it belongs on the atKey-aware tier rather than the generic key-value contract.KeyValueStore.existsand the pre/post-remove hook callbacks are typed at the generic keyK(wasString), since they don't carry the atKey assumption. A newAtKeyValueStoreSnapshot extends KeyStoreSnapshotaddsscanKeyson top of the base snapshot for the same reason;HiveAtNotificationKeystore(and its snapshot) no longer carry the awkwardscanKeysimpl that only honouredidPrefix.- Model classes decoupled from Hive.
AtData,AtMetaData,CommitEntry,AccessLogEntryandAtNotificationno longer carry Hive's@HiveType/@HiveFieldannotations (they were dead — no codegen) orextends HiveObject. The hand-writtenTypeAdapters moved tolib/src/hive/adapters/(re-exported by the barrel; on-disk wire format unchanged).AtDataandCommitEntrykeep a transient, non-serializedkeyfield that the keystore populates on read, replacing theHiveObject.keythat Hive used to auto-stamp. The five models are now plain Dart objects — the seam a future SQLite/Postgres backend needs. HiveAtKeyValueStorevalue type tightened to non-nullAtData. The main keystore now implementsAtKeyValueStore<String, AtData, AtMetaData?>(was<String, AtData?, AtMetaData?>), andAtPersistenceBundlesurfaces it as such.put/create/putAllreject a null value at compile time instead of crashing on an internalvalue!;getManyreturnsMap<String, AtData>.getis unchanged — it still returnsFuture<AtData?>(nullfor an absent key). The metadata type parameter stays nullable (AtMetaData?) sincegetMetalegitimately returnsnull.- Keystore listing/existence APIs are now fully async, and
list-returning queries stream.
KeyValueStore.getKeys,getExpiredKeysandscanKeysnow returnFuture<Stream<…>>;LogKeyStore.getExpiredandAtCommitLog.getChangeslikewise returnFuture<Stream<…>>. TheFuturecompletes once the backend has accepted the request (so setup failures — store not open, invalid regex — reject eagerly rather than mid-stream); theStreamthen yields the results. The synchronousisKeyExistsis removed — use the asyncexists(the two were duplicates;existsis the backend-agnostic shape SQL backends need). AtKeyValueStore.commitLogis nullable, end to end. Server bundles hold a non-null commit log on the keystore; client bundles holdnull(sync via fsync or other mechanism).AtPersistenceConfig.enableCommitLog(defaulttrue;serverDefaults→true,clientDefaults→false) selects which — whenfalsethe factory builds the keystore commit-log-free and never opens the commit-log box.HiveAtKeyValueStorehonours anullcommit log throughout:put/create/putAll/putMeta/remove/removeManysucceed and returnnull(no sequence number), andcompact()is a no-op that yields nothing. The server's bootstrap asserts non-null once and binds to a non-nullable local for downstream consumers.- Bundle no longer exposes the commit log directly.
AtPersistenceBundle.commitLogis gone; reach it asbundle.keyValueStore.commitLog!. AtNotificationKeystorere-parented toKeyValueStore. No longer extends the AtKeyValueStore tier — notifications don't participate in the commit log. Drops the sixUnimplementedErrorstubs and the no-opcommitLogoverride that the old interface forced on it. Notification keystore implementations stop pretending to supportputMeta/putAll/getMeta/queryByPath. Now strongly typedKeyValueStore<String, AtNotification>(was<dynamic, dynamic>) — keys are notification ids, values areAtNotification; consumers no longer cast.- Compaction is intrinsic, not strategy-wrapped. The
Compactableinterface (one method:Stream<Object> compact(bool dryRun)) replacesAtCompactionStrategy,HiveCompactionStrategy,AtCompaction,AtLogType,AtCompactionConfig, andAtCompactionStats— all deleted.AtCommitLog,AtAccessLog,AtNotificationKeystore, andAtKeyValueStoreall implementCompactable. The Hive impls each carry their owncompactionPercentageconstructor parameter; oncompact(false)they yield the items removed,compact(true)yields what would be removed. - Compactor scheduling moves to
at_secondary_server. The persistence layer no longer schedules anything — the secondary server runs threeTimer.periodicticks with overlap guards.AtCompactionJobdeleted.AtCompactionStatsService.record()takes primitives (label,start,compactedCount,duration) instead of anAtCompactionStatsobject. AtPersistenceConfigcompactor flags removed.enableCommitLogCompactor/enableAccessLogCompactor/enableKeyStoreCompactormoved toAtSecondaryConfig(withenableKeyStoreCompactor→enableNotificationCompactor, matching the resource it actually gates).- Migrator-friendly walks.
replay(CommitEntry)anditerate({fromCommitId, where})onAtCommitLog, plusiterate()on access log + notification keystore. Lazy box- walks; no upfront map materialisation. - Commit log is one-entry-per-atKey by construction. Inline
single-atKey dedup in
CommitLogKeyStore.add()plus a startup dedup migration.CommitLogCompactionServiceandCompactionSortedListretired (their job is done eagerly by the write path). AtCommitLog.getEntriesretired. Migrate toiterate(fromCommitId, where: closure); the closure carries any caller-side filtering (regex, skipDeletesUntil, etc.).- Keystore signatures tightened to remove
dynamic. CRUD methods onKeyValueStorenow returnFuture<int?>(commit-log sequence number ornull) instead ofFuture<dynamic>.KeyValueStore.getisFuture<V?>instead ofFuture<V>?.LogKeyStore.addreturnsFuture<int>(Hive-assigned key);update/removereturnFuture<void>;getFirstNEntriesreturnsList<int>;getExpiredreturnsFuture<List<K>>.AtAccessLog'smostVisitedAtSigns/mostVisitedKeysreturnFuture<Map<String, int>>. Pre/post-remove hook callbacks areFuture<void> Function(...). Call sites can dropas int?/as AtData?/as Mapcasts. AtKeyValueStore.deleteExpiredKeysdropsskipCommit. Expiry is treated as backend-local maintenance: the sweep never advancescommitIdand never propagates via sync.AtKeyValueStore.commitLogtypedAtCommitLog?rather thanAtLogType?. Callers no longer needas AtCommitLogat every access site.HiveAtNotificationKeystore.commitLogis a no-op getter/setter instead of a mutable field — notifications never participate in the commit log.SyncProgressiveVerbHandlerempty-response wedge fixed. When every entry in a requested range failedisAuthorized(or another in-loop filter), the handler returned[]and the client could not advance itsfromwatermark. The switch toiterate(where:)makes the scan unbounded; the client now always sees forward progress. No wire-format change.AtConfigmoved out of this package intoat_secondary_server. Block-list writes passskipCommit: trueand don't bump the localcommitId.- Test-isolation primitive.
AtPersistenceBundle.clear()drops every entry from each store while keeping underlying boxes open. - Dependency on
at_persistence_specdropped. The interface types this package previously imported fromat_persistence_spec(AtKeyValueStore, exceptions, the@server/@clientannotations, and the new query / change-stream types —KeyEntry,KeyPattern,KeyStoreChange,KeyStoreSnapshot,KeyStoreStats,KeyStoreTxn,OrderByKey,Predicate) now live alongside the Hive implementation in this package. Future backend packages depend onat_persistence_secondary_serverfor the interface types they need to satisfy. - Factory close-and-reuse hardened.
AtPersistenceBundleexposesbool get isClosed.AtPersistenceFactorygainsFuture<void> closeFor(String atSign)— closes a single bundle and drops the factory's reference.initializeandbundleFornow treat a closed entry as absent (a closed bundle is dropped and rebuilt on nextinitialize, andbundleForreturnsnull), so a caller closing a bundle directly viabundle.close()can no longer cause the factory to hand out a stale closed reference. - Dead code removed. Deleted the unused
StoreTypeenum,AtCommitLog.firstCommittedSequenceNumber(no callers), theNullCommitEntrysentinel (the package returnsnull, never a sentinel), and the vestigialLogKeyStoreinterface — one implementer, no consumers — along with the orphaned keystore methods it carried (getExpired, and the commit-log keystore'sgetFirstNEntries).
See
MIGRATION.mdfor the full migration guide: what changed in this package (5.0.0 vs 4.3.5), howat_secondary_serverwas reworked to consume it, and a step-by-step guide for migratingat_clientonto 5.0.0 and a commit-log-free local keystore. - Bootstrap is now factory + bundle, not singletons.
-
4.3.527 Apr 2026Release notes
Open source →- perf:
NullCommitEntryis now a singleton — no freshDateTime.now()allocation per commit-log miss - perf:
HiveKeystore.getKeyscaches compiledRegExppatterns in a bounded LRU and capturesDateTime.timestamp()once per call, threading it into_isExpired/_isBorn/_isKeyAvailableso each key check no longer allocates two DateTime objects - perf:
DateTime.now().toUtc()replaced withDateTime.timestamp()(one allocation instead of two) on the keystore expiry, notification expiry, access-log and commit-log hot paths - perf:
HiveKeystore.createno longer builds a 17-elementSetper put just to ask "any metadata field non-null?" — replaced with a direct null-check chain
- perf:
-
4.3.422 Mar 2026 -
4.3.304 Mar 2026 -
4.3.103 Mar 2026Release notes
Open source →- fix: backwards compatibility: revert AtNotificationKeystore.currentAtSign to a non-final instance variable
-
4.3.002 Mar 2026 withdrawnRelease notes
Open source →- feat: deprecated AtNotificationCallback class
- feat: deprecated use of AtNotificationKeystore singleton
- feat: deprecated NotificationManagerSpec
- fix: AtNotificationBuilder.build will update a null ttl to the default value
- fix: AtNotification.isExpired treats null expiry as expired, as the idea of notifications without expiration is a historical antipattern and is no longer possible
-
4.2.007 Aug 2025 -
4.1.006 Jun 2025 -
4.0.010 Mar 2025Release notes
Open source →- refactor: Take up new major version 3.0.0 of at_persistence_spec, update and simplify the HiveKeyStore implementation accordingly
- feat: (non-breaking) Add persistence support for the new
immutableflag
-
3.1.004 Dec 2024Release notes
Open source →- feat: commit log changes for sync skipDeletesUntil feature
- build[deps]: Upgraded the following package:
- at_commons to v5.1.0
-
3.0.6602 Dec 2024Release notes
Open source →- feat: Add "PublicKeyHash" to the "AtMetadata" which holds the hash value of encryption public key
- build[deps]: Upgraded the following packages:
- at_commons to v5.0.2
- lints to v5.0.0
- test to v1.25.8
-
3.0.6516 Oct 2024Release notes
Open source →- fix: Modified checks in commit log keystore _alwaysIncludeInSync method to match only reserved shared_key, encryption public key and public key without namespace.
- build[deps]: Upgraded the following packages:
- at_commons to v5.0.1
-
3.0.6428 Sep 2024Release notes
Open source →- build[deps]: Upgraded the following packages:
- at_commons to v5.0.0
- at_utils to v3.0.19
- build[deps]: Upgraded the following packages:
-
3.0.6301 Aug 2024 -
3.0.6205 Apr 2024Release notes
Open source →- fix: Add check for hive key max length (255 chars)
- build[deps]: Upgraded the following packages:
- at_commons to v4.0.5
- hive to v2.2.3
- crypto to v3.0.3
-
3.0.6121 Feb 2024Release notes
Open source →- feat: delete entries for expired keys are not committed to the commitLog [feature not enabled yet]
-
3.0.6003 Jan 2024Release notes
Open source →- build[deps]: Upgraded the following packages:
- at_commons to v4.0.0
- at_utils to v3.0.16
- build[deps]: Upgraded the following packages:
-
3.0.5923 Oct 2023Release notes
Open source →- fix: When checking namespace authorization, gracefully handle any malformed keys which happen to be in the commit log for historical reasons
-
3.0.5816 Oct 2023Release notes
Open source →- fix: Modify "lastCommittedSequenceNumberWithRegex" to return highest commitId among enrolled namespaces
-
3.0.5723 Aug 2023 -
3.0.5620 Jul 2023Release notes
Open source →- fix: Refactor Hive keystore to optimize memory usage
- fix: Apply Utf7.decode function to decode the keys and atSigns containing emojis.
- feat: add skipCommit flag to keystore implementation which enables skipping commit log for put/create/remove.
-
3.0.5511 Jul 2023 withdrawnNothing published for this version
-
3.0.5407 Jul 2023Release notes
Open source →- fix: Add NotificationType.Self in read and write methods of at_notification.dart
-
3.0.5306 Jul 2023Release notes
Open source →- feat: Introduced self notification type in enum for apkam enrollment
- chore: upgraded at_commons to 3.0.50 and at_utils to 3.0.14
-
3.0.5216 Mar 2023 -
3.0.5111 Mar 2023 -
3.0.5009 Mar 2023Release notes
Open source →- fix: AtMetaData.fromJson now preserves null values for ttl, ttb and ttr
- test: Add '==' & hashCode to AtMetaData in order to be able to test equality
- test: Added tests which verify JSON round-tripping of AtMetaData objects
- refactor: Deprecate at_metadata_adapter; extract the 'to' and 'from' commons Metadata methods from there into the AtMetaData class itself
-
3.0.4922 Feb 2023 -
3.0.4821 Feb 2023 -
3.0.4718 Feb 2023 -
3.0.4606 Feb 2023 -
3.0.4525 Jan 2023Release notes
Open source →- fix: Introduce "isScheduled" method in "AtCompactionService" to know if the compaction job is running
-
3.0.4427 Dec 2022 -
3.0.4315 Nov 2022Release notes
Open source →- fix: Fetch only commit entries with 'null' commit-id for uncommitted entries in at_client persistence
-
3.0.4203 Nov 2022 -
3.0.4128 Oct 2022Release notes
Open source →- fix: store actual keys in hive keystore metadata cache instead of encoded keys
- feat: throw KeyNotFoundException if key to be removed is not present in keystore
-
3.0.4020 Oct 2022 -
3.0.3912 Oct 2022 -
3.0.3812 Oct 2022 -
3.0.3707 Oct 2022 -
3.0.3616 Sep 2022Release notes
Open source →- fix: skip commit id and sync for signing keys
- fix: dart analyzer issues
- chore: upgrade third party dependencies
-
3.0.3508 Sep 2022Release notes
Open source →- fix: Randomize the cron job's start interval
- fix: Reduce the default notification expiry duration
-
3.0.3418 Aug 2022Release notes
Open source →- fix: Reverted dependency on 'meta' package to ^1.7.0 as flutter_test package (currently) requires 1.7.0
-
3.0.3312 Aug 2022Release notes
Open source →- feat: added key validation to keystore put and create methods
- chore: upgraded at_commons version to 3.0.24
-
3.0.3208 Aug 2022 -
3.0.3127 Jul 2022 -
3.0.3022 Jul 2022Release notes
Open source →- Enhance KeyNotFoundException to chain into exception hierarchy.
- Upgrade at_commons version to 3.0.20 to encrypt notify text
-
3.0.2927 Jun 2022Release notes
Open source →- Introduced option to stop current schedule of a compaction job
- Enable the public hidden keys to sync between local and cloud secondary
- Uptake at_commons to 3.0.18 to optionally display hidden keys in scan
-
3.0.2817 Jun 2022Release notes
Open source →- Updated lastSyncedEntryCacheMap regex to match the reserved keys
- Upgraded to version 2.0.6 of at_persistence_spec containing @server/@client annotations
-
3.0.2715 Jun 2022 -
3.0.2615 Jun 2022Release notes
Open source →- Replace null commitId's with hive internal key on secondary server startup
- Return commit entry with highest commitId from lastSyncedEntry
- Upgrade at_commons version for AtException hierarchy
-
3.0.2518 May 2022Release notes
Open source →- To reduce latency on notifications, publish the event for the notification before persisting the notification
-
3.0.2425 Apr 2022Release notes
Open source →- Introduced a cache to speed up metaData retrieval.
- Removed unnecessary print statements
-
3.0.2313 Apr 2022 -
3.0.2212 Apr 2022 -
3.0.2104 Apr 2022 -
3.0.2004 Apr 2022