Notes/src/routes/api/sync.ts

341 lines
10 KiB
TypeScript
Raw Normal View History

2017-10-24 22:58:59 -04:00
"use strict";
import syncService from "../../services/sync.js";
import syncUpdateService from "../../services/sync_update.js";
import entityChangesService from "../../services/entity_changes.js";
import sql from "../../services/sql.js";
import sqlInit from "../../services/sql_init.js";
import optionService from "../../services/options.js";
import contentHashService from "../../services/content_hash.js";
import log from "../../services/log.js";
import syncOptions from "../../services/sync_options.js";
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
import ws from "../../services/ws.js";
import type { Request } from "express";
import type { EntityChange } from "../../services/entity_changes_interface.js";
import ValidationError from "../../errors/validation_error.js";
import consistencyChecksService from "../../services/consistency_checks.js";
2024-11-02 15:43:16 +02:00
import { t } from "i18next";
2017-11-21 22:11:27 -05:00
async function testSync() {
2018-07-23 10:29:17 +02:00
try {
2020-06-20 12:31:38 +02:00
if (!syncOptions.isSyncSetup()) {
2024-11-02 15:43:16 +02:00
return { success: false, message: t("test_sync.not-configured") };
}
await syncService.login();
2018-07-23 10:29:17 +02:00
// login was successful, so we'll kick off sync now
// this is important in case when sync server has been just initialized
syncService.sync();
2024-11-02 15:43:16 +02:00
return { success: true, message: t("test_sync.successful") };
} catch (e: unknown) {
const [errMessage] = safeExtractMessageAndStackFromError(e);
2018-07-23 10:29:17 +02:00
return {
success: false,
error: errMessage
2018-07-23 10:29:17 +02:00
};
}
}
2020-06-20 12:31:38 +02:00
function getStats() {
if (!sqlInit.schemaExists()) {
2018-07-25 09:46:57 +02:00
// fail silently but prevent errors from not existing options table
return {};
}
2020-08-27 22:03:56 +02:00
const stats = {
2025-01-09 18:07:02 +02:00
initialized: sql.getValue("SELECT value FROM options WHERE name = 'initialized'") === "true",
outstandingPullCount: syncService.getOutstandingPullCount()
};
2020-08-27 22:03:56 +02:00
log.info(`Returning sync stats: ${JSON.stringify(stats)}`);
return stats;
}
2020-06-20 12:31:38 +02:00
function checkSync() {
2018-03-30 14:27:41 -04:00
return {
2020-06-20 12:31:38 +02:00
entityHashes: contentHashService.getEntityHashes(),
2025-01-09 18:07:02 +02:00
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
2018-03-30 14:27:41 -04:00
};
}
2017-10-24 22:58:59 -04:00
2020-06-20 12:31:38 +02:00
function syncNow() {
log.info("Received request to trigger sync now.");
// when explicitly asked for set in progress status immediately for faster user feedback
ws.syncPullInProgress();
2020-06-20 12:31:38 +02:00
return syncService.sync();
2018-03-30 14:27:41 -04:00
}
2017-10-29 11:22:41 -04:00
function fillEntityChanges() {
entityChangesService.fillAllEntityChanges();
2017-12-23 13:16:18 -05:00
log.info("Sync rows have been filled.");
2018-03-30 14:27:41 -04:00
}
2017-12-23 13:16:18 -05:00
2020-06-20 12:31:38 +02:00
function forceFullSync() {
2025-01-09 18:07:02 +02:00
optionService.setOption("lastSyncedPull", 0);
optionService.setOption("lastSyncedPush", 0);
2017-12-23 13:16:18 -05:00
log.info("Forcing full sync.");
// not awaiting for the job to finish (will probably take a long time)
syncService.sync();
2018-03-30 14:27:41 -04:00
}
2025-02-13 23:51:42 +01:00
/**
* @swagger
* /api/sync/changed:
* get:
* summary: Pull sync changes
* operationId: sync-changed
* externalDocs:
* description: Server implementation
* url: https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/api/sync.ts
* parameters:
* - in: query
* name: instanceId
* required: true
* schema:
* type: string
* description: Local instance ID
* - in: query
* name: lastEntityChangeId
* required: true
* schema:
* type: integer
* description: Last locally present change ID
* - in: query
* name: logMarkerId
* required: true
* schema:
* type: string
* description: Marker to identify this request in server log
* responses:
* '200':
* description: Sync changes, limited to approximately one megabyte.
2025-02-13 23:51:42 +01:00
* content:
* application/json:
* schema:
* type: object
* properties:
* entityChanges:
* type: list
* items:
* $ref: '#/components/schemas/EntityChange'
* lastEntityChangeId:
* type: integer
* description: If `outstandingPullCount > 0`, pass this as parameter in your next request to continue.
* outstandingPullCount:
* type: int
* example: 42
* description: Number of changes not yet returned by the remote.
* security:
* - session: []
* tags:
* - sync
*/
2024-04-06 23:28:51 +03:00
function getChanged(req: Request) {
const startTime = Date.now();
2024-04-06 23:28:51 +03:00
if (typeof req.query.lastEntityChangeId !== "string") {
throw new ValidationError("Missing or invalid last entity change ID.");
}
let lastEntityChangeId: number | null | undefined = parseInt(req.query.lastEntityChangeId);
2023-07-29 21:59:20 +02:00
const clientInstanceId = req.query.instanceId;
2024-04-06 23:28:51 +03:00
let filteredEntityChanges: EntityChange[] = [];
2023-07-29 21:59:20 +02:00
do {
2025-01-09 18:07:02 +02:00
const entityChanges: EntityChange[] = sql.getRows<EntityChange>(
`
SELECT *
FROM entity_changes
WHERE isSynced = 1
AND id > ?
ORDER BY id
2025-01-09 18:07:02 +02:00
LIMIT 1000`,
[lastEntityChangeId]
);
if (entityChanges.length === 0) {
break;
}
2025-01-09 18:07:02 +02:00
filteredEntityChanges = entityChanges.filter((ec) => ec.instanceId !== clientInstanceId);
2022-01-09 21:25:15 +01:00
if (filteredEntityChanges.length === 0) {
lastEntityChangeId = entityChanges[entityChanges.length - 1].id;
}
2023-07-29 21:59:20 +02:00
} while (filteredEntityChanges.length === 0);
2017-10-24 22:58:59 -04:00
2022-01-09 21:25:15 +01:00
const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges);
2022-01-09 21:25:15 +01:00
if (entityChangeRecords.length > 0) {
lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id;
2023-07-29 21:59:20 +02:00
log.info(`Returning ${entityChangeRecords.length} entity changes in ${Date.now() - startTime}ms`);
}
2023-07-29 21:59:20 +02:00
return {
2022-01-09 21:25:15 +01:00
entityChanges: entityChangeRecords,
lastEntityChangeId,
2025-01-09 18:07:02 +02:00
outstandingPullCount: sql.getValue(
`
SELECT COUNT(id)
FROM entity_changes
WHERE isSynced = 1
AND instanceId != ?
2025-01-09 18:07:02 +02:00
AND id > ?`,
[clientInstanceId, lastEntityChangeId]
)
};
}
2025-01-09 18:07:02 +02:00
const partialRequests: Record<
string,
{
createdAt: number;
payload: string;
}
> = {};
2021-01-10 21:56:40 +01:00
2025-02-13 23:51:42 +01:00
/**
* @swagger
* /api/sync/update:
* put:
* summary: Push sync changes
* description:
* "Basic usage: set `pageCount = 1`, `pageIndex = 0`, and omit `requestId`. Supply your entity changes in the request body."
* operationId: sync-update
* externalDocs:
* description: Server implementation
* url: https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/api/sync.ts
* parameters:
* - in: header
* name: pageCount
* required: true
* schema:
* type: integer
* - in: header
* name: pageIndex
* required: true
* schema:
* type: integer
* - in: header
* name: requestId
* schema:
* type: string
* description: ID to identify paginated requests
* - in: query
* name: logMarkerId
* required: true
* schema:
* type: string
* description: Marker to identify this request in server log
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* instanceId:
* type: string
* description: Local instance ID
* entities:
* type: list
* items:
* $ref: '#/components/schemas/EntityChange'
* responses:
* '200':
* description: Changes processed successfully
* security:
* - session: []
* tags:
* - sync
*/
2024-04-06 23:28:51 +03:00
function update(req: Request) {
2024-04-03 20:47:41 +03:00
let { body } = req;
2021-01-10 21:56:40 +01:00
2025-01-09 18:07:02 +02:00
const pageCount = parseInt(req.get("pageCount") as string);
const pageIndex = parseInt(req.get("pageIndex") as string);
2021-01-10 21:56:40 +01:00
if (pageCount !== 1) {
2025-01-09 18:07:02 +02:00
const requestId = req.get("requestId");
2024-04-06 23:28:51 +03:00
if (!requestId) {
throw new Error("Missing request ID.");
}
2021-01-10 21:56:40 +01:00
if (pageIndex === 0) {
partialRequests[requestId] = {
createdAt: Date.now(),
2025-01-09 18:07:02 +02:00
payload: ""
2021-01-10 21:56:40 +01:00
};
}
if (!partialRequests[requestId]) {
2023-07-29 21:59:20 +02:00
throw new Error(`Partial request ${requestId}, page ${pageIndex + 1} of ${pageCount} of pages does not have expected record.`);
2021-01-10 21:56:40 +01:00
}
partialRequests[requestId].payload += req.body;
2023-07-29 21:59:20 +02:00
log.info(`Receiving a partial request ${requestId}, page ${pageIndex + 1} out of ${pageCount} pages.`);
2021-01-11 22:48:51 +01:00
2021-01-10 21:56:40 +01:00
if (pageIndex !== pageCount - 1) {
return;
2025-01-09 18:07:02 +02:00
} else {
2021-01-10 21:56:40 +01:00
body = JSON.parse(partialRequests[requestId].payload);
delete partialRequests[requestId];
}
}
2024-04-03 20:47:41 +03:00
const { entities, instanceId } = body;
2023-09-21 18:13:14 +02:00
sql.transactional(() => syncUpdateService.updateEntities(entities, instanceId));
2018-03-30 14:27:41 -04:00
}
2017-10-26 21:16:21 -04:00
2021-01-10 21:56:40 +01:00
setInterval(() => {
for (const key in partialRequests) {
if (Date.now() - partialRequests[key].createdAt > 20 * 60 * 1000) {
2021-01-11 22:48:51 +01:00
log.info(`Cleaning up unfinished partial requests for ${key}`);
2021-01-10 21:56:40 +01:00
delete partialRequests[key];
}
}
}, 60 * 1000);
2020-06-20 12:31:38 +02:00
function syncFinished() {
2023-06-30 11:18:34 +02:00
// after the first sync finishes, the application is ready to be used
// this is meaningless but at the same time harmless (idempotent) for further syncs
2020-06-20 21:42:41 +02:00
sqlInit.setDbAsInitialized();
}
2024-04-06 23:28:51 +03:00
function queueSector(req: Request) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector);
entityChangesService.addEntityChangesForSector(entityName, sector);
}
function checkEntityChanges() {
consistencyChecksService.runEntityChangesChecks();
}
export default {
2018-07-23 10:29:17 +02:00
testSync,
2018-03-30 14:27:41 -04:00
checkSync,
syncNow,
fillEntityChanges,
2018-03-30 14:27:41 -04:00
forceFullSync,
getChanged,
update,
getStats,
syncFinished,
queueSector,
checkEntityChanges
};