@tanstack/powersync-db-collection 0.1.66 → 0.1.69
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.
- package/dist/cjs/PowerSyncTransactor.cjs +16 -1
- package/dist/cjs/PowerSyncTransactor.cjs.map +1 -1
- package/dist/cjs/powersync.cjs +230 -90
- package/dist/cjs/powersync.cjs.map +1 -1
- package/dist/esm/PowerSyncTransactor.js +16 -1
- package/dist/esm/PowerSyncTransactor.js.map +1 -1
- package/dist/esm/powersync.js +230 -90
- package/dist/esm/powersync.js.map +1 -1
- package/package.json +2 -2
- package/src/PowerSyncTransactor.ts +24 -1
- package/src/powersync.ts +305 -126
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const common = require("@powersync/common");
|
|
4
|
+
const db = require("@tanstack/db");
|
|
4
5
|
const DebugModule = require("debug");
|
|
5
6
|
const PendingOperationStore = require("./PendingOperationStore.cjs");
|
|
6
7
|
const helpers = require("./helpers.cjs");
|
|
@@ -35,7 +36,21 @@ class PowerSyncTransactor {
|
|
|
35
36
|
if (collection.isReady()) {
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
await new Promise((resolve) =>
|
|
39
|
+
await new Promise((resolve, reject) => {
|
|
40
|
+
const check = () => {
|
|
41
|
+
if (collection.isReady()) {
|
|
42
|
+
unsubscribe();
|
|
43
|
+
resolve();
|
|
44
|
+
} else if (collection.status === `error` || collection.status === `cleaned-up`) {
|
|
45
|
+
unsubscribe();
|
|
46
|
+
reject(
|
|
47
|
+
collection.status === `error` ? collection._lifecycle.getSyncError() ?? new Error(`Collection failed before readiness`) : new db.LoadSubsetOperationAbortedError()
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const unsubscribe = collection.on(`status:change`, check);
|
|
52
|
+
check();
|
|
53
|
+
});
|
|
39
54
|
})
|
|
40
55
|
);
|
|
41
56
|
const { whenComplete } = await this.database.writeTransaction(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PowerSyncTransactor.cjs","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n await new Promise<void>((resolve) => collection.onFirstReady(resolve))\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName}\n (${keys.join(`, `)})\n VALUES\n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName}\n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY operation_id DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["PendingOperationStore","mutation","sanitizeSQL","asPowerSyncRecord","mapOperationToPowerSync"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PowerSyncTransactor.cjs","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport { LoadSubsetOperationAbortedError } from '@tanstack/db'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n // Observe this session without starting new demand from mutationFn.\n // Cleanup and startup failure must settle the wait before taking a lock.\n await new Promise<void>((resolve, reject) => {\n const check = () => {\n if (collection.isReady()) {\n unsubscribe()\n resolve()\n } else if (\n collection.status === `error` ||\n collection.status === `cleaned-up`\n ) {\n unsubscribe()\n reject(\n collection.status === `error`\n ? (collection._lifecycle.getSyncError() ??\n new Error(`Collection failed before readiness`))\n : new LoadSubsetOperationAbortedError(),\n )\n }\n }\n const unsubscribe = collection.on(`status:change`, check)\n check()\n })\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName}\n (${keys.join(`, `)})\n VALUES\n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName}\n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY operation_id DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["PendingOperationStore","LoadSubsetOperationAbortedError","mutation","sanitizeSQL","asPowerSyncRecord","mapOperationToPowerSync"],"mappings":"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"}
|
package/dist/cjs/powersync.cjs
CHANGED
|
@@ -59,12 +59,17 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
59
59
|
const { begin, write, collection, commit, markReady, markError } = params;
|
|
60
60
|
const abortController = new AbortController();
|
|
61
61
|
let disposeTracking = null;
|
|
62
|
+
let trackingSetup = null;
|
|
62
63
|
if (syncMode === `eager`) {
|
|
63
64
|
return runEagerSync();
|
|
64
65
|
} else {
|
|
65
66
|
return runOnDemandSync();
|
|
66
67
|
}
|
|
67
68
|
async function safelyDisposeTracking(context) {
|
|
69
|
+
const setup = trackingSetup;
|
|
70
|
+
if (setup) {
|
|
71
|
+
await setup.catch(() => void 0);
|
|
72
|
+
}
|
|
68
73
|
const dispose = disposeTracking;
|
|
69
74
|
if (!dispose) {
|
|
70
75
|
return;
|
|
@@ -72,6 +77,20 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
72
77
|
disposeTracking = null;
|
|
73
78
|
await dispose(context ? { context } : void 0);
|
|
74
79
|
}
|
|
80
|
+
async function establishTracking(options, appliedReceipts) {
|
|
81
|
+
const setup = (async () => {
|
|
82
|
+
const dispose = await createDiffTrigger(options, appliedReceipts);
|
|
83
|
+
disposeTracking = dispose;
|
|
84
|
+
})();
|
|
85
|
+
trackingSetup = setup;
|
|
86
|
+
try {
|
|
87
|
+
await setup;
|
|
88
|
+
} finally {
|
|
89
|
+
if (trackingSetup === setup) {
|
|
90
|
+
trackingSetup = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
75
94
|
async function createDiffTrigger(options, appliedReceipts) {
|
|
76
95
|
const { setupContext, immediate, when, writeType, batchQuery } = options;
|
|
77
96
|
return await database.triggers.createDiffTrigger({
|
|
@@ -108,6 +127,13 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
108
127
|
});
|
|
109
128
|
}
|
|
110
129
|
async function flushDiffRecords() {
|
|
130
|
+
const setup = trackingSetup;
|
|
131
|
+
if (setup) {
|
|
132
|
+
await setup.catch(() => void 0);
|
|
133
|
+
}
|
|
134
|
+
if (!disposeTracking) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
111
137
|
const ignoredReceipts = [];
|
|
112
138
|
await database.writeTransaction(async (context) => {
|
|
113
139
|
await flushDiffRecordsWithContext(context, ignoredReceipts);
|
|
@@ -193,9 +219,14 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
193
219
|
function runEagerSync() {
|
|
194
220
|
let onUnload = null;
|
|
195
221
|
start(async () => {
|
|
196
|
-
|
|
222
|
+
const cleanup = await restConfig.onLoad?.();
|
|
223
|
+
if (abortController.signal.aborted) {
|
|
224
|
+
cleanup?.();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
onUnload = cleanup;
|
|
197
228
|
const appliedReceipts = [];
|
|
198
|
-
|
|
229
|
+
await establishTracking(
|
|
199
230
|
{
|
|
200
231
|
// Initial eager hydration must make the source usable before
|
|
201
232
|
// PowerSync can persist a mutation queued during startup.
|
|
@@ -233,81 +264,125 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
233
264
|
};
|
|
234
265
|
}
|
|
235
266
|
function runOnDemandSync() {
|
|
236
|
-
const
|
|
267
|
+
const demands = /* @__PURE__ */ new Map();
|
|
237
268
|
const releasedSubsets = /* @__PURE__ */ new WeakSet();
|
|
269
|
+
const pendingReleases = [];
|
|
238
270
|
let stopped = false;
|
|
239
|
-
|
|
240
|
-
|
|
271
|
+
let trackingRevision = 0;
|
|
272
|
+
let reconciledTrackingRevision = 0;
|
|
273
|
+
let rebuildPromise = null;
|
|
274
|
+
let drainingReleases = false;
|
|
275
|
+
let releaseRetryTimer;
|
|
276
|
+
const startup = start();
|
|
277
|
+
void startup.catch(
|
|
241
278
|
(error) => database.logger.error(
|
|
242
279
|
`Could not start syncing process for ${viewName} into ${trackedTableName}`,
|
|
243
280
|
error
|
|
244
281
|
)
|
|
245
282
|
);
|
|
246
|
-
const activeWhereExpressions =
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const cleanup = await restConfig.onLoadSubset?.(options);
|
|
253
|
-
if (hasStopped()) {
|
|
254
|
-
cleanup?.();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (cleanup) {
|
|
258
|
-
if (releasedSubsets.has(options) || options.signal?.aborted) {
|
|
259
|
-
cleanup();
|
|
260
|
-
} else {
|
|
261
|
-
unloadSubsetCallbacks.set(options, cleanup);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (activeWhereExpressions.length === 0) {
|
|
283
|
+
const activeWhereExpressions = () => Array.from(demands.values()).filter((demand) => demand.active).map((demand) => demand.options.where);
|
|
284
|
+
const reconcileTracking = async () => {
|
|
285
|
+
while (!stopped && reconciledTrackingRevision !== trackingRevision) {
|
|
286
|
+
const revision = trackingRevision;
|
|
287
|
+
const isCurrent = () => !stopped && trackingRevision === revision;
|
|
288
|
+
const appliedReceipts = [];
|
|
266
289
|
await database.writeLock(async (ctx) => {
|
|
290
|
+
if (!isCurrent()) return;
|
|
267
291
|
await flushDiffRecordsWithContext(ctx, appliedReceipts);
|
|
292
|
+
if (!isCurrent()) return;
|
|
268
293
|
await safelyDisposeTracking(ctx);
|
|
294
|
+
if (!isCurrent()) return;
|
|
295
|
+
const active = activeWhereExpressions();
|
|
296
|
+
const missing = collection.status === `error` ? new Set(collection.keys()) : void 0;
|
|
297
|
+
if (active.length > 0) {
|
|
298
|
+
const combinedWhere = active.length === 1 ? active[0] : db.or(active[0], active[1], ...active.slice(2));
|
|
299
|
+
const compiledNewData = sqliteCompiler.compileSQLite(
|
|
300
|
+
{ where: combinedWhere },
|
|
301
|
+
{ jsonColumn: "NEW.data" }
|
|
302
|
+
);
|
|
303
|
+
const compiledOldData = sqliteCompiler.compileSQLite(
|
|
304
|
+
{ where: combinedWhere },
|
|
305
|
+
{ jsonColumn: "OLD.data" }
|
|
306
|
+
);
|
|
307
|
+
const compiledView = sqliteCompiler.compileSQLite({ where: combinedWhere });
|
|
308
|
+
const newDataWhenClause = toInlinedWhereClause(compiledNewData);
|
|
309
|
+
const oldDataWhenClause = toInlinedWhereClause(compiledOldData);
|
|
310
|
+
const viewWhereClause = toInlinedWhereClause(compiledView);
|
|
311
|
+
await establishTracking(
|
|
312
|
+
{
|
|
313
|
+
setupContext: ctx,
|
|
314
|
+
when: {
|
|
315
|
+
[common.DiffTriggerOperation.INSERT]: newDataWhenClause,
|
|
316
|
+
[common.DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
|
|
317
|
+
[common.DiffTriggerOperation.DELETE]: oldDataWhenClause
|
|
318
|
+
},
|
|
319
|
+
writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
|
|
320
|
+
batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
|
|
321
|
+
`SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
|
|
322
|
+
[batchSize, cursor]
|
|
323
|
+
).then((rows) => {
|
|
324
|
+
for (const row of rows) missing?.delete(row.id);
|
|
325
|
+
return rows;
|
|
326
|
+
})
|
|
327
|
+
},
|
|
328
|
+
appliedReceipts
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (!isCurrent()) await safelyDisposeTracking(ctx);
|
|
332
|
+
else if (missing?.size) {
|
|
333
|
+
begin();
|
|
334
|
+
for (const key of missing) write({ type: `delete`, key });
|
|
335
|
+
appliedReceipts.push(commit());
|
|
336
|
+
}
|
|
269
337
|
});
|
|
270
338
|
await Promise.all(appliedReceipts);
|
|
339
|
+
if (isCurrent()) {
|
|
340
|
+
reconciledTrackingRevision = revision;
|
|
341
|
+
if (collection.status === `error`) markReady();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const rebuildTracking = async () => {
|
|
346
|
+
while (!stopped && reconciledTrackingRevision !== trackingRevision) {
|
|
347
|
+
await (rebuildPromise ??= reconcileTracking().catch((error) => {
|
|
348
|
+
if (!stopped) markError(error);
|
|
349
|
+
throw error;
|
|
350
|
+
}).finally(() => {
|
|
351
|
+
rebuildPromise = null;
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const loadSubset = async (options) => {
|
|
356
|
+
if (stopped) return;
|
|
357
|
+
await startup;
|
|
358
|
+
if (
|
|
359
|
+
// Cleanup can run while startup is pending.
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
361
|
+
stopped || releasedSubsets.has(options) || options.signal?.aborted
|
|
362
|
+
) {
|
|
271
363
|
return;
|
|
272
364
|
}
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
{
|
|
295
|
-
setupContext: ctx,
|
|
296
|
-
when: {
|
|
297
|
-
[common.DiffTriggerOperation.INSERT]: newDataWhenClause,
|
|
298
|
-
[common.DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
|
|
299
|
-
[common.DiffTriggerOperation.DELETE]: oldDataWhenClause
|
|
300
|
-
},
|
|
301
|
-
writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
|
|
302
|
-
batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
|
|
303
|
-
`SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
|
|
304
|
-
[batchSize, cursor]
|
|
305
|
-
)
|
|
306
|
-
},
|
|
307
|
-
appliedReceipts
|
|
308
|
-
);
|
|
309
|
-
});
|
|
310
|
-
await Promise.all(appliedReceipts);
|
|
365
|
+
const demand = { options, active: false };
|
|
366
|
+
demands.set(options, demand);
|
|
367
|
+
try {
|
|
368
|
+
const cleanup = await restConfig.onLoadSubset?.(options);
|
|
369
|
+
if (cleanup) demand.cleanup = cleanup;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
demands.delete(options);
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
if (
|
|
375
|
+
// The user hook can reenter cleanup.
|
|
376
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
377
|
+
stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
|
|
378
|
+
) {
|
|
379
|
+
demands.delete(options);
|
|
380
|
+
demand.cleanup?.();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
demand.active = true;
|
|
384
|
+
trackingRevision++;
|
|
385
|
+
await rebuildTracking();
|
|
311
386
|
};
|
|
312
387
|
const toInlinedWhereClause = (compiled) => {
|
|
313
388
|
if (!compiled.where) return "TRUE";
|
|
@@ -317,32 +392,40 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
317
392
|
...compiled.params
|
|
318
393
|
);
|
|
319
394
|
};
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
395
|
+
const cleanupDemand = (demand) => {
|
|
396
|
+
demands.delete(demand.options);
|
|
397
|
+
try {
|
|
398
|
+
demand.cleanup?.();
|
|
399
|
+
} catch (error) {
|
|
400
|
+
database.logger.error(
|
|
401
|
+
`Could not clean up subset hook for ${viewName}`,
|
|
402
|
+
error
|
|
403
|
+
);
|
|
327
404
|
}
|
|
405
|
+
};
|
|
406
|
+
const performPhysicalRelease = async (options) => {
|
|
328
407
|
const compiledDeparting = sqliteCompiler.compileSQLite({ where: options.where });
|
|
329
408
|
const departingWhereSQL = toInlinedWhereClause(compiledDeparting);
|
|
330
|
-
let
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
409
|
+
let rowsToEvict;
|
|
410
|
+
for (; ; ) {
|
|
411
|
+
if (stopped) return;
|
|
412
|
+
const revision = trackingRevision;
|
|
413
|
+
const active = activeWhereExpressions();
|
|
414
|
+
let evictionSQL;
|
|
415
|
+
if (active.length === 0) {
|
|
416
|
+
evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`;
|
|
417
|
+
} else {
|
|
418
|
+
const combinedRemaining = active.length === 1 ? active[0] : db.or(active[0], active[1], ...active.slice(2));
|
|
419
|
+
const compiledRemaining = sqliteCompiler.compileSQLite({
|
|
420
|
+
where: combinedRemaining
|
|
421
|
+
});
|
|
422
|
+
const remainingWhereSQL = toInlinedWhereClause(compiledRemaining);
|
|
423
|
+
evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`;
|
|
424
|
+
}
|
|
425
|
+
rowsToEvict = await database.getAll(evictionSQL);
|
|
426
|
+
if (stopped) return;
|
|
427
|
+
if (trackingRevision === revision) break;
|
|
344
428
|
}
|
|
345
|
-
const rowsToEvict = await database.getAll(evictionSQL);
|
|
346
429
|
if (rowsToEvict.length > 0) {
|
|
347
430
|
begin();
|
|
348
431
|
for (const { id } of rowsToEvict) {
|
|
@@ -350,22 +433,79 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
350
433
|
}
|
|
351
434
|
void commit();
|
|
352
435
|
}
|
|
353
|
-
await
|
|
436
|
+
await rebuildTracking();
|
|
437
|
+
};
|
|
438
|
+
function scheduleReleaseDrain(delay = 0) {
|
|
439
|
+
if (stopped || drainingReleases || releaseRetryTimer) return;
|
|
440
|
+
if (delay > 0) {
|
|
441
|
+
releaseRetryTimer = setTimeout(() => {
|
|
442
|
+
releaseRetryTimer = void 0;
|
|
443
|
+
void drainReleases();
|
|
444
|
+
}, delay);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
void drainReleases();
|
|
448
|
+
}
|
|
449
|
+
async function drainReleases() {
|
|
450
|
+
if (stopped || drainingReleases) return;
|
|
451
|
+
drainingReleases = true;
|
|
452
|
+
let retryDelay = 0;
|
|
453
|
+
try {
|
|
454
|
+
const attempts = pendingReleases.length;
|
|
455
|
+
for (let index = 0; !stopped && index < attempts; index++) {
|
|
456
|
+
const pending = pendingReleases.shift();
|
|
457
|
+
try {
|
|
458
|
+
await performPhysicalRelease(pending.options);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
pending.failures++;
|
|
461
|
+
pendingReleases.push(pending);
|
|
462
|
+
const delay = Math.min(
|
|
463
|
+
1e3 * 2 ** (pending.failures - 1),
|
|
464
|
+
3e4
|
|
465
|
+
);
|
|
466
|
+
retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
|
|
467
|
+
database.logger.error(
|
|
468
|
+
`Could not release subset tracking for ${viewName}; retrying`,
|
|
469
|
+
error
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} finally {
|
|
474
|
+
drainingReleases = false;
|
|
475
|
+
}
|
|
476
|
+
if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay);
|
|
477
|
+
}
|
|
478
|
+
const unloadSubset = (options) => {
|
|
479
|
+
releasedSubsets.add(options);
|
|
480
|
+
const demand = demands.get(options);
|
|
481
|
+
if (!demand) return;
|
|
482
|
+
const wasActive = demand.active;
|
|
483
|
+
if (wasActive) trackingRevision++;
|
|
484
|
+
cleanupDemand(demand);
|
|
485
|
+
if (wasActive) {
|
|
486
|
+
pendingReleases.push({ options, failures: 0 });
|
|
487
|
+
clearTimeout(releaseRetryTimer);
|
|
488
|
+
releaseRetryTimer = void 0;
|
|
489
|
+
scheduleReleaseDrain();
|
|
490
|
+
}
|
|
354
491
|
};
|
|
355
492
|
markReady();
|
|
356
493
|
return {
|
|
357
494
|
cleanup: () => {
|
|
358
495
|
stopped = true;
|
|
496
|
+
clearTimeout(releaseRetryTimer);
|
|
497
|
+
releaseRetryTimer = void 0;
|
|
359
498
|
database.logger.info(
|
|
360
499
|
`Sync has been stopped for ${viewName} into ${trackedTableName}`
|
|
361
500
|
);
|
|
362
501
|
abortController.abort();
|
|
363
|
-
for (const
|
|
364
|
-
|
|
365
|
-
|
|
502
|
+
for (const demand of demands.values()) {
|
|
503
|
+
cleanupDemand(demand);
|
|
504
|
+
}
|
|
505
|
+
pendingReleases.length = 0;
|
|
366
506
|
},
|
|
367
507
|
loadSubset: (options) => loadSubset(options),
|
|
368
|
-
unloadSubset
|
|
508
|
+
unloadSubset
|
|
369
509
|
};
|
|
370
510
|
}
|
|
371
511
|
},
|