@powersync/common 1.34.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +56 -5
  4. package/lib/client/AbstractPowerSyncDatabase.js +96 -29
  5. package/lib/client/CustomQuery.d.ts +22 -0
  6. package/lib/client/CustomQuery.js +42 -0
  7. package/lib/client/Query.d.ts +97 -0
  8. package/lib/client/Query.js +1 -0
  9. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
  10. package/lib/client/sync/stream/AbstractRemote.js +31 -19
  11. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +3 -2
  12. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +16 -4
  13. package/lib/client/watched/GetAllQuery.d.ts +32 -0
  14. package/lib/client/watched/GetAllQuery.js +24 -0
  15. package/lib/client/watched/WatchedQuery.d.ts +98 -0
  16. package/lib/client/watched/WatchedQuery.js +12 -0
  17. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
  18. package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
  19. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
  20. package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
  21. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
  22. package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
  23. package/lib/client/watched/processors/comparators.d.ts +30 -0
  24. package/lib/client/watched/processors/comparators.js +34 -0
  25. package/lib/index.d.ts +8 -0
  26. package/lib/index.js +8 -0
  27. package/lib/utils/BaseObserver.d.ts +3 -4
  28. package/lib/utils/BaseObserver.js +3 -0
  29. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  30. package/lib/utils/MetaBaseObserver.js +50 -0
  31. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
3
+ import { WatchCompatibleQuery } from './WatchedQuery.js';
4
+ /**
5
+ * Options for {@link GetAllQuery}.
6
+ */
7
+ export type GetAllQueryOptions<RowType = unknown> = {
8
+ sql: string;
9
+ parameters?: ReadonlyArray<unknown>;
10
+ /**
11
+ * Optional mapper function to convert raw rows into the desired RowType.
12
+ * @example
13
+ * ```javascript
14
+ * (rawRow) => ({
15
+ * id: rawRow.id,
16
+ * created_at: new Date(rawRow.created_at),
17
+ * })
18
+ * ```
19
+ */
20
+ mapper?: (rawRow: Record<string, unknown>) => RowType;
21
+ };
22
+ /**
23
+ * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
24
+ */
25
+ export declare class GetAllQuery<RowType = unknown> implements WatchCompatibleQuery<RowType[]> {
26
+ protected options: GetAllQueryOptions<RowType>;
27
+ constructor(options: GetAllQueryOptions<RowType>);
28
+ compile(): CompiledQuery;
29
+ execute(options: {
30
+ db: AbstractPowerSyncDatabase;
31
+ }): Promise<RowType[]>;
32
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
3
+ */
4
+ export class GetAllQuery {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ compile() {
10
+ return {
11
+ sql: this.options.sql,
12
+ parameters: this.options.parameters ?? []
13
+ };
14
+ }
15
+ async execute(options) {
16
+ const { db } = options;
17
+ const { sql, parameters = [] } = this.compile();
18
+ const rawResult = await db.getAll(sql, [...parameters]);
19
+ if (this.options.mapper) {
20
+ return rawResult.map(this.options.mapper);
21
+ }
22
+ return rawResult;
23
+ }
24
+ }
@@ -0,0 +1,98 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { BaseListener } from '../../utils/BaseObserver.js';
3
+ import { MetaBaseObserverInterface } from '../../utils/MetaBaseObserver.js';
4
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
5
+ /**
6
+ * State for {@link WatchedQuery} instances.
7
+ */
8
+ export interface WatchedQueryState<Data> {
9
+ /**
10
+ * Indicates the initial loading state (hard loading).
11
+ * Loading becomes false once the first set of results from the watched query is available or an error occurs.
12
+ */
13
+ readonly isLoading: boolean;
14
+ /**
15
+ * Indicates whether the query is currently fetching data, is true during the initial load
16
+ * and any time when the query is re-evaluating (useful for large queries).
17
+ */
18
+ readonly isFetching: boolean;
19
+ /**
20
+ * The last error that occurred while executing the query.
21
+ */
22
+ readonly error: Error | null;
23
+ /**
24
+ * The last time the query was updated.
25
+ */
26
+ readonly lastUpdated: Date | null;
27
+ /**
28
+ * The last data returned by the query.
29
+ */
30
+ readonly data: Data;
31
+ }
32
+ /**
33
+ * Options provided to the `execute` method of a {@link WatchCompatibleQuery}.
34
+ */
35
+ export interface WatchExecuteOptions {
36
+ sql: string;
37
+ parameters: any[];
38
+ db: AbstractPowerSyncDatabase;
39
+ }
40
+ /**
41
+ * Similar to {@link CompatibleQuery}, except the `execute` method
42
+ * does not enforce an Array result type.
43
+ */
44
+ export interface WatchCompatibleQuery<ResultType> {
45
+ execute(options: WatchExecuteOptions): Promise<ResultType>;
46
+ compile(): CompiledQuery;
47
+ }
48
+ export interface WatchedQueryOptions {
49
+ /** The minimum interval between queries. */
50
+ throttleMs?: number;
51
+ /**
52
+ * If true (default) the watched query will update its state to report
53
+ * on the fetching state of the query.
54
+ * Setting to false reduces the number of state changes if the fetch status
55
+ * is not relevant to the consumer.
56
+ */
57
+ reportFetching?: boolean;
58
+ /**
59
+ * By default, watched queries requery the database on any change to any dependent table of the query.
60
+ * Supplying an override here can be used to limit the tables which trigger querying the database.
61
+ */
62
+ triggerOnTables?: string[];
63
+ }
64
+ export declare enum WatchedQueryListenerEvent {
65
+ ON_DATA = "onData",
66
+ ON_ERROR = "onError",
67
+ ON_STATE_CHANGE = "onStateChange",
68
+ CLOSED = "closed"
69
+ }
70
+ export interface WatchedQueryListener<Data> extends BaseListener {
71
+ [WatchedQueryListenerEvent.ON_DATA]?: (data: Data) => void | Promise<void>;
72
+ [WatchedQueryListenerEvent.ON_ERROR]?: (error: Error) => void | Promise<void>;
73
+ [WatchedQueryListenerEvent.ON_STATE_CHANGE]?: (state: WatchedQueryState<Data>) => void | Promise<void>;
74
+ [WatchedQueryListenerEvent.CLOSED]?: () => void | Promise<void>;
75
+ }
76
+ export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
77
+ export declare const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions;
78
+ export interface WatchedQuery<Data = unknown, Settings extends WatchedQueryOptions = WatchedQueryOptions, Listener extends WatchedQueryListener<Data> = WatchedQueryListener<Data>> extends MetaBaseObserverInterface<Listener> {
79
+ /**
80
+ * Current state of the watched query.
81
+ */
82
+ readonly state: WatchedQueryState<Data>;
83
+ readonly closed: boolean;
84
+ /**
85
+ * Subscribe to watched query events.
86
+ * @returns A function to unsubscribe from the events.
87
+ */
88
+ registerListener(listener: Listener): () => void;
89
+ /**
90
+ * Updates the underlying query options.
91
+ * This will trigger a re-evaluation of the query and update the state.
92
+ */
93
+ updateSettings(options: Settings): Promise<void>;
94
+ /**
95
+ * Close the watched query and end all subscriptions.
96
+ */
97
+ close(): Promise<void>;
98
+ }
@@ -0,0 +1,12 @@
1
+ export var WatchedQueryListenerEvent;
2
+ (function (WatchedQueryListenerEvent) {
3
+ WatchedQueryListenerEvent["ON_DATA"] = "onData";
4
+ WatchedQueryListenerEvent["ON_ERROR"] = "onError";
5
+ WatchedQueryListenerEvent["ON_STATE_CHANGE"] = "onStateChange";
6
+ WatchedQueryListenerEvent["CLOSED"] = "closed";
7
+ })(WatchedQueryListenerEvent || (WatchedQueryListenerEvent = {}));
8
+ export const DEFAULT_WATCH_THROTTLE_MS = 30;
9
+ export const DEFAULT_WATCH_QUERY_OPTIONS = {
10
+ throttleMs: DEFAULT_WATCH_THROTTLE_MS,
11
+ reportFetching: true
12
+ };
@@ -0,0 +1,67 @@
1
+ import { AbstractPowerSyncDatabase } from '../../../client/AbstractPowerSyncDatabase.js';
2
+ import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
3
+ import { WatchedQuery, WatchedQueryListener, WatchedQueryOptions, WatchedQueryState } from '../WatchedQuery.js';
4
+ /**
5
+ * @internal
6
+ */
7
+ export interface AbstractQueryProcessorOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
8
+ db: AbstractPowerSyncDatabase;
9
+ watchOptions: Settings;
10
+ placeholderData: Data;
11
+ }
12
+ /**
13
+ * @internal
14
+ */
15
+ export interface LinkQueryOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
16
+ abortSignal: AbortSignal;
17
+ settings: Settings;
18
+ }
19
+ type MutableDeep<T> = T extends ReadonlyArray<infer U> ? U[] : T;
20
+ /**
21
+ * @internal Mutable version of {@link WatchedQueryState}.
22
+ * This is used internally to allow updates to the state.
23
+ */
24
+ export type MutableWatchedQueryState<Data> = {
25
+ -readonly [P in keyof WatchedQueryState<Data>]: MutableDeep<WatchedQueryState<Data>[P]>;
26
+ };
27
+ type WatchedQueryProcessorListener<Data> = WatchedQueryListener<Data>;
28
+ /**
29
+ * Performs underlying watching and yields a stream of results.
30
+ * @internal
31
+ */
32
+ export declare abstract class AbstractQueryProcessor<Data = unknown[], Settings extends WatchedQueryOptions = WatchedQueryOptions> extends MetaBaseObserver<WatchedQueryProcessorListener<Data>> implements WatchedQuery<Data, Settings> {
33
+ protected options: AbstractQueryProcessorOptions<Data, Settings>;
34
+ readonly state: WatchedQueryState<Data>;
35
+ protected abortController: AbortController;
36
+ protected initialized: Promise<void>;
37
+ protected _closed: boolean;
38
+ protected disposeListeners: (() => void) | null;
39
+ get closed(): boolean;
40
+ constructor(options: AbstractQueryProcessorOptions<Data, Settings>);
41
+ protected constructInitialState(): WatchedQueryState<Data>;
42
+ protected get reportFetching(): boolean;
43
+ /**
44
+ * Updates the underlying query.
45
+ */
46
+ updateSettings(settings: Settings): Promise<void>;
47
+ /**
48
+ * This method is used to link a query to the subscribers of this listener class.
49
+ * This method should perform actual query watching and report results via {@link updateState} method.
50
+ */
51
+ protected abstract linkQuery(options: LinkQueryOptions<Data>): Promise<void>;
52
+ protected updateState(update: Partial<MutableWatchedQueryState<Data>>): Promise<void>;
53
+ /**
54
+ * Configures base DB listeners and links the query to listeners.
55
+ */
56
+ protected init(): Promise<void>;
57
+ close(): Promise<void>;
58
+ /**
59
+ * Runs a callback and reports errors to the error listeners.
60
+ */
61
+ protected runWithReporting<T>(callback: () => Promise<T>): Promise<void>;
62
+ /**
63
+ * Iterate listeners and reports errors to onError handlers.
64
+ */
65
+ protected iterateAsyncListenersWithError(callback: (listener: Partial<WatchedQueryProcessorListener<Data>>) => Promise<void> | void): Promise<void>;
66
+ }
67
+ export {};
@@ -0,0 +1,135 @@
1
+ import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
2
+ /**
3
+ * Performs underlying watching and yields a stream of results.
4
+ * @internal
5
+ */
6
+ export class AbstractQueryProcessor extends MetaBaseObserver {
7
+ options;
8
+ state;
9
+ abortController;
10
+ initialized;
11
+ _closed;
12
+ disposeListeners;
13
+ get closed() {
14
+ return this._closed;
15
+ }
16
+ constructor(options) {
17
+ super();
18
+ this.options = options;
19
+ this.abortController = new AbortController();
20
+ this._closed = false;
21
+ this.state = this.constructInitialState();
22
+ this.disposeListeners = null;
23
+ this.initialized = this.init();
24
+ }
25
+ constructInitialState() {
26
+ return {
27
+ isLoading: true,
28
+ isFetching: this.reportFetching, // Only set to true if we will report updates in future
29
+ error: null,
30
+ lastUpdated: null,
31
+ data: this.options.placeholderData
32
+ };
33
+ }
34
+ get reportFetching() {
35
+ return this.options.watchOptions.reportFetching ?? true;
36
+ }
37
+ /**
38
+ * Updates the underlying query.
39
+ */
40
+ async updateSettings(settings) {
41
+ await this.initialized;
42
+ if (!this.state.isFetching && this.reportFetching) {
43
+ await this.updateState({
44
+ isFetching: true
45
+ });
46
+ }
47
+ this.options.watchOptions = settings;
48
+ this.abortController.abort();
49
+ this.abortController = new AbortController();
50
+ await this.runWithReporting(() => this.linkQuery({
51
+ abortSignal: this.abortController.signal,
52
+ settings
53
+ }));
54
+ }
55
+ async updateState(update) {
56
+ if (typeof update.error !== 'undefined') {
57
+ await this.iterateAsyncListenersWithError(async (l) => l.onError?.(update.error));
58
+ // An error always stops for the current fetching state
59
+ update.isFetching = false;
60
+ update.isLoading = false;
61
+ }
62
+ Object.assign(this.state, { lastUpdated: new Date() }, update);
63
+ if (typeof update.data !== 'undefined') {
64
+ await this.iterateAsyncListenersWithError(async (l) => l.onData?.(this.state.data));
65
+ }
66
+ await this.iterateAsyncListenersWithError(async (l) => l.onStateChange?.(this.state));
67
+ }
68
+ /**
69
+ * Configures base DB listeners and links the query to listeners.
70
+ */
71
+ async init() {
72
+ const { db } = this.options;
73
+ const disposeCloseListener = db.registerListener({
74
+ closing: async () => {
75
+ await this.close();
76
+ }
77
+ });
78
+ // Wait for the schema to be set before listening to changes
79
+ await db.waitForReady();
80
+ const disposeSchemaListener = db.registerListener({
81
+ schemaChanged: async () => {
82
+ await this.runWithReporting(async () => {
83
+ await this.updateSettings(this.options.watchOptions);
84
+ });
85
+ }
86
+ });
87
+ this.disposeListeners = () => {
88
+ disposeCloseListener();
89
+ disposeSchemaListener();
90
+ };
91
+ // Initial setup
92
+ this.runWithReporting(async () => {
93
+ await this.updateSettings(this.options.watchOptions);
94
+ });
95
+ }
96
+ async close() {
97
+ await this.initialized;
98
+ this.abortController.abort();
99
+ this.disposeListeners?.();
100
+ this.disposeListeners = null;
101
+ this._closed = true;
102
+ this.iterateListeners((l) => l.closed?.());
103
+ this.listeners.clear();
104
+ }
105
+ /**
106
+ * Runs a callback and reports errors to the error listeners.
107
+ */
108
+ async runWithReporting(callback) {
109
+ try {
110
+ await callback();
111
+ }
112
+ catch (error) {
113
+ // This will update the error on the state and iterate error listeners
114
+ await this.updateState({ error });
115
+ }
116
+ }
117
+ /**
118
+ * Iterate listeners and reports errors to onError handlers.
119
+ */
120
+ async iterateAsyncListenersWithError(callback) {
121
+ try {
122
+ await this.iterateAsyncListeners(async (l) => callback(l));
123
+ }
124
+ catch (error) {
125
+ try {
126
+ await this.iterateAsyncListeners(async (l) => l.onError?.(error));
127
+ }
128
+ catch (error) {
129
+ // Errors here are ignored
130
+ // since we are already in an error state
131
+ this.options.db.logger.error('Watched query error handler threw an Error', error);
132
+ }
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,121 @@
1
+ import { WatchCompatibleQuery, WatchedQuery, WatchedQueryListener, WatchedQueryOptions } from '../WatchedQuery.js';
2
+ import { AbstractQueryProcessor, AbstractQueryProcessorOptions, LinkQueryOptions } from './AbstractQueryProcessor.js';
3
+ /**
4
+ * Represents an updated row in a differential watched query.
5
+ * It contains both the current and previous state of the row.
6
+ */
7
+ export interface WatchedQueryRowDifferential<RowType> {
8
+ readonly current: RowType;
9
+ readonly previous: RowType;
10
+ }
11
+ /**
12
+ * Represents the result of a watched query that has been diffed.
13
+ * {@link DifferentialWatchedQueryState#diff} is of the {@link WatchedQueryDifferential} form.
14
+ */
15
+ export interface WatchedQueryDifferential<RowType> {
16
+ readonly added: ReadonlyArray<Readonly<RowType>>;
17
+ /**
18
+ * The entire current result set.
19
+ * Array item object references are preserved between updates if the item is unchanged.
20
+ *
21
+ * e.g. In the query
22
+ * ```sql
23
+ * SELECT name, make FROM assets ORDER BY make ASC;
24
+ * ```
25
+ *
26
+ * If a previous result set contains an item (A) `{name: 'pc', make: 'Cool PC'}` and
27
+ * an update has been made which adds another item (B) to the result set (the item A is unchanged) - then
28
+ * the updated result set will be contain the same object reference, to item A, as the previous result set.
29
+ * This is regardless of the item A's position in the updated result set.
30
+ */
31
+ readonly all: ReadonlyArray<Readonly<RowType>>;
32
+ readonly removed: ReadonlyArray<Readonly<RowType>>;
33
+ readonly updated: ReadonlyArray<WatchedQueryRowDifferential<Readonly<RowType>>>;
34
+ readonly unchanged: ReadonlyArray<Readonly<RowType>>;
35
+ }
36
+ /**
37
+ * Row comparator for differentially watched queries which keys and compares items in the result set.
38
+ */
39
+ export interface DifferentialWatchedQueryComparator<RowType> {
40
+ /**
41
+ * Generates a unique key for the item.
42
+ */
43
+ keyBy: (item: RowType) => string;
44
+ /**
45
+ * Generates a token for comparing items with matching keys.
46
+ */
47
+ compareBy: (item: RowType) => string;
48
+ }
49
+ /**
50
+ * Options for building a differential watched query with the {@link Query} builder.
51
+ */
52
+ export interface DifferentialWatchedQueryOptions<RowType> extends WatchedQueryOptions {
53
+ /**
54
+ * Initial result data which is presented while the initial loading is executing.
55
+ */
56
+ placeholderData?: RowType[];
57
+ /**
58
+ * Row comparator used to identify and compare rows in the result set.
59
+ * If not provided, the default comparator will be used which keys items by their `id` property if available,
60
+ * otherwise it uses JSON stringification of the entire item for keying and comparison.
61
+ * @defaultValue {@link DEFAULT_ROW_COMPARATOR}
62
+ */
63
+ rowComparator?: DifferentialWatchedQueryComparator<RowType>;
64
+ }
65
+ /**
66
+ * Settings for differential incremental watched queries using.
67
+ */
68
+ export interface DifferentialWatchedQuerySettings<RowType> extends DifferentialWatchedQueryOptions<RowType> {
69
+ /**
70
+ * The query here must return an array of items that can be differentiated.
71
+ */
72
+ query: WatchCompatibleQuery<RowType[]>;
73
+ }
74
+ export interface DifferentialWatchedQueryListener<RowType> extends WatchedQueryListener<ReadonlyArray<Readonly<RowType>>> {
75
+ onDiff?: (diff: WatchedQueryDifferential<RowType>) => void | Promise<void>;
76
+ }
77
+ export type DifferentialWatchedQuery<RowType> = WatchedQuery<ReadonlyArray<Readonly<RowType>>, DifferentialWatchedQuerySettings<RowType>, DifferentialWatchedQueryListener<RowType>>;
78
+ /**
79
+ * @internal
80
+ */
81
+ export interface DifferentialQueryProcessorOptions<RowType> extends AbstractQueryProcessorOptions<RowType[], DifferentialWatchedQuerySettings<RowType>> {
82
+ rowComparator?: DifferentialWatchedQueryComparator<RowType>;
83
+ }
84
+ type DataHashMap<RowType> = Map<string, {
85
+ hash: string;
86
+ item: RowType;
87
+ }>;
88
+ /**
89
+ * An empty differential result set.
90
+ * This is used as the initial state for differential incrementally watched queries.
91
+ */
92
+ export declare const EMPTY_DIFFERENTIAL: {
93
+ added: never[];
94
+ all: never[];
95
+ removed: never[];
96
+ updated: never[];
97
+ unchanged: never[];
98
+ };
99
+ /**
100
+ * Default implementation of the {@link DifferentialWatchedQueryComparator} for watched queries.
101
+ * It keys items by their `id` property if available, alternatively it uses JSON stringification
102
+ * of the entire item for the key and comparison.
103
+ */
104
+ export declare const DEFAULT_ROW_COMPARATOR: DifferentialWatchedQueryComparator<any>;
105
+ /**
106
+ * Uses the PowerSync onChange event to trigger watched queries.
107
+ * Results are emitted on every change of the relevant tables.
108
+ * @internal
109
+ */
110
+ export declare class DifferentialQueryProcessor<RowType> extends AbstractQueryProcessor<ReadonlyArray<Readonly<RowType>>, DifferentialWatchedQuerySettings<RowType>> implements DifferentialWatchedQuery<RowType> {
111
+ protected options: DifferentialQueryProcessorOptions<RowType>;
112
+ protected comparator: DifferentialWatchedQueryComparator<RowType>;
113
+ constructor(options: DifferentialQueryProcessorOptions<RowType>);
114
+ protected differentiate(current: RowType[], previousMap: DataHashMap<RowType>): {
115
+ diff: WatchedQueryDifferential<RowType>;
116
+ map: DataHashMap<RowType>;
117
+ hasChanged: boolean;
118
+ };
119
+ protected linkQuery(options: LinkQueryOptions<WatchedQueryDifferential<RowType>>): Promise<void>;
120
+ }
121
+ export {};
@@ -0,0 +1,166 @@
1
+ import { AbstractQueryProcessor } from './AbstractQueryProcessor.js';
2
+ /**
3
+ * An empty differential result set.
4
+ * This is used as the initial state for differential incrementally watched queries.
5
+ */
6
+ export const EMPTY_DIFFERENTIAL = {
7
+ added: [],
8
+ all: [],
9
+ removed: [],
10
+ updated: [],
11
+ unchanged: []
12
+ };
13
+ /**
14
+ * Default implementation of the {@link DifferentialWatchedQueryComparator} for watched queries.
15
+ * It keys items by their `id` property if available, alternatively it uses JSON stringification
16
+ * of the entire item for the key and comparison.
17
+ */
18
+ export const DEFAULT_ROW_COMPARATOR = {
19
+ keyBy: (item) => {
20
+ if (item && typeof item == 'object' && typeof item['id'] == 'string') {
21
+ return item['id'];
22
+ }
23
+ return JSON.stringify(item);
24
+ },
25
+ compareBy: (item) => JSON.stringify(item)
26
+ };
27
+ /**
28
+ * Uses the PowerSync onChange event to trigger watched queries.
29
+ * Results are emitted on every change of the relevant tables.
30
+ * @internal
31
+ */
32
+ export class DifferentialQueryProcessor extends AbstractQueryProcessor {
33
+ options;
34
+ comparator;
35
+ constructor(options) {
36
+ super(options);
37
+ this.options = options;
38
+ this.comparator = options.rowComparator ?? DEFAULT_ROW_COMPARATOR;
39
+ }
40
+ /*
41
+ * @returns If the sets are equal
42
+ */
43
+ differentiate(current, previousMap) {
44
+ const { keyBy, compareBy } = this.comparator;
45
+ let hasChanged = false;
46
+ const currentMap = new Map();
47
+ const removedTracker = new Set(previousMap.keys());
48
+ // Allow mutating to populate the data temporarily.
49
+ const diff = {
50
+ all: [],
51
+ added: [],
52
+ removed: [],
53
+ updated: [],
54
+ unchanged: []
55
+ };
56
+ /**
57
+ * Looping over the current result set array is important to preserve
58
+ * the ordering of the result set.
59
+ * We can replace items in the current array with previous object references if they are equal.
60
+ */
61
+ for (const item of current) {
62
+ const key = keyBy(item);
63
+ const hash = compareBy(item);
64
+ currentMap.set(key, { hash, item });
65
+ const previousItem = previousMap.get(key);
66
+ if (!previousItem) {
67
+ // New item
68
+ hasChanged = true;
69
+ diff.added.push(item);
70
+ diff.all.push(item);
71
+ }
72
+ else {
73
+ // Existing item
74
+ if (hash == previousItem.hash) {
75
+ diff.unchanged.push(previousItem.item);
76
+ // Use the previous object reference
77
+ diff.all.push(previousItem.item);
78
+ // update the map to preserve the reference
79
+ currentMap.set(key, previousItem);
80
+ }
81
+ else {
82
+ hasChanged = true;
83
+ diff.updated.push({ current: item, previous: previousItem.item });
84
+ // Use the new reference
85
+ diff.all.push(item);
86
+ }
87
+ }
88
+ // The item is present, we don't consider it removed
89
+ removedTracker.delete(key);
90
+ }
91
+ diff.removed = Array.from(removedTracker).map((key) => previousMap.get(key).item);
92
+ hasChanged = hasChanged || diff.removed.length > 0;
93
+ return {
94
+ diff,
95
+ hasChanged,
96
+ map: currentMap
97
+ };
98
+ }
99
+ async linkQuery(options) {
100
+ const { db, watchOptions } = this.options;
101
+ const { abortSignal } = options;
102
+ const compiledQuery = watchOptions.query.compile();
103
+ const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters, {
104
+ tables: options.settings.triggerOnTables
105
+ });
106
+ let currentMap = new Map();
107
+ // populate the currentMap from the placeholder data
108
+ this.state.data.forEach((item) => {
109
+ currentMap.set(this.comparator.keyBy(item), {
110
+ hash: this.comparator.compareBy(item),
111
+ item
112
+ });
113
+ });
114
+ db.onChangeWithCallback({
115
+ onChange: async () => {
116
+ if (this.closed) {
117
+ return;
118
+ }
119
+ // This fires for each change of the relevant tables
120
+ try {
121
+ if (this.reportFetching && !this.state.isFetching) {
122
+ await this.updateState({ isFetching: true });
123
+ }
124
+ const partialStateUpdate = {};
125
+ // Always run the query if an underlying table has changed
126
+ const result = await watchOptions.query.execute({
127
+ sql: compiledQuery.sql,
128
+ // Allows casting from ReadOnlyArray[unknown] to Array<unknown>
129
+ // This allows simpler compatibility with PowerSync queries
130
+ parameters: [...compiledQuery.parameters],
131
+ db: this.options.db
132
+ });
133
+ if (this.reportFetching) {
134
+ partialStateUpdate.isFetching = false;
135
+ }
136
+ if (this.state.isLoading) {
137
+ partialStateUpdate.isLoading = false;
138
+ }
139
+ const { diff, hasChanged, map } = this.differentiate(result, currentMap);
140
+ // Update for future comparisons
141
+ currentMap = map;
142
+ if (hasChanged) {
143
+ await this.iterateAsyncListenersWithError((l) => l.onDiff?.(diff));
144
+ Object.assign(partialStateUpdate, {
145
+ data: diff.all
146
+ });
147
+ }
148
+ if (Object.keys(partialStateUpdate).length > 0) {
149
+ await this.updateState(partialStateUpdate);
150
+ }
151
+ }
152
+ catch (error) {
153
+ await this.updateState({ error });
154
+ }
155
+ },
156
+ onError: async (error) => {
157
+ await this.updateState({ error });
158
+ }
159
+ }, {
160
+ signal: abortSignal,
161
+ tables,
162
+ throttleMs: watchOptions.throttleMs,
163
+ triggerImmediate: true // used to emit the initial state
164
+ });
165
+ }
166
+ }