@dinoxx/dinox-cli 1.0.22 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- 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 +74 -15
- 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.js +16 -12
- 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 +42 -0
- package/dist/daemon/lifecycle.js +115 -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 +19 -0
- package/dist/powersync/exclusiveRuntime.js +42 -0
- package/dist/powersync/runtime.d.ts +24 -4
- package/dist/powersync/runtime.js +234 -36
- package/dist/powersync/syncGate.d.ts +3 -1
- package/dist/powersync/syncGate.js +7 -1
- package/dist/powersync/tokenIndex.d.ts +11 -0
- package/dist/powersync/tokenIndex.js +159 -22
- 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 +12 -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,19 @@
|
|
|
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
|
+
waitForInitialSync?: boolean;
|
|
16
|
+
expectedTokenFingerprint?: string | null;
|
|
17
|
+
cleanupFailureMode?: 'warn' | 'throw';
|
|
18
|
+
}, run: (connected: ConnectResult) => Promise<T>, overrides?: Partial<ExclusivePowerSyncDependencies>): Promise<T>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
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, ...connectArgs } = args;
|
|
16
|
+
return dependencies.withDaemonExclusiveAccess(args.config, args.timeoutMs, async (daemonLifecycle) => {
|
|
17
|
+
const connected = await dependencies.connectPowerSync(connectArgs);
|
|
18
|
+
let outcome;
|
|
19
|
+
try {
|
|
20
|
+
outcome = { ok: true, value: await run(connected) };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
outcome = { ok: false, error };
|
|
24
|
+
}
|
|
25
|
+
let cleanupError;
|
|
26
|
+
try {
|
|
27
|
+
await dependencies.closePowerSyncAndReleaseOwner(connected.db, connected.releaseOwner);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
cleanupError = error;
|
|
31
|
+
daemonLifecycle.suppressRestore();
|
|
32
|
+
dependencies.printWarning(`warning: failed to close the local PowerSync runtime safely: ${errorMessage(error)}`);
|
|
33
|
+
}
|
|
34
|
+
if (!outcome.ok) {
|
|
35
|
+
throw outcome.error;
|
|
36
|
+
}
|
|
37
|
+
if (cleanupError && cleanupFailureMode === 'throw') {
|
|
38
|
+
throw cleanupError;
|
|
39
|
+
}
|
|
40
|
+
return outcome.value;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -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,28 @@ 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 waitForLastSyncedAfter(db: AbstractPowerSyncDatabase, startedAtIso: string, timeoutMs: number): Promise<boolean>;
|
|
43
|
+
export type UploadQueueWaitResult = {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
pendingCount: number | null;
|
|
46
|
+
uploadError?: {
|
|
47
|
+
name: string;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
queueError?: {
|
|
51
|
+
name: string;
|
|
52
|
+
message: string;
|
|
53
|
+
};
|
|
54
|
+
timedOut: boolean;
|
|
55
|
+
};
|
|
56
|
+
export declare function waitForUploadQueueEmpty(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<UploadQueueWaitResult>;
|
|
38
57
|
export declare function withPowerSyncWriteTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
|
|
58
|
+
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,52 +120,107 @@ 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);
|
|
90
179
|
}
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
93
224
|
}
|
|
94
225
|
}
|
|
95
226
|
export async function waitForIdleDataFlow(db, timeoutMs) {
|
|
@@ -97,7 +228,32 @@ export async function waitForIdleDataFlow(db, timeoutMs) {
|
|
|
97
228
|
try {
|
|
98
229
|
await db.waitForStatus(status => {
|
|
99
230
|
const flow = status.dataFlowStatus;
|
|
100
|
-
return !flow.downloading && !flow.uploading;
|
|
231
|
+
return Boolean(status.connected) && !flow.downloading && !flow.uploading;
|
|
232
|
+
}, signal);
|
|
233
|
+
return !signal.aborted;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
cancel();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export async function waitForLastSyncedAfter(db, startedAtIso, timeoutMs) {
|
|
243
|
+
const startedAtMs = Date.parse(startedAtIso);
|
|
244
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const status = getStatusSnapshot(db);
|
|
248
|
+
if (status.connected && status.lastSyncedAt && Date.parse(status.lastSyncedAt) >= startedAtMs) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const { signal, cancel } = createTimeoutSignal(timeoutMs);
|
|
252
|
+
try {
|
|
253
|
+
await db.waitForStatus((nextStatus) => {
|
|
254
|
+
const value = nextStatus.lastSyncedAt;
|
|
255
|
+
const lastSyncedAt = value instanceof Date ? value.toISOString() : value ?? null;
|
|
256
|
+
return Boolean(nextStatus.connected && lastSyncedAt && Date.parse(lastSyncedAt) >= startedAtMs);
|
|
101
257
|
}, signal);
|
|
102
258
|
return !signal.aborted;
|
|
103
259
|
}
|
|
@@ -108,6 +264,48 @@ export async function waitForIdleDataFlow(db, timeoutMs) {
|
|
|
108
264
|
cancel();
|
|
109
265
|
}
|
|
110
266
|
}
|
|
267
|
+
export async function waitForUploadQueueEmpty(db, timeoutMs) {
|
|
268
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
269
|
+
let pendingCount = Number.POSITIVE_INFINITY;
|
|
270
|
+
while (Date.now() <= deadline) {
|
|
271
|
+
const status = getStatusSnapshot(db);
|
|
272
|
+
try {
|
|
273
|
+
const stats = await db.getUploadQueueStats(false);
|
|
274
|
+
pendingCount = stats.count;
|
|
275
|
+
if (status.dataFlowStatus.uploadError) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
pendingCount,
|
|
279
|
+
uploadError: status.dataFlowStatus.uploadError,
|
|
280
|
+
timedOut: false,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (stats.count === 0) {
|
|
284
|
+
return { ok: true, pendingCount: 0, timedOut: false };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
pendingCount: Number.isFinite(pendingCount) ? pendingCount : null,
|
|
291
|
+
...(status.dataFlowStatus.uploadError
|
|
292
|
+
? { uploadError: status.dataFlowStatus.uploadError }
|
|
293
|
+
: {}),
|
|
294
|
+
queueError: {
|
|
295
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
296
|
+
message: error instanceof Error ? error.message : String(error),
|
|
297
|
+
},
|
|
298
|
+
timedOut: false,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
pendingCount: Number.isFinite(pendingCount) ? pendingCount : null,
|
|
306
|
+
timedOut: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
111
309
|
export async function withPowerSyncWriteTransaction(db, run) {
|
|
112
310
|
const txDb = db;
|
|
113
311
|
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' | 'not-connected' | 'not-synced' | 'download-error';
|
|
4
4
|
export type SyncGateResult = {
|
|
5
5
|
ok: boolean;
|
|
6
6
|
reasons: SyncGateFailureReason[];
|
|
@@ -9,7 +9,9 @@ export type SyncGateInput = {
|
|
|
9
9
|
offline: boolean;
|
|
10
10
|
firstSyncStale: boolean;
|
|
11
11
|
idle: boolean;
|
|
12
|
+
recentSync?: boolean;
|
|
12
13
|
status: {
|
|
14
|
+
connected: boolean;
|
|
13
15
|
hasSynced: boolean;
|
|
14
16
|
dataFlowStatus: {
|
|
15
17
|
downloadError?: {
|
|
@@ -7,12 +7,18 @@ 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.status.connected) {
|
|
20
|
+
reasons.push('not-connected');
|
|
21
|
+
}
|
|
16
22
|
if (!input.status.hasSynced) {
|
|
17
23
|
reasons.push('not-synced');
|
|
18
24
|
}
|
|
@@ -10,8 +10,19 @@ export type NoteTokenIndexResult = {
|
|
|
10
10
|
id: string;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
|
+
export type NoteTokenIndexInspection = {
|
|
14
|
+
notes: number;
|
|
15
|
+
ftsRows: number;
|
|
16
|
+
metaRows: number;
|
|
17
|
+
missingRows: number;
|
|
18
|
+
extraRows: number;
|
|
19
|
+
drift: number;
|
|
20
|
+
ftsAvailable: boolean;
|
|
21
|
+
};
|
|
13
22
|
export declare function syncNoteTokenIndex(db: AbstractPowerSyncDatabase, options?: {
|
|
14
23
|
batchSize?: number;
|
|
15
24
|
nowIso?: () => string;
|
|
16
25
|
tokenize?: (text: string, limit?: number) => string[];
|
|
17
26
|
}): Promise<NoteTokenIndexResult>;
|
|
27
|
+
export declare function inspectNoteTokenIndex(db: AbstractPowerSyncDatabase): Promise<NoteTokenIndexInspection>;
|
|
28
|
+
export declare function rebuildNoteTokenIndex(db: AbstractPowerSyncDatabase, options?: Parameters<typeof syncNoteTokenIndex>[1]): Promise<NoteTokenIndexResult>;
|