@tanstack/powersync-db-collection 0.1.65 → 0.1.67
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 +219 -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 +219 -90
- package/dist/esm/powersync.js.map +1 -1
- package/package.json +2 -2
- package/src/PowerSyncTransactor.ts +24 -1
- package/src/powersync.ts +286 -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,114 @@ 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
|
+
if (active.length === 0) return;
|
|
297
|
+
const combinedWhere = active.length === 1 ? active[0] : db.or(active[0], active[1], ...active.slice(2));
|
|
298
|
+
const compiledNewData = sqliteCompiler.compileSQLite(
|
|
299
|
+
{ where: combinedWhere },
|
|
300
|
+
{ jsonColumn: "NEW.data" }
|
|
301
|
+
);
|
|
302
|
+
const compiledOldData = sqliteCompiler.compileSQLite(
|
|
303
|
+
{ where: combinedWhere },
|
|
304
|
+
{ jsonColumn: "OLD.data" }
|
|
305
|
+
);
|
|
306
|
+
const compiledView = sqliteCompiler.compileSQLite({ where: combinedWhere });
|
|
307
|
+
const newDataWhenClause = toInlinedWhereClause(compiledNewData);
|
|
308
|
+
const oldDataWhenClause = toInlinedWhereClause(compiledOldData);
|
|
309
|
+
const viewWhereClause = toInlinedWhereClause(compiledView);
|
|
310
|
+
await establishTracking(
|
|
311
|
+
{
|
|
312
|
+
setupContext: ctx,
|
|
313
|
+
when: {
|
|
314
|
+
[common.DiffTriggerOperation.INSERT]: newDataWhenClause,
|
|
315
|
+
[common.DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
|
|
316
|
+
[common.DiffTriggerOperation.DELETE]: oldDataWhenClause
|
|
317
|
+
},
|
|
318
|
+
writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
|
|
319
|
+
batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
|
|
320
|
+
`SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
|
|
321
|
+
[batchSize, cursor]
|
|
322
|
+
)
|
|
323
|
+
},
|
|
324
|
+
appliedReceipts
|
|
325
|
+
);
|
|
326
|
+
if (!isCurrent()) await safelyDisposeTracking(ctx);
|
|
269
327
|
});
|
|
270
328
|
await Promise.all(appliedReceipts);
|
|
329
|
+
if (isCurrent()) {
|
|
330
|
+
reconciledTrackingRevision = revision;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const rebuildTracking = async () => {
|
|
335
|
+
while (!stopped && reconciledTrackingRevision !== trackingRevision) {
|
|
336
|
+
await (rebuildPromise ??= reconcileTracking().catch((error) => {
|
|
337
|
+
if (!stopped) markError(error);
|
|
338
|
+
throw error;
|
|
339
|
+
}).finally(() => {
|
|
340
|
+
rebuildPromise = null;
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const loadSubset = async (options) => {
|
|
345
|
+
if (stopped) return;
|
|
346
|
+
await startup;
|
|
347
|
+
if (
|
|
348
|
+
// Cleanup can run while startup is pending.
|
|
349
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
350
|
+
stopped || releasedSubsets.has(options) || options.signal?.aborted
|
|
351
|
+
) {
|
|
271
352
|
return;
|
|
272
353
|
}
|
|
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);
|
|
354
|
+
const demand = { options, active: false };
|
|
355
|
+
demands.set(options, demand);
|
|
356
|
+
try {
|
|
357
|
+
const cleanup = await restConfig.onLoadSubset?.(options);
|
|
358
|
+
if (cleanup) demand.cleanup = cleanup;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
demands.delete(options);
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
if (
|
|
364
|
+
// The user hook can reenter cleanup.
|
|
365
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
366
|
+
stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
|
|
367
|
+
) {
|
|
368
|
+
demands.delete(options);
|
|
369
|
+
demand.cleanup?.();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
demand.active = true;
|
|
373
|
+
trackingRevision++;
|
|
374
|
+
await rebuildTracking();
|
|
311
375
|
};
|
|
312
376
|
const toInlinedWhereClause = (compiled) => {
|
|
313
377
|
if (!compiled.where) return "TRUE";
|
|
@@ -317,32 +381,40 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
317
381
|
...compiled.params
|
|
318
382
|
);
|
|
319
383
|
};
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
384
|
+
const cleanupDemand = (demand) => {
|
|
385
|
+
demands.delete(demand.options);
|
|
386
|
+
try {
|
|
387
|
+
demand.cleanup?.();
|
|
388
|
+
} catch (error) {
|
|
389
|
+
database.logger.error(
|
|
390
|
+
`Could not clean up subset hook for ${viewName}`,
|
|
391
|
+
error
|
|
392
|
+
);
|
|
327
393
|
}
|
|
394
|
+
};
|
|
395
|
+
const performPhysicalRelease = async (options) => {
|
|
328
396
|
const compiledDeparting = sqliteCompiler.compileSQLite({ where: options.where });
|
|
329
397
|
const departingWhereSQL = toInlinedWhereClause(compiledDeparting);
|
|
330
|
-
let
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
398
|
+
let rowsToEvict;
|
|
399
|
+
for (; ; ) {
|
|
400
|
+
if (stopped) return;
|
|
401
|
+
const revision = trackingRevision;
|
|
402
|
+
const active = activeWhereExpressions();
|
|
403
|
+
let evictionSQL;
|
|
404
|
+
if (active.length === 0) {
|
|
405
|
+
evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`;
|
|
406
|
+
} else {
|
|
407
|
+
const combinedRemaining = active.length === 1 ? active[0] : db.or(active[0], active[1], ...active.slice(2));
|
|
408
|
+
const compiledRemaining = sqliteCompiler.compileSQLite({
|
|
409
|
+
where: combinedRemaining
|
|
410
|
+
});
|
|
411
|
+
const remainingWhereSQL = toInlinedWhereClause(compiledRemaining);
|
|
412
|
+
evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`;
|
|
413
|
+
}
|
|
414
|
+
rowsToEvict = await database.getAll(evictionSQL);
|
|
415
|
+
if (stopped) return;
|
|
416
|
+
if (trackingRevision === revision) break;
|
|
344
417
|
}
|
|
345
|
-
const rowsToEvict = await database.getAll(evictionSQL);
|
|
346
418
|
if (rowsToEvict.length > 0) {
|
|
347
419
|
begin();
|
|
348
420
|
for (const { id } of rowsToEvict) {
|
|
@@ -350,22 +422,79 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
350
422
|
}
|
|
351
423
|
void commit();
|
|
352
424
|
}
|
|
353
|
-
await
|
|
425
|
+
await rebuildTracking();
|
|
426
|
+
};
|
|
427
|
+
function scheduleReleaseDrain(delay = 0) {
|
|
428
|
+
if (stopped || drainingReleases || releaseRetryTimer) return;
|
|
429
|
+
if (delay > 0) {
|
|
430
|
+
releaseRetryTimer = setTimeout(() => {
|
|
431
|
+
releaseRetryTimer = void 0;
|
|
432
|
+
void drainReleases();
|
|
433
|
+
}, delay);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
void drainReleases();
|
|
437
|
+
}
|
|
438
|
+
async function drainReleases() {
|
|
439
|
+
if (stopped || drainingReleases) return;
|
|
440
|
+
drainingReleases = true;
|
|
441
|
+
let retryDelay = 0;
|
|
442
|
+
try {
|
|
443
|
+
const attempts = pendingReleases.length;
|
|
444
|
+
for (let index = 0; !stopped && index < attempts; index++) {
|
|
445
|
+
const pending = pendingReleases.shift();
|
|
446
|
+
try {
|
|
447
|
+
await performPhysicalRelease(pending.options);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
pending.failures++;
|
|
450
|
+
pendingReleases.push(pending);
|
|
451
|
+
const delay = Math.min(
|
|
452
|
+
1e3 * 2 ** (pending.failures - 1),
|
|
453
|
+
3e4
|
|
454
|
+
);
|
|
455
|
+
retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
|
|
456
|
+
database.logger.error(
|
|
457
|
+
`Could not release subset tracking for ${viewName}; retrying`,
|
|
458
|
+
error
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} finally {
|
|
463
|
+
drainingReleases = false;
|
|
464
|
+
}
|
|
465
|
+
if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay);
|
|
466
|
+
}
|
|
467
|
+
const unloadSubset = (options) => {
|
|
468
|
+
releasedSubsets.add(options);
|
|
469
|
+
const demand = demands.get(options);
|
|
470
|
+
if (!demand) return;
|
|
471
|
+
const wasActive = demand.active;
|
|
472
|
+
if (wasActive) trackingRevision++;
|
|
473
|
+
cleanupDemand(demand);
|
|
474
|
+
if (wasActive) {
|
|
475
|
+
pendingReleases.push({ options, failures: 0 });
|
|
476
|
+
clearTimeout(releaseRetryTimer);
|
|
477
|
+
releaseRetryTimer = void 0;
|
|
478
|
+
scheduleReleaseDrain();
|
|
479
|
+
}
|
|
354
480
|
};
|
|
355
481
|
markReady();
|
|
356
482
|
return {
|
|
357
483
|
cleanup: () => {
|
|
358
484
|
stopped = true;
|
|
485
|
+
clearTimeout(releaseRetryTimer);
|
|
486
|
+
releaseRetryTimer = void 0;
|
|
359
487
|
database.logger.info(
|
|
360
488
|
`Sync has been stopped for ${viewName} into ${trackedTableName}`
|
|
361
489
|
);
|
|
362
490
|
abortController.abort();
|
|
363
|
-
for (const
|
|
364
|
-
|
|
365
|
-
|
|
491
|
+
for (const demand of demands.values()) {
|
|
492
|
+
cleanupDemand(demand);
|
|
493
|
+
}
|
|
494
|
+
pendingReleases.length = 0;
|
|
366
495
|
},
|
|
367
496
|
loadSubset: (options) => loadSubset(options),
|
|
368
|
-
unloadSubset
|
|
497
|
+
unloadSubset
|
|
369
498
|
};
|
|
370
499
|
}
|
|
371
500
|
},
|