@dinoxx/dinox-cli 1.0.22 → 1.1.1
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/README.md +33 -10
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +55 -14
- package/dist/cli.js +36 -2
- package/dist/commands/auth/index.d.ts +35 -0
- package/dist/commands/auth/index.js +382 -83
- package/dist/commands/boxes/index.js +2 -6
- package/dist/commands/boxes/model.d.ts +3 -0
- package/dist/commands/boxes/model.js +16 -1
- package/dist/commands/boxes/operations.js +18 -8
- package/dist/commands/boxes/repo.js +13 -11
- package/dist/commands/boxes/store.js +1 -1
- package/dist/commands/config/index.js +7 -5
- package/dist/commands/descriptor/register.js +9 -0
- package/dist/commands/doctor.d.ts +123 -0
- package/dist/commands/doctor.js +539 -0
- package/dist/commands/graph/index.d.ts +1 -1
- package/dist/commands/graph/index.js +35 -21
- package/dist/commands/graph/repo.js +92 -17
- package/dist/commands/meta/index.js +4 -4
- package/dist/commands/meta/repo.js +4 -3
- package/dist/commands/notes/actions.d.ts +1 -1
- package/dist/commands/notes/actions.js +121 -78
- package/dist/commands/notes/bulk.d.ts +1 -0
- package/dist/commands/notes/bulk.js +23 -17
- package/dist/commands/notes/contentEngine.js +69 -11
- package/dist/commands/notes/contentPatch.d.ts +4 -1
- package/dist/commands/notes/contentPatch.js +105 -41
- package/dist/commands/notes/contentPatchCommand.js +28 -11
- package/dist/commands/notes/descriptors.js +50 -20
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +4 -7
- package/dist/commands/notes/export.d.ts +6 -0
- package/dist/commands/notes/export.js +26 -6
- package/dist/commands/notes/metadataMutation.d.ts +1 -0
- package/dist/commands/notes/metadataMutation.js +14 -3
- package/dist/commands/notes/repo.d.ts +46 -4
- package/dist/commands/notes/repo.js +113 -28
- package/dist/commands/notes/runtime.d.ts +2 -1
- package/dist/commands/notes/runtime.js +17 -1
- package/dist/commands/notes/searchSql.js +13 -6
- package/dist/commands/notes/writeReceipt.d.ts +20 -0
- package/dist/commands/notes/writeReceipt.js +82 -0
- package/dist/commands/prompt/index.js +3 -3
- package/dist/commands/prompt/repo.d.ts +4 -1
- package/dist/commands/prompt/repo.js +17 -7
- package/dist/commands/runtime.d.ts +14 -2
- package/dist/commands/runtime.js +89 -19
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/storage/index.js +8 -2
- package/dist/commands/storage/repo.d.ts +4 -0
- package/dist/commands/storage/repo.js +356 -92
- package/dist/commands/sync.d.ts +23 -1
- package/dist/commands/sync.js +105 -60
- package/dist/commands/tags/content.js +9 -0
- package/dist/commands/tags/index.js +2 -6
- package/dist/commands/tags/operations.js +15 -10
- package/dist/commands/tags/repo.d.ts +2 -0
- package/dist/commands/tags/repo.js +18 -5
- package/dist/commands/tags/store.js +1 -1
- package/dist/commands/todo/contentModel.d.ts +4 -3
- package/dist/commands/todo/contentModel.js +78 -28
- package/dist/commands/todo/dryRun.d.ts +1 -0
- package/dist/commands/todo/dryRun.js +17 -13
- package/dist/commands/todo/index.d.ts +6 -0
- package/dist/commands/todo/index.js +89 -123
- package/dist/commands/todo/repo.d.ts +36 -1
- package/dist/commands/todo/repo.js +146 -37
- package/dist/commands/todo/taskIdentity.d.ts +2 -0
- package/dist/commands/todo/taskIdentity.js +64 -0
- package/dist/commands/todo/taskModel.js +27 -17
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +135 -23
- package/dist/commands/writeDryRun.d.ts +3 -2
- package/dist/commands/writeDryRun.js +4 -3
- package/dist/commands/writeDurability.d.ts +5 -0
- package/dist/commands/writeDurability.js +22 -0
- package/dist/config/paths.js +6 -1
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +23 -9
- package/dist/config/store.d.ts +3 -0
- package/dist/config/store.js +61 -14
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/client.d.ts +26 -1
- package/dist/daemon/client.js +236 -12
- package/dist/daemon/ensure.js +74 -12
- package/dist/daemon/index.d.ts +42 -0
- package/dist/daemon/index.js +202 -52
- package/dist/daemon/lifecycle.d.ts +44 -0
- package/dist/daemon/lifecycle.js +118 -0
- package/dist/daemon/pid.d.ts +19 -6
- package/dist/daemon/pid.js +147 -13
- package/dist/daemon/route.d.ts +14 -0
- package/dist/daemon/route.js +40 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +201 -50
- package/dist/daemon/types.d.ts +29 -1
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +25 -1
- package/dist/powersync/connector.js +31 -16
- package/dist/powersync/exclusiveRuntime.d.ts +20 -0
- package/dist/powersync/exclusiveRuntime.js +48 -0
- package/dist/powersync/runtime.d.ts +25 -4
- package/dist/powersync/runtime.js +255 -35
- package/dist/powersync/syncGate.d.ts +4 -1
- package/dist/powersync/syncGate.js +11 -2
- package/dist/powersync/syncPass.d.ts +2 -0
- package/dist/powersync/syncPass.js +15 -4
- package/dist/powersync/tokenIndex.d.ts +13 -0
- package/dist/powersync/tokenIndex.js +216 -37
- package/dist/powersync/uploader.js +107 -52
- package/dist/utils/abort.js +0 -4
- package/dist/utils/asyncRelease.d.ts +2 -0
- package/dist/utils/asyncRelease.js +25 -0
- package/dist/utils/bidlinkCanonicalization.js +3 -0
- package/dist/utils/dryRun.d.ts +1 -1
- package/dist/utils/errors.d.ts +15 -1
- package/dist/utils/errors.js +223 -5
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +51 -15
- package/dist/utils/redact.js +1 -1
- package/dist/utils/sqliteLease.d.ts +8 -0
- package/dist/utils/sqliteLease.js +59 -0
- package/dist/utils/syncTimeout.d.ts +7 -1
- package/dist/utils/syncTimeout.js +8 -7
- package/dist/utils/terminalSanitization.js +6 -2
- package/dist/utils/tiptapMarkdown.js +49 -3
- package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
- package/dist/utils/tiptapMarkdownSyntax.js +249 -0
- package/dist/utils/tokenize.js +24 -2
- package/dist/utils/updateNotice.js +60 -12
- package/package.json +14 -10
- package/dist/commands/mcp/auth.d.ts +0 -4
- package/dist/commands/mcp/auth.js +0 -75
- package/dist/commands/mcp/index.d.ts +0 -2
- package/dist/commands/mcp/index.js +0 -86
- package/dist/commands/mcp/noteContentTools.d.ts +0 -222
- package/dist/commands/mcp/noteContentTools.js +0 -390
- package/dist/commands/mcp/server.d.ts +0 -34
- package/dist/commands/mcp/server.js +0 -424
- package/dist/commands/mcp/tools.d.ts +0 -2
- package/dist/commands/mcp/tools.js +0 -32
- package/dist/commands/mcp/types.d.ts +0 -39
- package/dist/commands/mcp/types.js +0 -1
- package/dist/utils/noteContentEngine.d.ts +0 -333
- package/dist/utils/noteContentEngine.js +0 -740
package/dist/dinox.js
CHANGED
|
@@ -18,9 +18,26 @@ async function main() {
|
|
|
18
18
|
function isVerbose(argv) {
|
|
19
19
|
return argv.includes('--verbose');
|
|
20
20
|
}
|
|
21
|
+
function isCommanderError(error) {
|
|
22
|
+
return Boolean(error)
|
|
23
|
+
&& typeof error === 'object'
|
|
24
|
+
&& typeof error.code === 'string'
|
|
25
|
+
&& error.code.startsWith('commander.')
|
|
26
|
+
&& typeof error.exitCode === 'number';
|
|
27
|
+
}
|
|
21
28
|
main().catch((error) => {
|
|
29
|
+
if (isCommanderError(error)) {
|
|
30
|
+
process.exitCode = error.exitCode === 0 ? 0 : 2;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
22
33
|
const argv = normalizeArgv(process.argv.slice(2));
|
|
23
|
-
|
|
34
|
+
let format = null;
|
|
35
|
+
try {
|
|
36
|
+
format = parseStructuredOutputFormatFromArgv(argv);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
format = null;
|
|
40
|
+
}
|
|
24
41
|
const verbose = isVerbose(argv);
|
|
25
42
|
const payload = buildStructuredErrorPayload(error, verbose);
|
|
26
43
|
const exitCode = isDinoxError(error) ? error.exitCode : 1;
|
|
@@ -32,6 +49,13 @@ main().catch((error) => {
|
|
|
32
49
|
return;
|
|
33
50
|
}
|
|
34
51
|
printHumanReadableError(payload.message);
|
|
52
|
+
if (payload.suggested_action) {
|
|
53
|
+
printHumanReadableError(`suggested action: ${payload.suggested_action.command}`);
|
|
54
|
+
printHumanReadableError(`reason: ${payload.suggested_action.reason}`);
|
|
55
|
+
}
|
|
56
|
+
else if (payload.suggestion) {
|
|
57
|
+
printHumanReadableError(`suggestion: ${payload.suggestion}`);
|
|
58
|
+
}
|
|
35
59
|
if (verbose && payload.stack) {
|
|
36
60
|
printHumanReadableError(payload.stack);
|
|
37
61
|
}
|
|
@@ -39,9 +39,8 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
|
|
|
39
39
|
? createTimeoutSignal(timeoutMs)
|
|
40
40
|
: null;
|
|
41
41
|
const requestSignal = mergeAbortSignals([signal, timeout?.signal]);
|
|
42
|
-
let response;
|
|
43
42
|
try {
|
|
44
|
-
response = await fetch(credentialsUrl, {
|
|
43
|
+
const response = await fetch(credentialsUrl, {
|
|
45
44
|
method: 'GET',
|
|
46
45
|
headers: {
|
|
47
46
|
Authorization: toAuthorizationHeaderValue(authorization),
|
|
@@ -49,6 +48,36 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
|
|
|
49
48
|
},
|
|
50
49
|
signal: requestSignal,
|
|
51
50
|
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
let errorText = '';
|
|
53
|
+
try {
|
|
54
|
+
errorText = await response.text();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (requestSignal?.aborted) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Received ${response.status} from ${credentialsUrl}: ${errorText}`);
|
|
62
|
+
}
|
|
63
|
+
let payload = null;
|
|
64
|
+
try {
|
|
65
|
+
payload = await response.json();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (requestSignal?.aborted) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const token = payload?.token ?? payload?.data?.token ?? null;
|
|
73
|
+
const endpointFromPayload = payload?.endpoint ?? payload?.data?.endpoint ?? null;
|
|
74
|
+
if (!token) {
|
|
75
|
+
throw new Error('PowerSync credential response did not include a token');
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
endpoint: endpointFromPayload ?? endpoint,
|
|
79
|
+
token,
|
|
80
|
+
};
|
|
52
81
|
}
|
|
53
82
|
catch (error) {
|
|
54
83
|
if (timeout?.signal.aborted && !signal?.aborted) {
|
|
@@ -59,18 +88,4 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
|
|
|
59
88
|
finally {
|
|
60
89
|
timeout?.cancel();
|
|
61
90
|
}
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
const errorText = await response.text().catch(() => '');
|
|
64
|
-
throw new Error(`Received ${response.status} from ${credentialsUrl}: ${errorText}`);
|
|
65
|
-
}
|
|
66
|
-
const payload = await response.json().catch(() => null);
|
|
67
|
-
const token = payload?.token ?? payload?.data?.token ?? null;
|
|
68
|
-
const endpointFromPayload = payload?.endpoint ?? payload?.data?.endpoint ?? null;
|
|
69
|
-
if (!token) {
|
|
70
|
-
throw new Error('PowerSync credential response did not include a token');
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
endpoint: endpointFromPayload ?? endpoint,
|
|
74
|
-
token,
|
|
75
|
-
};
|
|
76
91
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedDinoxConfig } from '../config/types.js';
|
|
2
|
+
import { withDaemonExclusiveAccess } from '../daemon/lifecycle.js';
|
|
3
|
+
import { printHumanReadableWarning } from '../utils/output.js';
|
|
4
|
+
import { closePowerSyncAndReleaseOwner, connectPowerSync, type ConnectResult } from './runtime.js';
|
|
5
|
+
type ExclusivePowerSyncDependencies = {
|
|
6
|
+
withDaemonExclusiveAccess: typeof withDaemonExclusiveAccess;
|
|
7
|
+
connectPowerSync: typeof connectPowerSync;
|
|
8
|
+
closePowerSyncAndReleaseOwner: typeof closePowerSyncAndReleaseOwner;
|
|
9
|
+
printWarning: typeof printHumanReadableWarning;
|
|
10
|
+
};
|
|
11
|
+
export declare function withExclusivePowerSync<T>(args: {
|
|
12
|
+
config: ResolvedDinoxConfig;
|
|
13
|
+
offline: boolean;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
deadlineAtMs?: number;
|
|
16
|
+
waitForInitialSync?: boolean;
|
|
17
|
+
expectedTokenFingerprint?: string | null;
|
|
18
|
+
cleanupFailureMode?: 'warn' | 'throw';
|
|
19
|
+
}, run: (connected: ConnectResult) => Promise<T>, overrides?: Partial<ExclusivePowerSyncDependencies>): Promise<T>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { withDaemonExclusiveAccess } from '../daemon/lifecycle.js';
|
|
2
|
+
import { printHumanReadableWarning } from '../utils/output.js';
|
|
3
|
+
import { closePowerSyncAndReleaseOwner, connectPowerSync, } from './runtime.js';
|
|
4
|
+
const exclusivePowerSyncDependencies = {
|
|
5
|
+
withDaemonExclusiveAccess,
|
|
6
|
+
connectPowerSync,
|
|
7
|
+
closePowerSyncAndReleaseOwner,
|
|
8
|
+
printWarning: printHumanReadableWarning,
|
|
9
|
+
};
|
|
10
|
+
function errorMessage(error) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
export async function withExclusivePowerSync(args, run, overrides = {}) {
|
|
14
|
+
const dependencies = { ...exclusivePowerSyncDependencies, ...overrides };
|
|
15
|
+
const { cleanupFailureMode, deadlineAtMs, ...connectArgs } = args;
|
|
16
|
+
const remainingTimeoutMs = () => deadlineAtMs === undefined
|
|
17
|
+
? args.timeoutMs
|
|
18
|
+
: Math.max(1, Math.min(args.timeoutMs, deadlineAtMs - Date.now()));
|
|
19
|
+
return dependencies.withDaemonExclusiveAccess(args.config, remainingTimeoutMs(), async (daemonLifecycle) => {
|
|
20
|
+
const connected = await dependencies.connectPowerSync({
|
|
21
|
+
...connectArgs,
|
|
22
|
+
timeoutMs: remainingTimeoutMs(),
|
|
23
|
+
});
|
|
24
|
+
let outcome;
|
|
25
|
+
try {
|
|
26
|
+
outcome = { ok: true, value: await run(connected) };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
outcome = { ok: false, error };
|
|
30
|
+
}
|
|
31
|
+
let cleanupError;
|
|
32
|
+
try {
|
|
33
|
+
await dependencies.closePowerSyncAndReleaseOwner(connected.db, connected.releaseOwner);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
cleanupError = error;
|
|
37
|
+
daemonLifecycle.suppressRestore();
|
|
38
|
+
dependencies.printWarning(`warning: failed to close the local PowerSync runtime safely: ${errorMessage(error)}`);
|
|
39
|
+
}
|
|
40
|
+
if (!outcome.ok) {
|
|
41
|
+
throw outcome.error;
|
|
42
|
+
}
|
|
43
|
+
if (cleanupError && cleanupFailureMode === 'throw') {
|
|
44
|
+
throw cleanupError;
|
|
45
|
+
}
|
|
46
|
+
return outcome.value;
|
|
47
|
+
}, {}, { deadlineAtMs });
|
|
48
|
+
}
|
|
@@ -5,7 +5,13 @@ export type ConnectResult = {
|
|
|
5
5
|
db: PowerSyncDatabase;
|
|
6
6
|
dbPath: string;
|
|
7
7
|
stale: boolean;
|
|
8
|
+
releaseOwner: () => Promise<void>;
|
|
8
9
|
};
|
|
10
|
+
type ClosablePowerSyncDatabase = {
|
|
11
|
+
close: () => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
export declare function acquirePowerSyncOwnerLease(dbPath: string, timeoutMs: number): Promise<() => Promise<void>>;
|
|
14
|
+
export declare function closePowerSyncAndReleaseOwner(db: ClosablePowerSyncDatabase, releaseOwner: () => Promise<void>): Promise<void>;
|
|
9
15
|
export declare function getStatusSnapshot(db: AbstractPowerSyncDatabase): {
|
|
10
16
|
connected: boolean;
|
|
11
17
|
connecting: boolean;
|
|
@@ -25,14 +31,29 @@ export declare function getStatusSnapshot(db: AbstractPowerSyncDatabase): {
|
|
|
25
31
|
};
|
|
26
32
|
raw: SyncStatusOptions;
|
|
27
33
|
};
|
|
28
|
-
export declare function openPowerSyncDatabase(userId?: string | null): Promise<{
|
|
29
|
-
db: PowerSyncDatabase;
|
|
30
|
-
dbPath: string;
|
|
31
|
-
}>;
|
|
32
34
|
export declare function connectPowerSync(args: {
|
|
33
35
|
config: ResolvedDinoxConfig;
|
|
34
36
|
offline: boolean;
|
|
35
37
|
timeoutMs: number;
|
|
38
|
+
waitForInitialSync?: boolean;
|
|
39
|
+
expectedTokenFingerprint?: string | null;
|
|
36
40
|
}): Promise<ConnectResult>;
|
|
37
41
|
export declare function waitForIdleDataFlow(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<boolean>;
|
|
42
|
+
export declare function waitForDownloadIdle(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<boolean>;
|
|
43
|
+
export declare function waitForLastSyncedAfter(db: AbstractPowerSyncDatabase, startedAtIso: string, timeoutMs: number): Promise<boolean>;
|
|
44
|
+
export type UploadQueueWaitResult = {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
pendingCount: number | null;
|
|
47
|
+
uploadError?: {
|
|
48
|
+
name: string;
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
queueError?: {
|
|
52
|
+
name: string;
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
timedOut: boolean;
|
|
56
|
+
};
|
|
57
|
+
export declare function waitForUploadQueueEmpty(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<UploadQueueWaitResult>;
|
|
38
58
|
export declare function withPowerSyncWriteTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
|
|
59
|
+
export {};
|
|
@@ -2,11 +2,87 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { PowerSyncDatabase } from '@powersync/node';
|
|
4
4
|
import { getDataDir, getPowerSyncDbPath } from '../config/paths.js';
|
|
5
|
+
import { resolveConfig } from '../config/resolve.js';
|
|
6
|
+
import { loadConfig } from '../config/store.js';
|
|
5
7
|
import { DinoxError } from '../utils/errors.js';
|
|
6
8
|
import { createTimeoutSignal } from '../utils/abort.js';
|
|
9
|
+
import { acquireExclusiveSqliteLease } from '../utils/sqliteLease.js';
|
|
7
10
|
import { AppSchema } from './schema/index.js';
|
|
8
11
|
import { PowerSyncConnector, fetchCredentialsWithAuthorization } from './connector.js';
|
|
9
12
|
import { createPowerSyncUploadHandler } from './uploader.js';
|
|
13
|
+
const OWNER_LEASE_WAIT_MAX_MS = 10_000;
|
|
14
|
+
const retainedFailedPowerSyncClosures = new Set();
|
|
15
|
+
function retainFailedPowerSyncClosure(db, releaseOwner, dbClosed) {
|
|
16
|
+
for (const retained of retainedFailedPowerSyncClosures) {
|
|
17
|
+
if (retained.db === db && retained.releaseOwner === releaseOwner) {
|
|
18
|
+
retained.dbClosed ||= dbClosed;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
retainedFailedPowerSyncClosures.add({ db, releaseOwner, dbClosed });
|
|
23
|
+
}
|
|
24
|
+
function findFailedPowerSyncClosure(db, releaseOwner) {
|
|
25
|
+
for (const retained of retainedFailedPowerSyncClosures) {
|
|
26
|
+
if (retained.db === db && retained.releaseOwner === releaseOwner) {
|
|
27
|
+
return retained;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function forgetFailedPowerSyncClosure(db, releaseOwner) {
|
|
33
|
+
for (const retained of retainedFailedPowerSyncClosures) {
|
|
34
|
+
if (retained.db === db && retained.releaseOwner === releaseOwner) {
|
|
35
|
+
retainedFailedPowerSyncClosures.delete(retained);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function ownerLeasePath(dbPath) {
|
|
40
|
+
return `${dbPath}.owner.sqlite`;
|
|
41
|
+
}
|
|
42
|
+
export async function acquirePowerSyncOwnerLease(dbPath, timeoutMs) {
|
|
43
|
+
const lockPath = ownerLeasePath(dbPath);
|
|
44
|
+
return acquireExclusiveSqliteLease({
|
|
45
|
+
lockPath,
|
|
46
|
+
timeoutMs,
|
|
47
|
+
maxWaitMs: OWNER_LEASE_WAIT_MAX_MS,
|
|
48
|
+
createBusyError: () => new DinoxError('Another Dinox process owns the PowerSync runtime for this account.', {
|
|
49
|
+
code: 'POWERSYNC_OWNER_BUSY',
|
|
50
|
+
details: { dbPath },
|
|
51
|
+
suggestion: 'Wait for the other Dinox command to finish, then retry.',
|
|
52
|
+
recoverable: true,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function closePowerSyncAndReleaseOwner(db, releaseOwner) {
|
|
57
|
+
const retained = findFailedPowerSyncClosure(db, releaseOwner);
|
|
58
|
+
if (!retained?.dbClosed) {
|
|
59
|
+
try {
|
|
60
|
+
await db.close();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
retainFailedPowerSyncClosure(db, releaseOwner, false);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await releaseOwner();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
retainFailedPowerSyncClosure(db, releaseOwner, true);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
forgetFailedPowerSyncClosure(db, releaseOwner);
|
|
75
|
+
}
|
|
76
|
+
async function throwAfterPowerSyncCleanup(operationError, db, releaseOwner) {
|
|
77
|
+
try {
|
|
78
|
+
await closePowerSyncAndReleaseOwner(db, releaseOwner);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Preserve the actionable operation failure. The cleanup helper keeps the
|
|
82
|
+
// owner lease held whenever database close did not complete safely.
|
|
83
|
+
}
|
|
84
|
+
throw operationError;
|
|
85
|
+
}
|
|
10
86
|
async function ensureDataDir() {
|
|
11
87
|
const dir = getDataDir();
|
|
12
88
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
@@ -34,7 +110,7 @@ export function getStatusSnapshot(db) {
|
|
|
34
110
|
raw,
|
|
35
111
|
};
|
|
36
112
|
}
|
|
37
|
-
|
|
113
|
+
async function createPowerSyncDatabase(userId) {
|
|
38
114
|
await ensureDataDir();
|
|
39
115
|
const dbPath = getPowerSyncDbPath(userId);
|
|
40
116
|
await fs.mkdir(path.dirname(dbPath), { recursive: true, mode: 0o700 });
|
|
@@ -44,60 +120,162 @@ export async function openPowerSyncDatabase(userId) {
|
|
|
44
120
|
dbFilename: dbPath,
|
|
45
121
|
},
|
|
46
122
|
});
|
|
47
|
-
// Ensure initialization errors surface early.
|
|
48
|
-
await db.init();
|
|
49
123
|
return { db, dbPath };
|
|
50
124
|
}
|
|
51
125
|
function assertConfigured(config) {
|
|
52
126
|
if (!config.auth.authorization) {
|
|
53
|
-
throw new DinoxError('Not logged in.
|
|
127
|
+
throw new DinoxError('Not logged in. Set DINOX_TOKEN or pipe a token to `dino auth login --token-stdin`.');
|
|
54
128
|
}
|
|
55
129
|
}
|
|
56
130
|
export async function connectPowerSync(args) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
131
|
+
if (!args.offline) {
|
|
132
|
+
assertConfigured(args.config);
|
|
133
|
+
}
|
|
134
|
+
const dbPath = getPowerSyncDbPath(args.config.auth.userId);
|
|
135
|
+
await fs.mkdir(path.dirname(dbPath), { recursive: true, mode: 0o700 });
|
|
136
|
+
const releaseOwner = await acquirePowerSyncOwnerLease(dbPath, args.timeoutMs);
|
|
137
|
+
if (args.expectedTokenFingerprint !== undefined) {
|
|
138
|
+
try {
|
|
139
|
+
const currentAuth = resolveConfig(await loadConfig()).auth;
|
|
140
|
+
const currentTokenFingerprint = currentAuth.tokenFingerprint ?? null;
|
|
141
|
+
const expectedUserId = args.config.auth.userId ?? null;
|
|
142
|
+
const currentUserId = currentAuth.userId ?? null;
|
|
143
|
+
const persistedUserChanged = currentUserId !== null && currentUserId !== expectedUserId;
|
|
144
|
+
if (currentTokenFingerprint !== args.expectedTokenFingerprint || persistedUserChanged) {
|
|
145
|
+
throw new DinoxError('Authentication changed while waiting for the local database.', {
|
|
146
|
+
code: 'AUTH_STATE_CHANGED',
|
|
147
|
+
details: { dbPath, expectedUserId, currentUserId },
|
|
148
|
+
suggestion: 'Retry the command so it uses the current login.',
|
|
149
|
+
recoverable: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
await releaseOwner();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Preserve the authentication/configuration failure that triggered cleanup.
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let db = null;
|
|
81
164
|
try {
|
|
82
|
-
await
|
|
83
|
-
|
|
165
|
+
({ db } = await createPowerSyncDatabase(args.config.auth.userId));
|
|
166
|
+
await db.init();
|
|
84
167
|
}
|
|
85
168
|
catch (error) {
|
|
86
|
-
if (
|
|
87
|
-
|
|
169
|
+
if (!db) {
|
|
170
|
+
try {
|
|
171
|
+
await releaseOwner();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Preserve the database construction failure that triggered cleanup.
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
88
177
|
}
|
|
89
|
-
|
|
178
|
+
return throwAfterPowerSyncCleanup(error, db, releaseOwner);
|
|
179
|
+
}
|
|
180
|
+
if (args.offline) {
|
|
181
|
+
return { db, dbPath, stale: true, releaseOwner };
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const uploader = args.config.powersync.uploadBaseUrl
|
|
185
|
+
? createPowerSyncUploadHandler({
|
|
186
|
+
authorization: args.config.auth.authorization,
|
|
187
|
+
uploadBaseUrl: args.config.powersync.uploadBaseUrl,
|
|
188
|
+
uploadV4Path: args.config.powersync.uploadV4Path,
|
|
189
|
+
uploadV2Path: args.config.powersync.uploadV2Path,
|
|
190
|
+
timeoutMs: args.timeoutMs,
|
|
191
|
+
})
|
|
192
|
+
: undefined;
|
|
193
|
+
const connector = new PowerSyncConnector(async () => {
|
|
194
|
+
return fetchCredentialsWithAuthorization({
|
|
195
|
+
authorization: args.config.auth.authorization,
|
|
196
|
+
endpoint: args.config.powersync.endpoint,
|
|
197
|
+
credentialsUrl: args.config.powersync.tokenEndpoint,
|
|
198
|
+
timeoutMs: args.timeoutMs,
|
|
199
|
+
});
|
|
200
|
+
}, uploader);
|
|
201
|
+
await db.connect(connector);
|
|
202
|
+
if (args.waitForInitialSync === false) {
|
|
203
|
+
const status = getStatusSnapshot(db);
|
|
204
|
+
return { db, dbPath, stale: !status.connected || !status.hasSynced, releaseOwner };
|
|
205
|
+
}
|
|
206
|
+
const { signal, cancel } = createTimeoutSignal(args.timeoutMs);
|
|
207
|
+
try {
|
|
208
|
+
await db.waitForFirstSync(signal);
|
|
209
|
+
const status = getStatusSnapshot(db);
|
|
210
|
+
return { db, dbPath, stale: signal.aborted || !status.connected, releaseOwner };
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (signal.aborted) {
|
|
214
|
+
return { db, dbPath, stale: true, releaseOwner };
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
cancel();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return throwAfterPowerSyncCleanup(error, db, releaseOwner);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export async function waitForIdleDataFlow(db, timeoutMs) {
|
|
227
|
+
const { signal, cancel } = createTimeoutSignal(timeoutMs);
|
|
228
|
+
try {
|
|
229
|
+
await db.waitForStatus(status => {
|
|
230
|
+
const flow = status.dataFlowStatus;
|
|
231
|
+
return Boolean(status.connected) && !flow.downloading && !flow.uploading;
|
|
232
|
+
}, signal);
|
|
233
|
+
return !signal.aborted;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
90
237
|
}
|
|
91
238
|
finally {
|
|
92
239
|
cancel();
|
|
93
240
|
}
|
|
94
241
|
}
|
|
95
|
-
export async function
|
|
242
|
+
export async function waitForDownloadIdle(db, timeoutMs) {
|
|
96
243
|
const { signal, cancel } = createTimeoutSignal(timeoutMs);
|
|
97
244
|
try {
|
|
98
245
|
await db.waitForStatus(status => {
|
|
99
246
|
const flow = status.dataFlowStatus;
|
|
100
|
-
return
|
|
247
|
+
return Boolean(status.connected) && Boolean(status.hasSynced) && !flow.downloading;
|
|
248
|
+
}, signal);
|
|
249
|
+
return !signal.aborted;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
cancel();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function waitForLastSyncedAfter(db, startedAtIso, timeoutMs) {
|
|
259
|
+
const startedAtMs = Date.parse(startedAtIso);
|
|
260
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
// PowerSync reports checkpoint timestamps with whole-second precision.
|
|
264
|
+
const startedAtCheckpointPrecisionMs = Math.floor(startedAtMs / 1_000) * 1_000;
|
|
265
|
+
const status = getStatusSnapshot(db);
|
|
266
|
+
if (status.connected
|
|
267
|
+
&& status.lastSyncedAt
|
|
268
|
+
&& Date.parse(status.lastSyncedAt) >= startedAtCheckpointPrecisionMs) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const { signal, cancel } = createTimeoutSignal(timeoutMs);
|
|
272
|
+
try {
|
|
273
|
+
await db.waitForStatus((nextStatus) => {
|
|
274
|
+
const value = nextStatus.lastSyncedAt;
|
|
275
|
+
const lastSyncedAt = value instanceof Date ? value.toISOString() : value ?? null;
|
|
276
|
+
return Boolean(nextStatus.connected
|
|
277
|
+
&& lastSyncedAt
|
|
278
|
+
&& Date.parse(lastSyncedAt) >= startedAtCheckpointPrecisionMs);
|
|
101
279
|
}, signal);
|
|
102
280
|
return !signal.aborted;
|
|
103
281
|
}
|
|
@@ -108,6 +286,48 @@ export async function waitForIdleDataFlow(db, timeoutMs) {
|
|
|
108
286
|
cancel();
|
|
109
287
|
}
|
|
110
288
|
}
|
|
289
|
+
export async function waitForUploadQueueEmpty(db, timeoutMs) {
|
|
290
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
291
|
+
let pendingCount = Number.POSITIVE_INFINITY;
|
|
292
|
+
while (Date.now() <= deadline) {
|
|
293
|
+
const status = getStatusSnapshot(db);
|
|
294
|
+
try {
|
|
295
|
+
const stats = await db.getUploadQueueStats(false);
|
|
296
|
+
pendingCount = stats.count;
|
|
297
|
+
if (status.dataFlowStatus.uploadError) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
pendingCount,
|
|
301
|
+
uploadError: status.dataFlowStatus.uploadError,
|
|
302
|
+
timedOut: false,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (stats.count === 0) {
|
|
306
|
+
return { ok: true, pendingCount: 0, timedOut: false };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
pendingCount: Number.isFinite(pendingCount) ? pendingCount : null,
|
|
313
|
+
...(status.dataFlowStatus.uploadError
|
|
314
|
+
? { uploadError: status.dataFlowStatus.uploadError }
|
|
315
|
+
: {}),
|
|
316
|
+
queueError: {
|
|
317
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
318
|
+
message: error instanceof Error ? error.message : String(error),
|
|
319
|
+
},
|
|
320
|
+
timedOut: false,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
pendingCount: Number.isFinite(pendingCount) ? pendingCount : null,
|
|
328
|
+
timedOut: true,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
111
331
|
export async function withPowerSyncWriteTransaction(db, run) {
|
|
112
332
|
const txDb = db;
|
|
113
333
|
if (typeof txDb.writeTransaction === 'function') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SyncStatusOptions } from '@powersync/common';
|
|
2
2
|
import { DinoxError } from '../utils/errors.js';
|
|
3
|
-
export type SyncGateFailureReason = 'offline' | 'first-sync-timeout' | 'data-flow-timeout' | 'not-synced' | 'download-error';
|
|
3
|
+
export type SyncGateFailureReason = 'offline' | 'first-sync-timeout' | 'checkpoint-timeout' | 'data-flow-timeout' | 'index-timeout' | 'not-connected' | 'not-synced' | 'download-error';
|
|
4
4
|
export type SyncGateResult = {
|
|
5
5
|
ok: boolean;
|
|
6
6
|
reasons: SyncGateFailureReason[];
|
|
@@ -9,7 +9,10 @@ export type SyncGateInput = {
|
|
|
9
9
|
offline: boolean;
|
|
10
10
|
firstSyncStale: boolean;
|
|
11
11
|
idle: boolean;
|
|
12
|
+
indexComplete?: boolean;
|
|
13
|
+
recentSync?: boolean;
|
|
12
14
|
status: {
|
|
15
|
+
connected: boolean;
|
|
13
16
|
hasSynced: boolean;
|
|
14
17
|
dataFlowStatus: {
|
|
15
18
|
downloadError?: {
|
|
@@ -7,12 +7,21 @@ export function evaluateSyncGate(input) {
|
|
|
7
7
|
reasons: ['offline'],
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
if (input.firstSyncStale) {
|
|
10
|
+
if (input.firstSyncStale && !input.status.hasSynced) {
|
|
11
11
|
reasons.push('first-sync-timeout');
|
|
12
12
|
}
|
|
13
|
+
if (input.recentSync === false) {
|
|
14
|
+
reasons.push('checkpoint-timeout');
|
|
15
|
+
}
|
|
13
16
|
if (!input.idle) {
|
|
14
17
|
reasons.push('data-flow-timeout');
|
|
15
18
|
}
|
|
19
|
+
if (input.indexComplete === false) {
|
|
20
|
+
reasons.push('index-timeout');
|
|
21
|
+
}
|
|
22
|
+
if (!input.status.connected) {
|
|
23
|
+
reasons.push('not-connected');
|
|
24
|
+
}
|
|
16
25
|
if (!input.status.hasSynced) {
|
|
17
26
|
reasons.push('not-synced');
|
|
18
27
|
}
|
|
@@ -31,6 +40,6 @@ export function createSyncRequiredError(gate, details) {
|
|
|
31
40
|
reasons: gate.reasons,
|
|
32
41
|
...(details !== undefined ? { status: details } : {}),
|
|
33
42
|
},
|
|
34
|
-
suggestion: 'Run `dino sync --strict --sync-timeout
|
|
43
|
+
suggestion: 'Run `dino sync --strict --sync-timeout 20000 --format json`, then retry the command.',
|
|
35
44
|
});
|
|
36
45
|
}
|
|
@@ -2,6 +2,8 @@ import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
|
2
2
|
import { type NoteTokenIndexResult } from './tokenIndex.js';
|
|
3
3
|
export type SyncPassResult = {
|
|
4
4
|
idle: boolean;
|
|
5
|
+
downloadIdle: boolean;
|
|
6
|
+
uploadIdle: boolean;
|
|
5
7
|
tokenIndex: NoteTokenIndexResult;
|
|
6
8
|
};
|
|
7
9
|
export declare function runSingleSyncPass(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<SyncPassResult>;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createTimeoutSignal } from '../utils/abort.js';
|
|
2
|
+
import { getStatusSnapshot, waitForDownloadIdle } from './runtime.js';
|
|
2
3
|
import { syncNoteTokenIndex } from './tokenIndex.js';
|
|
3
4
|
export async function runSingleSyncPass(db, timeoutMs) {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
5
|
+
const deadline = Date.now() + Math.max(1, timeoutMs);
|
|
6
|
+
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
7
|
+
const downloadIdle = await waitForDownloadIdle(db, Math.max(1, Math.min(remainingMs(), 60_000)));
|
|
8
|
+
const indexTimeoutMs = remainingMs();
|
|
9
|
+
const timeout = indexTimeoutMs > 0 ? createTimeoutSignal(indexTimeoutMs) : null;
|
|
10
|
+
const tokenIndex = await syncNoteTokenIndex(db, {
|
|
11
|
+
signal: timeout?.signal ?? AbortSignal.abort(),
|
|
12
|
+
}).finally(() => timeout?.cancel());
|
|
13
|
+
const status = getStatusSnapshot(db);
|
|
14
|
+
const uploadIdle = !status.dataFlowStatus.uploading;
|
|
6
15
|
return {
|
|
7
|
-
idle,
|
|
16
|
+
idle: downloadIdle && uploadIdle,
|
|
17
|
+
downloadIdle,
|
|
18
|
+
uploadIdle,
|
|
8
19
|
tokenIndex,
|
|
9
20
|
};
|
|
10
21
|
}
|