From 85996fbf78f775054cdc1a6c3d1c88d86e86948e Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 11:40:25 +0200 Subject: [PATCH 01/15] Add failing test. --- packages/web/tests/crud.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/web/tests/crud.test.ts b/packages/web/tests/crud.test.ts index c615d11b..714080b3 100644 --- a/packages/web/tests/crud.test.ts +++ b/packages/web/tests/crud.test.ts @@ -3,6 +3,7 @@ import { AbstractPowerSyncDatabase, Column, ColumnType, CrudEntry, Schema, Table import { PowerSyncDatabase } from '@powersync/web'; import { v4 as uuid } from 'uuid'; import { generateTestDb } from './utils/testDb'; +import pDefer from 'p-defer'; const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42'; @@ -289,4 +290,25 @@ describe('CRUD Tests', () => { await tx2.complete(); expect(await powersync.getNextCrudTransaction()).equals(null); }); + + it('Transaction exclusivity', async () => { + const outside = pDefer(); + const inTx = pDefer(); + + const txPromise = powersync.writeTransaction(async (tx) => { + await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test1']); + inTx.resolve(); + await outside.promise; + await tx.rollback(); + }); + + await inTx.promise; + + const r = powersync.getOptional('SELECT * FROM assets WHERE id = ?', [testId]); + await new Promise((resolve) => setTimeout(resolve, 10)); + outside.resolve(); + + await txPromise; + expect(await r).toEqual(null); + }); }); From 7a5ea5b5378cce026bee8e97102f4a522e758b74 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 11:46:44 +0200 Subject: [PATCH 02/15] Correctly lock read queries. --- packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts index 96d6044b..c13856ae 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts @@ -46,7 +46,9 @@ export class WASQLiteDBAdapter extends BaseObserver implement this.dbGetHelpers = null; this.methods = null; this.initialized = this.init(); - this.dbGetHelpers = this.generateDBHelpers({ execute: this._execute.bind(this) }); + this.dbGetHelpers = this.generateDBHelpers({ + execute: (query, params) => this.acquireLock(() => this._execute(query, params)) + }); } get name() { From 73eecf244f27436ad7b59623f8cbf5390df05c7f Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 11:50:50 +0200 Subject: [PATCH 03/15] Use a local mutex to lock individual statements. --- packages/web/src/shared/open-db.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/web/src/shared/open-db.ts b/packages/web/src/shared/open-db.ts index b570d8db..57128682 100644 --- a/packages/web/src/shared/open-db.ts +++ b/packages/web/src/shared/open-db.ts @@ -2,6 +2,7 @@ import * as SQLite from '@journeyapps/wa-sqlite'; import '@journeyapps/wa-sqlite'; import * as Comlink from 'comlink'; import type { DBFunctionsInterface, OnTableChangeCallback, WASQLExecuteResult } from './types'; +import { Mutex } from 'async-mutex'; let nextId = 1; @@ -18,6 +19,7 @@ export async function _openDB( sqlite3.vfs_register(vfs, true); const db = await sqlite3.open_v2(dbFileName); + const statementMutex = new Mutex(); /** * Listeners are exclusive to the DB connection. @@ -40,10 +42,10 @@ export async function _openDB( /** * This requests a lock for executing statements. - * Should only be used interanlly. + * Should only be used internally. */ const _acquireExecuteLock = (callback: () => Promise): Promise => { - return navigator.locks.request(`db-execute-${dbFileName}`, callback); + return statementMutex.runExclusive(callback); }; /** From d5c09c9975be023cc32872a365b6c7160403af36 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 11:53:03 +0200 Subject: [PATCH 04/15] Fix type issue. --- packages/web/src/shared/open-db.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/web/src/shared/open-db.ts b/packages/web/src/shared/open-db.ts index 57128682..bf02aa7b 100644 --- a/packages/web/src/shared/open-db.ts +++ b/packages/web/src/shared/open-db.ts @@ -44,7 +44,7 @@ export async function _openDB( * This requests a lock for executing statements. * Should only be used internally. */ - const _acquireExecuteLock = (callback: () => Promise): Promise => { + const _acquireExecuteLock = (callback: () => Promise): Promise => { return statementMutex.runExclusive(callback); }; @@ -117,7 +117,7 @@ export async function _openDB( * This executes SQL statements in a batch. */ const executeBatch = async (sql: string, bindings?: any[][]): Promise => { - return _acquireExecuteLock(async () => { + return _acquireExecuteLock(async (): Promise => { let affectedRows = 0; const str = sqlite3.str_new(db, sql); @@ -129,7 +129,8 @@ export async function _openDB( const prepared = await sqlite3.prepare_v2(db, query); if (prepared === null) { return { - rowsAffected: 0 + rowsAffected: 0, + rows: { _array: [], length: 0 } }; } const wrappedBindings = bindings ? bindings : []; @@ -160,13 +161,15 @@ export async function _openDB( } catch (err) { await executeSingleStatement('ROLLBACK'); return { - rowsAffected: 0 + rowsAffected: 0, + rows: { _array: [], length: 0 } }; } finally { sqlite3.str_finish(str); } const result = { - rowsAffected: affectedRows + rowsAffected: affectedRows, + rows: { _array: [], length: 0 } }; return result; From 541ee5dab27170a08eb75fc61050dc8b3f949300 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:08:25 +0200 Subject: [PATCH 05/15] Fix Kysely test. --- packages/kysely-driver/tests/setup/db.ts | 2 -- packages/kysely-driver/tests/sqlite/sqlite-connection.test.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/kysely-driver/tests/setup/db.ts b/packages/kysely-driver/tests/setup/db.ts index 1d18c54c..ae021860 100644 --- a/packages/kysely-driver/tests/setup/db.ts +++ b/packages/kysely-driver/tests/setup/db.ts @@ -18,5 +18,3 @@ export const getPowerSyncDb = () => { return database; }; - -export const getKyselyDb = wrapPowerSyncWithKysely(getPowerSyncDb()); diff --git a/packages/kysely-driver/tests/sqlite/sqlite-connection.test.ts b/packages/kysely-driver/tests/sqlite/sqlite-connection.test.ts index ffb78303..cbaed441 100644 --- a/packages/kysely-driver/tests/sqlite/sqlite-connection.test.ts +++ b/packages/kysely-driver/tests/sqlite/sqlite-connection.test.ts @@ -21,7 +21,6 @@ describe('PowerSyncConnection', () => { it('should execute a select query using getAll from the table', async () => { await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(uuid(), ?)', ['John']); - const getAllSpy = vi.spyOn(powerSyncDb, 'getAll'); const compiledQuery: CompiledQuery = { @@ -35,7 +34,7 @@ describe('PowerSyncConnection', () => { expect(rows.length).toEqual(1); expect(rows[0].name).toEqual('John'); - expect(getAllSpy).toHaveBeenCalledTimes(1); + expect(getAllSpy).toHaveBeenCalledWith('SELECT * From users', []); }); it('should insert to the table', async () => { From a1b52be98874514f193eeb750c77de57b96d1e10 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:14:38 +0200 Subject: [PATCH 06/15] Add changeset. --- .changeset/real-bottles-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/real-bottles-mate.md diff --git a/.changeset/real-bottles-mate.md b/.changeset/real-bottles-mate.md new file mode 100644 index 00000000..377b58c6 --- /dev/null +++ b/.changeset/real-bottles-mate.md @@ -0,0 +1,5 @@ +--- +'@powersync/web': patch +--- + +Fix read statements not using the transaction lock From 1a662edbd3137753ed3715c85107051abaa34145 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:18:03 +0200 Subject: [PATCH 07/15] Fix diagnostics queries over-reporting rows and sizes. --- tools/diagnostics-app/src/app/views/sync-diagnostics.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx index 44311362..d20f9302 100644 --- a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx +++ b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx @@ -24,9 +24,9 @@ WITH (SELECT bucket, row_type, - sum(length(data)) as data_size, + sum(case when op = 3 and superseded = 0 then length(data) else 0 end) as data_size, sum(length(row_type) + length(row_id) + length(bucket) + length(key) + 40) as metadata_size, - count() as row_count + sum(case when op = 3 and superseded = 0 then 1 else 0 end) as row_count FROM ps_oplog GROUP BY bucket, row_type), oplog_stats AS @@ -51,7 +51,7 @@ FROM local_bucket_data local LEFT JOIN oplog_stats stats ON stats.name = local.id`; const TABLES_QUERY = ` -SELECT row_type as name, count() as count, sum(length(data)) as size FROM ps_oplog GROUP BY row_type +SELECT row_type as name, count() as count, sum(length(data)) as size FROM ps_oplog WHERE superseded = 0 and op = 3 GROUP BY row_type `; export default function SyncDiagnosticsPage() { From 623a740272562665840183be7e68ef09b4f6000f Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:18:24 +0200 Subject: [PATCH 08/15] Use websocket connection by default. --- .../src/library/powersync/ConnectionManager.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts index ac838f2c..24ccd6e0 100644 --- a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts +++ b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts @@ -2,6 +2,7 @@ import { BaseListener, BaseObserver, PowerSyncDatabase, + SyncStreamConnectionMethod, WebRemote, WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions @@ -11,6 +12,12 @@ import { DynamicSchemaManager } from './DynamicSchemaManager'; import { RecordingStorageAdapter } from './RecordingStorageAdapter'; import { TokenConnector } from './TokenConnector'; +import { Buffer } from 'buffer'; + +if (typeof self.Buffer == 'undefined') { + self.Buffer = Buffer; +} + Logger.useDefaults(); Logger.setLevel(Logger.DEBUG); @@ -71,7 +78,7 @@ if (connector.hasCredentials()) { } export async function connect() { - await sync.connect(); + await sync.connect({ connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET }); if (!sync.syncStatus.connected) { // Disconnect but don't wait for it sync.disconnect(); @@ -87,7 +94,7 @@ export async function clearData() { await schemaManager.clear(); await schemaManager.refreshSchema(db.database); if (connector.hasCredentials()) { - await sync.connect(); + await sync.connect({ connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET }); } } From ec1a076f3c3b19b5228db6659e5209311f0a7722 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:18:40 +0200 Subject: [PATCH 09/15] Asynchronously refresh the schema. --- .../src/library/powersync/RecordingStorageAdapter.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/diagnostics-app/src/library/powersync/RecordingStorageAdapter.ts b/tools/diagnostics-app/src/library/powersync/RecordingStorageAdapter.ts index ef690085..6c025c05 100644 --- a/tools/diagnostics-app/src/library/powersync/RecordingStorageAdapter.ts +++ b/tools/diagnostics-app/src/library/powersync/RecordingStorageAdapter.ts @@ -42,7 +42,11 @@ export class RecordingStorageAdapter extends SqliteBucketStorage { async syncLocalDatabase(checkpoint: Checkpoint) { const r = await super.syncLocalDatabase(checkpoint); - await this.schemaManager.refreshSchema(this.rdb); + // Refresh schema asynchronously, to allow us to better measure + // performance of initial sync. + setTimeout(() => { + this.schemaManager.refreshSchema(this.rdb); + }, 60); if (r.checkpointValid) { await this.rdb.execute('UPDATE local_bucket_data SET downloading = FALSE'); } From b846ee3f464bd7c2a5166d3c60464b8cdee61a98 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:42:13 +0200 Subject: [PATCH 10/15] Improve display of loaders. --- .../src/app/views/sync-diagnostics.tsx | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx index d20f9302..c58977a2 100644 --- a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx +++ b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx @@ -181,50 +181,40 @@ export default function SyncDiagnosticsPage() { ); const tablesTable = ( - - - Tables - - - + } + }} + pageSizeOptions={[10, 50, 100]} + disableRowSelectionOnClick + /> ); const bucketsTable = ( - - - Buckets - - - + }, + sorting: { + sortModel: [{ field: 'total_operations', sort: 'desc' }] + } + }} + pageSizeOptions={[10, 50, 100]} + disableRowSelectionOnClick + /> ); return ( @@ -239,8 +229,18 @@ export default function SyncDiagnosticsPage() { }}> Clear & Redownload - {tableRowsLoading ? : tablesTable} - {bucketRowsLoading ? : bucketsTable} + + + Tables + + {tableRowsLoading ? : tablesTable} + + + + Buckets + + {bucketRowsLoading ? : bucketsTable} + ); From fd8d58749b3d74e56f52bce6a3b9d261784dd2e9 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:42:29 +0200 Subject: [PATCH 11/15] Avoid running slow queries during initial sync. --- .../src/app/views/sync-diagnostics.tsx | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx index c58977a2..e919e412 100644 --- a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx +++ b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx @@ -1,5 +1,5 @@ import { NavigationPage } from '@/components/navigation/NavigationPage'; -import { clearData, syncErrorTracker } from '@/library/powersync/ConnectionManager'; +import { clearData, db, syncErrorTracker } from '@/library/powersync/ConnectionManager'; import { Box, Button, @@ -54,20 +54,55 @@ const TABLES_QUERY = ` SELECT row_type as name, count() as count, sum(length(data)) as size FROM ps_oplog WHERE superseded = 0 and op = 3 GROUP BY row_type `; -export default function SyncDiagnosticsPage() { - const { data: bucketRows, isLoading: bucketRowsLoading } = useQuery(BUCKETS_QUERY, undefined, { - rawTableNames: true, - tables: ['ps_oplog', 'ps_data_local__local_bucket_data'], - throttleMs: 500 - }); - const { data: tableRows, isLoading: tableRowsLoading } = useQuery(TABLES_QUERY, undefined, { - rawTableNames: true, - tables: ['ps_oplog', 'ps_data_local__local_bucket_data'], - throttleMs: 500 - }); +const BUCKETS_QUERY_FAST = ` +SELECT + local.id as name, + '[]' as tables, + 0 as data_size, + 0 as metadata_size, + 0 as row_count, + local.download_size, + local.total_operations, + local.downloading +FROM local_bucket_data local`; +export default function SyncDiagnosticsPage() { + const [bucketRows, setBucketRows] = React.useState(null); + const [tableRows, setTableRows] = React.useState(null); const [syncError, setSyncError] = React.useState(syncErrorTracker.lastSyncError); + const bucketRowsLoading = bucketRows == null; + const tableRowsLoading = tableRows == null; + + React.useEffect(() => { + const controller = new AbortController(); + + db.onChangeWithCallback( + { + async onChange(event) { + // Similar to db.currentState.hasSynced, but synchronized to the onChange events + const hasSynced = await db.getOptional('SELECT 1 FROM ps_buckets WHERE last_applied_op > 0 LIMIT 1'); + if (hasSynced != null) { + // These are potentially expensive queries - do not run during initial sync + const bucketRows = await db.getAll(BUCKETS_QUERY); + const tableRows = await db.getAll(TABLES_QUERY); + setBucketRows(bucketRows); + setTableRows(tableRows); + } else { + // Fast query to show progress during initial sync + const bucketRows = await db.getAll(BUCKETS_QUERY_FAST); + setBucketRows(bucketRows); + setTableRows(null); + } + } + }, + { rawTableNames: true, tables: ['ps_oplog', 'ps_buckets', 'ps_data_local__local_bucket_data'], throttleMs: 500 } + ); + return () => { + controller.abort(); + }; + }, []); + React.useEffect(() => { const l = syncErrorTracker.registerListener({ lastErrorUpdated(error) { @@ -111,7 +146,7 @@ export default function SyncDiagnosticsPage() { } ]; - const rows = bucketRows.map((r) => { + const rows = (bucketRows ?? []).map((r) => { return { id: r.name, name: r.name, @@ -146,7 +181,7 @@ export default function SyncDiagnosticsPage() { } ]; - const tablesRows = tableRows.map((r) => { + const tablesRows = (tableRows ?? []).map((r) => { return { id: r.name, ...r From f72ce5a0e79a3405705d3957215207677f8e4d64 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:43:51 +0200 Subject: [PATCH 12/15] Cleanup. --- tools/diagnostics-app/src/app/views/sync-diagnostics.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx index e919e412..8d0d06e3 100644 --- a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx +++ b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx @@ -15,7 +15,6 @@ import { styled } from '@mui/material'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { useQuery } from '@powersync/react'; import React from 'react'; const BUCKETS_QUERY = ` From 995ae13ed84fce36fad71ed0d265425902e7f7c1 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:48:57 +0200 Subject: [PATCH 13/15] Fix displaying stats after navigating. --- .../src/app/views/sync-diagnostics.tsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx index 8d0d06e3..b628ae8d 100644 --- a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx +++ b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx @@ -73,30 +73,37 @@ export default function SyncDiagnosticsPage() { const bucketRowsLoading = bucketRows == null; const tableRowsLoading = tableRows == null; + const refreshStats = async () => { + // Similar to db.currentState.hasSynced, but synchronized to the onChange events + const hasSynced = await db.getOptional('SELECT 1 FROM ps_buckets WHERE last_applied_op > 0 LIMIT 1'); + if (hasSynced != null) { + // These are potentially expensive queries - do not run during initial sync + const bucketRows = await db.getAll(BUCKETS_QUERY); + const tableRows = await db.getAll(TABLES_QUERY); + setBucketRows(bucketRows); + setTableRows(tableRows); + } else { + // Fast query to show progress during initial sync + const bucketRows = await db.getAll(BUCKETS_QUERY_FAST); + setBucketRows(bucketRows); + setTableRows(null); + } + }; + React.useEffect(() => { const controller = new AbortController(); db.onChangeWithCallback( { async onChange(event) { - // Similar to db.currentState.hasSynced, but synchronized to the onChange events - const hasSynced = await db.getOptional('SELECT 1 FROM ps_buckets WHERE last_applied_op > 0 LIMIT 1'); - if (hasSynced != null) { - // These are potentially expensive queries - do not run during initial sync - const bucketRows = await db.getAll(BUCKETS_QUERY); - const tableRows = await db.getAll(TABLES_QUERY); - setBucketRows(bucketRows); - setTableRows(tableRows); - } else { - // Fast query to show progress during initial sync - const bucketRows = await db.getAll(BUCKETS_QUERY_FAST); - setBucketRows(bucketRows); - setTableRows(null); - } + await refreshStats(); } }, { rawTableNames: true, tables: ['ps_oplog', 'ps_buckets', 'ps_data_local__local_bucket_data'], throttleMs: 500 } ); + + refreshStats(); + return () => { controller.abort(); }; From edfe4cd552252983da7dec1be91cb4a67d638c7d Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:49:58 +0200 Subject: [PATCH 14/15] Increase cache size. --- .../diagnostics-app/src/library/powersync/ConnectionManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts index 24ccd6e0..4bfda3bc 100644 --- a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts +++ b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts @@ -29,6 +29,8 @@ export const db = new PowerSyncDatabase({ }, schema: schemaManager.buildSchema() }); +db.execute('PRAGMA cache_size=-50000'); + export const connector = new TokenConnector(); const remote = new WebRemote(connector); From 1b2b2079fbe677f5dd1bfb5b54548b8345eda9cd Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 16 Jul 2024 12:52:01 +0200 Subject: [PATCH 15/15] Add changeset. --- .changeset/happy-gifts-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/happy-gifts-sort.md diff --git a/.changeset/happy-gifts-sort.md b/.changeset/happy-gifts-sort.md new file mode 100644 index 00000000..208d943b --- /dev/null +++ b/.changeset/happy-gifts-sort.md @@ -0,0 +1,5 @@ +--- +'diagnostics-app': minor +--- + +Faster initial sync and other fixes