@dinoxx/dinox-cli 2.13.0 → 2.14.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/dist/cli.js +5 -2
- package/dist/commands/auth/index.d.ts +2 -7
- package/dist/commands/auth/index.js +26 -51
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +20 -1
- package/dist/commands/graph/index.js +2 -1
- package/dist/commands/notes/contentPatch.js +2 -1
- package/dist/commands/notes/export.js +3 -2
- package/dist/commands/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +1 -0
- package/dist/commands/schema/spec.js +9 -1
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +24 -5
- package/dist/daemon/client.js +32 -2
- package/dist/daemon/connectivity.d.ts +20 -0
- package/dist/daemon/connectivity.js +42 -0
- package/dist/daemon/ensure.js +48 -12
- package/dist/daemon/forward.d.ts +34 -0
- package/dist/daemon/forward.js +146 -0
- package/dist/daemon/fresh.d.ts +4 -0
- package/dist/daemon/fresh.js +10 -0
- package/dist/daemon/index.d.ts +11 -6
- package/dist/daemon/index.js +90 -25
- package/dist/daemon/launch.d.ts +13 -1
- package/dist/daemon/launch.js +31 -2
- package/dist/daemon/lifecycle.d.ts +3 -1
- package/dist/daemon/lifecycle.js +27 -3
- package/dist/daemon/pid.d.ts +8 -0
- package/dist/daemon/pid.js +20 -0
- package/dist/daemon/route.js +2 -1
- package/dist/daemon/server.d.ts +44 -0
- package/dist/daemon/server.js +175 -3
- package/dist/daemon/supervise.d.ts +6 -6
- package/dist/daemon/supervise.js +12 -10
- package/dist/daemon/supervisor.d.ts +21 -5
- package/dist/daemon/supervisor.js +125 -6
- package/dist/daemon/types.d.ts +16 -0
- package/dist/dinox.js +41 -64
- package/dist/gateway/mcpCommandTools.d.ts +9 -0
- package/dist/gateway/mcpCommandTools.js +40 -10
- package/dist/powersync/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- package/dist/requests/store.d.ts +10 -0
- package/dist/requests/store.js +190 -0
- package/dist/runCli.d.ts +2 -0
- package/dist/runCli.js +64 -0
- package/dist/utils/argValue.js +2 -1
- package/dist/utils/errorCodes.d.ts +20 -0
- package/dist/utils/errorCodes.js +5 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +31 -7
- package/dist/utils/requestContext.d.ts +24 -0
- package/dist/utils/requestContext.js +13 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -21,8 +21,9 @@ import { registerTagsCommand } from './commands/tags/index.js';
|
|
|
21
21
|
import { registerTemplateCommands } from './commands/templates/index.js';
|
|
22
22
|
import { registerTodoCommands } from './commands/todo/index.js';
|
|
23
23
|
import { getPackageVersion } from './utils/version.js';
|
|
24
|
+
import { validateRequestId } from './requests/store.js';
|
|
24
25
|
import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
|
|
25
|
-
import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
26
|
+
import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
26
27
|
export function createCli() {
|
|
27
28
|
clearRequestedStructuredOutputFormat();
|
|
28
29
|
const program = new Command();
|
|
@@ -33,9 +34,10 @@ export function createCli() {
|
|
|
33
34
|
.version(getPackageVersion())
|
|
34
35
|
.showHelpAfterError()
|
|
35
36
|
.configureOutput({
|
|
37
|
+
writeOut: writeToStdout,
|
|
36
38
|
writeErr: (text) => {
|
|
37
39
|
if (!structuredCommanderErrorWritten) {
|
|
38
|
-
|
|
40
|
+
writeToStderr(text);
|
|
39
41
|
}
|
|
40
42
|
},
|
|
41
43
|
outputError: (text, write) => {
|
|
@@ -68,6 +70,7 @@ export function createCli() {
|
|
|
68
70
|
.option('--offline', 'skip connect/sync and only use local cache')
|
|
69
71
|
.option('--require-sync', 'fail if the local PowerSync cache cannot be confirmed fresh before reading')
|
|
70
72
|
.option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
|
|
73
|
+
.option('--request-id <id>', 'idempotency key: replays the stored result instead of running the command again (24h)', validateRequestId)
|
|
71
74
|
.option('--verbose', 'enable verbose logging');
|
|
72
75
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
73
76
|
const options = actionCommand.optsWithGlobals?.() ?? {};
|
|
@@ -3,24 +3,19 @@ import { fetchUserInfoWithAuthorization } from '../../auth/userInfo.js';
|
|
|
3
3
|
import { loadConfig, updateConfig } from '../../config/store.js';
|
|
4
4
|
import { resolveConfig } from '../../config/resolve.js';
|
|
5
5
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked } from '../../daemon/lifecycle.js';
|
|
6
|
-
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
6
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
7
7
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
8
|
-
import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js';
|
|
9
|
-
import { runSingleSyncPass } from '../../powersync/syncPass.js';
|
|
10
8
|
import type { CommandGroupDescriptor } from '../descriptor/types.js';
|
|
11
9
|
import { printHumanReadableWarning } from '../../utils/output.js';
|
|
12
10
|
export declare function releaseOwnerLeases(releases: ReadonlyArray<() => Promise<void>>): Promise<unknown[]>;
|
|
13
11
|
type AuthLoginDependencies = {
|
|
14
12
|
acquireDaemonStartLock: typeof acquireDaemonStartLock;
|
|
13
|
+
acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
|
|
15
14
|
loadConfig: typeof loadConfig;
|
|
16
15
|
resolveConfig: typeof resolveConfig;
|
|
17
16
|
fetchCredentials: typeof fetchCredentialsWithAuthorization;
|
|
18
17
|
fetchUserInfo: typeof fetchUserInfoWithAuthorization;
|
|
19
18
|
suspendDaemon: typeof suspendDaemonForExclusiveAccessLocked;
|
|
20
|
-
connectPowerSync: typeof connectPowerSync;
|
|
21
|
-
runSingleSyncPass: typeof runSingleSyncPass;
|
|
22
|
-
getStatusSnapshot: typeof getStatusSnapshot;
|
|
23
|
-
closePowerSyncAndReleaseOwner: typeof closePowerSyncAndReleaseOwner;
|
|
24
19
|
updateConfig: typeof updateConfig;
|
|
25
20
|
restoreDaemon: typeof restoreSuspendedDaemonLocked;
|
|
26
21
|
printWarning: typeof printHumanReadableWarning;
|
|
@@ -10,11 +10,10 @@ import { getDaemonInfo } from '../../daemon/client.js';
|
|
|
10
10
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
|
|
11
11
|
import { stopDaemonLocked } from '../../daemon/index.js';
|
|
12
12
|
import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../../daemon/autoInstall.js';
|
|
13
|
-
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
13
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
14
14
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
15
15
|
import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
|
|
16
|
-
import { acquirePowerSyncOwnerLease,
|
|
17
|
-
import { runSingleSyncPass } from '../../powersync/syncPass.js';
|
|
16
|
+
import { acquirePowerSyncOwnerLease, getStatusSnapshot, } from '../../powersync/runtime.js';
|
|
18
17
|
import { registerCommandGroup } from '../descriptor/register.js';
|
|
19
18
|
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
20
19
|
import { normalizeAuthToken, readEnvAuthToken } from '../../utils/authToken.js';
|
|
@@ -23,6 +22,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
|
|
|
23
22
|
import { redactAuthorization } from '../../utils/redact.js';
|
|
24
23
|
import { withAsyncRelease } from '../../utils/asyncRelease.js';
|
|
25
24
|
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
25
|
+
import { assertStdinAvailable } from '../../utils/requestContext.js';
|
|
26
26
|
const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
27
27
|
const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
28
28
|
const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
|
|
@@ -81,6 +81,7 @@ export async function releaseOwnerLeases(releases) {
|
|
|
81
81
|
return errors;
|
|
82
82
|
}
|
|
83
83
|
async function readAuthTokenFromStdin() {
|
|
84
|
+
assertStdinAvailable();
|
|
84
85
|
if (process.stdin.isTTY) {
|
|
85
86
|
throw new DinoxError('--token-stdin requires piped stdin', {
|
|
86
87
|
code: 'USAGE_ERROR',
|
|
@@ -101,15 +102,12 @@ async function readAuthTokenFromStdin() {
|
|
|
101
102
|
}
|
|
102
103
|
const authLoginDependencies = {
|
|
103
104
|
acquireDaemonStartLock,
|
|
105
|
+
acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
|
|
104
106
|
loadConfig,
|
|
105
107
|
resolveConfig,
|
|
106
108
|
fetchCredentials: fetchCredentialsWithAuthorization,
|
|
107
109
|
fetchUserInfo: fetchUserInfoWithAuthorization,
|
|
108
110
|
suspendDaemon: suspendDaemonForExclusiveAccessLocked,
|
|
109
|
-
connectPowerSync,
|
|
110
|
-
runSingleSyncPass,
|
|
111
|
-
getStatusSnapshot,
|
|
112
|
-
closePowerSyncAndReleaseOwner,
|
|
113
111
|
updateConfig,
|
|
114
112
|
restoreDaemon: restoreSuspendedDaemonLocked,
|
|
115
113
|
printWarning: printHumanReadableWarning,
|
|
@@ -122,7 +120,15 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
122
120
|
const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
|
|
123
121
|
let previousConfig = null;
|
|
124
122
|
let suspended = null;
|
|
125
|
-
|
|
123
|
+
// Held from stopping the daemon until the new login is saved, so a daemon
|
|
124
|
+
// the service manager respawns meanwhile starts with the new identity.
|
|
125
|
+
// Released before any restore, which waits for the daemon to come up.
|
|
126
|
+
let releaseIntentLock = null;
|
|
127
|
+
const releaseIntent = async () => {
|
|
128
|
+
const release = releaseIntentLock;
|
|
129
|
+
releaseIntentLock = null;
|
|
130
|
+
await release?.();
|
|
131
|
+
};
|
|
126
132
|
let lifecycleLockReleaseError;
|
|
127
133
|
let outcome;
|
|
128
134
|
try {
|
|
@@ -144,38 +150,8 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
144
150
|
});
|
|
145
151
|
const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
|
|
146
152
|
const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
|
|
153
|
+
releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
|
|
147
154
|
suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
|
|
148
|
-
const { db, dbPath, stale, releaseOwner } = await dependencies.connectPowerSync({
|
|
149
|
-
config: verifiedResolved,
|
|
150
|
-
offline: false,
|
|
151
|
-
timeoutMs,
|
|
152
|
-
});
|
|
153
|
-
let syncOutcome;
|
|
154
|
-
try {
|
|
155
|
-
await dependencies.runSingleSyncPass(db, timeoutMs);
|
|
156
|
-
syncOutcome = {
|
|
157
|
-
ok: true,
|
|
158
|
-
lastSyncedAt: dependencies.getStatusSnapshot(db).lastSyncedAt,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
syncOutcome = { ok: false, error };
|
|
163
|
-
}
|
|
164
|
-
let cleanupError;
|
|
165
|
-
try {
|
|
166
|
-
await dependencies.closePowerSyncAndReleaseOwner(db, releaseOwner);
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
cleanupError = error;
|
|
170
|
-
restoreAllowed = false;
|
|
171
|
-
dependencies.printWarning(`warning: failed to close the login verification runtime safely: ${authLoginErrorMessage(error)}`);
|
|
172
|
-
}
|
|
173
|
-
if (!syncOutcome.ok) {
|
|
174
|
-
throw syncOutcome.error;
|
|
175
|
-
}
|
|
176
|
-
if (cleanupError) {
|
|
177
|
-
throw cleanupError;
|
|
178
|
-
}
|
|
179
155
|
await dependencies.updateConfig((current) => {
|
|
180
156
|
if (!persistedAuthStateMatches(current, previousConfig)) {
|
|
181
157
|
throw new DinoxError('Authentication changed while login was being verified.', {
|
|
@@ -186,22 +162,26 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
186
162
|
}
|
|
187
163
|
return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
|
|
188
164
|
}, { timeoutMs });
|
|
165
|
+
await releaseIntent();
|
|
189
166
|
outcome = {
|
|
190
167
|
ok: true,
|
|
191
168
|
value: {
|
|
192
169
|
ok: true,
|
|
193
170
|
verified: true,
|
|
194
171
|
endpoint: verifiedResolved.powersync.endpoint,
|
|
195
|
-
dbPath,
|
|
196
|
-
stale,
|
|
197
|
-
lastSyncedAt:
|
|
172
|
+
dbPath: getPowerSyncDbPath(identity.userId),
|
|
173
|
+
stale: true,
|
|
174
|
+
lastSyncedAt: null,
|
|
198
175
|
userId: identity.userId,
|
|
199
176
|
},
|
|
200
177
|
};
|
|
201
178
|
}
|
|
202
179
|
catch (error) {
|
|
203
180
|
outcome = { ok: false, error };
|
|
204
|
-
|
|
181
|
+
await releaseIntent().catch((releaseError) => {
|
|
182
|
+
dependencies.printWarning(`warning: failed to release daemon intent lock: ${authLoginErrorMessage(releaseError)}`);
|
|
183
|
+
});
|
|
184
|
+
if (previousConfig && suspended) {
|
|
205
185
|
try {
|
|
206
186
|
const current = await dependencies.loadConfig();
|
|
207
187
|
if (persistedAuthStateMatches(current, previousConfig)) {
|
|
@@ -315,12 +295,7 @@ async function runAuthLogin(context) {
|
|
|
315
295
|
printHumanReadableLine(`- endpoint: ${result.endpoint}`);
|
|
316
296
|
printHumanReadableLine(`- db: ${result.dbPath}`);
|
|
317
297
|
printHumanReadableLine(`- userId: ${result.userId}`);
|
|
318
|
-
|
|
319
|
-
printHumanReadableLine(`- lastSyncedAt: ${result.lastSyncedAt}`);
|
|
320
|
-
}
|
|
321
|
-
if (result.stale) {
|
|
322
|
-
printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
|
|
323
|
-
}
|
|
298
|
+
printHumanReadableLine('- initial sync continues in the background; run `dino sync` to wait for it');
|
|
324
299
|
const serviceNote = describeDaemonServiceAutoInstall(daemonService);
|
|
325
300
|
if (serviceNote)
|
|
326
301
|
printHumanReadableWarning(serviceNote);
|
|
@@ -567,7 +542,7 @@ export const authCommandGroup = {
|
|
|
567
542
|
commands: [
|
|
568
543
|
{
|
|
569
544
|
name: 'login',
|
|
570
|
-
description: 'Log in by scanning a QR code
|
|
545
|
+
description: 'Log in by scanning a QR code or with a token; the initial sync continues in the background',
|
|
571
546
|
arguments: [{ syntax: '[token]', description: 'Login token (omit to scan a QR code, or when using --token-stdin)' }],
|
|
572
547
|
options: [{ flags: '--token-stdin', description: 'Read the login token from piped stdin instead of argv', type: 'boolean' }],
|
|
573
548
|
schema: {
|
|
@@ -577,7 +552,7 @@ export const authCommandGroup = {
|
|
|
577
552
|
globalOptions: AUTH_LOGIN_GLOBAL_OPTIONS,
|
|
578
553
|
output: {
|
|
579
554
|
type: 'structured',
|
|
580
|
-
summary: 'Login verification result',
|
|
555
|
+
summary: 'Login verification result (initial sync is not awaited)',
|
|
581
556
|
fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId', 'daemonService'],
|
|
582
557
|
},
|
|
583
558
|
examples: [
|
|
@@ -5,7 +5,7 @@ import { resolveConfig } from '../config/resolve.js';
|
|
|
5
5
|
import type { ResolvedDinoxConfig } from '../config/types.js';
|
|
6
6
|
import { shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
|
|
7
7
|
import { restartDaemonLocked } from '../daemon/index.js';
|
|
8
|
-
import { acquireDaemonStartLock } from '../daemon/pid.js';
|
|
8
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../daemon/pid.js';
|
|
9
9
|
import type { DaemonPidInfo } from '../daemon/pid.js';
|
|
10
10
|
import { type DaemonPingResult } from '../daemon/types.js';
|
|
11
11
|
import { connectPowerSync } from '../powersync/runtime.js';
|
|
@@ -89,6 +89,8 @@ type DoctorRuntime = {
|
|
|
89
89
|
daemonBefore: DoctorDaemonSnapshot;
|
|
90
90
|
daemonWasStopped: boolean;
|
|
91
91
|
releaseDaemonStartLock: () => Promise<void>;
|
|
92
|
+
/** Held while doctor owns the database; see acquireDaemonExclusiveIntentLock. */
|
|
93
|
+
releaseIntentLock?: () => Promise<void>;
|
|
92
94
|
daemonStartLockReleased: boolean;
|
|
93
95
|
released: boolean;
|
|
94
96
|
};
|
|
@@ -102,6 +104,7 @@ type DoctorPreparationDependencies = {
|
|
|
102
104
|
resolveConfig: typeof resolveConfig;
|
|
103
105
|
hydrateAuthIdentity: typeof hydrateResolvedAuthIdentity;
|
|
104
106
|
acquireDaemonStartLock: typeof acquireDaemonStartLock;
|
|
107
|
+
acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
|
|
105
108
|
captureDaemonSnapshot: typeof captureDaemonSnapshot;
|
|
106
109
|
shutdownDaemon: typeof shutdownDaemonForExclusiveAccess;
|
|
107
110
|
connectPowerSync: typeof connectPowerSync;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig } from '../config/store.js';
|
|
|
5
5
|
import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
|
|
6
6
|
import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
|
|
7
7
|
import { restartDaemonLocked } from '../daemon/index.js';
|
|
8
|
-
import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
|
|
8
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
|
|
9
9
|
import { DEFAULT_DAEMON_PORT } from '../daemon/types.js';
|
|
10
10
|
import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
|
|
11
11
|
import { inspectNoteTokenIndex, rebuildNoteTokenIndex, } from '../powersync/tokenIndex.js';
|
|
@@ -20,6 +20,7 @@ const doctorPreparationDependencies = {
|
|
|
20
20
|
resolveConfig,
|
|
21
21
|
hydrateAuthIdentity: hydrateResolvedAuthIdentity,
|
|
22
22
|
acquireDaemonStartLock,
|
|
23
|
+
acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
|
|
23
24
|
captureDaemonSnapshot,
|
|
24
25
|
shutdownDaemon: shutdownDaemonForExclusiveAccess,
|
|
25
26
|
connectPowerSync,
|
|
@@ -39,6 +40,14 @@ function doctorTimeoutMs(globals, config) {
|
|
|
39
40
|
export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
40
41
|
const dependencies = { ...doctorPreparationDependencies, ...overrides };
|
|
41
42
|
const releaseDaemonStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(context.globals.syncTimeout) ?? DEFAULT_DOCTOR_TIMEOUT_MS);
|
|
43
|
+
let releaseIntentLock;
|
|
44
|
+
const releaseIntent = async () => {
|
|
45
|
+
const release = releaseIntentLock;
|
|
46
|
+
releaseIntentLock = undefined;
|
|
47
|
+
await release?.().catch((error) => {
|
|
48
|
+
printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
42
51
|
try {
|
|
43
52
|
const rawConfig = await dependencies.loadConfig();
|
|
44
53
|
const baseConfig = dependencies.resolveConfig(rawConfig);
|
|
@@ -56,6 +65,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
|
56
65
|
authError = errorMessage(error);
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
|
|
59
69
|
const daemonBefore = await dependencies.captureDaemonSnapshot();
|
|
60
70
|
const daemonDbPath = daemonBefore.info ? getPowerSyncDbPath(daemonBefore.info.userId) : null;
|
|
61
71
|
const targetDbPath = getPowerSyncDbPath(config.auth.userId);
|
|
@@ -83,6 +93,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
|
83
93
|
daemonBefore,
|
|
84
94
|
daemonWasStopped,
|
|
85
95
|
releaseDaemonStartLock,
|
|
96
|
+
releaseIntentLock,
|
|
86
97
|
daemonStartLockReleased: false,
|
|
87
98
|
released: false,
|
|
88
99
|
};
|
|
@@ -109,16 +120,19 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
|
109
120
|
daemonBefore,
|
|
110
121
|
daemonWasStopped,
|
|
111
122
|
releaseDaemonStartLock,
|
|
123
|
+
releaseIntentLock,
|
|
112
124
|
daemonStartLockReleased: false,
|
|
113
125
|
released: false,
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
128
|
catch (error) {
|
|
129
|
+
await releaseIntent();
|
|
117
130
|
await restoreStoppedDoctorDaemon(config, daemonBefore, daemonWasStopped, dependencies.restartDaemon);
|
|
118
131
|
throw error;
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
catch (error) {
|
|
135
|
+
await releaseIntent();
|
|
122
136
|
await releaseDaemonStartLock().catch((releaseError) => {
|
|
123
137
|
printHumanReadableWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(releaseError)}`);
|
|
124
138
|
});
|
|
@@ -357,6 +371,11 @@ export async function releaseDoctorRuntime(runtime) {
|
|
|
357
371
|
}
|
|
358
372
|
await closePowerSyncAndReleaseOwner(runtime.db, runtime.releaseOwner);
|
|
359
373
|
runtime.released = true;
|
|
374
|
+
const releaseIntentLock = runtime.releaseIntentLock;
|
|
375
|
+
runtime.releaseIntentLock = undefined;
|
|
376
|
+
await releaseIntentLock?.().catch((error) => {
|
|
377
|
+
printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
|
|
378
|
+
});
|
|
360
379
|
}
|
|
361
380
|
export async function releaseDoctorDaemonStartLock(runtime) {
|
|
362
381
|
if (runtime.daemonStartLockReleased) {
|
|
@@ -4,6 +4,7 @@ import { registerCommandGroup } from '../descriptor/register.js';
|
|
|
4
4
|
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
5
5
|
import { DinoxError } from '../../utils/errors.js';
|
|
6
6
|
import { printSuccess } from '../../utils/output.js';
|
|
7
|
+
import { localCommandExecution, requestContext } from '../../utils/requestContext.js';
|
|
7
8
|
import { commandStale, syncBeforeCommand, withCommandRuntime } from '../runtime.js';
|
|
8
9
|
import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from './repo.js';
|
|
9
10
|
const GRAPH_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
@@ -26,7 +27,7 @@ function parseDepth(value) {
|
|
|
26
27
|
}
|
|
27
28
|
async function runGraphCommand(context, onlineAction, offlineAction) {
|
|
28
29
|
const globals = context.globals;
|
|
29
|
-
if (Boolean(globals.offline) || Boolean(globals.requireSync)) {
|
|
30
|
+
if (requestContext.getStore()?.daemon || localCommandExecution.getStore() || Boolean(globals.offline) || Boolean(globals.requireSync)) {
|
|
30
31
|
await withCommandRuntime(context, async (runtime) => {
|
|
31
32
|
await syncBeforeCommand(runtime);
|
|
32
33
|
printSuccess({
|
|
@@ -11,6 +11,7 @@ import { nowIsoUtc } from '../../utils/time.js';
|
|
|
11
11
|
import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
|
|
12
12
|
import { applyJsonPatchToDocument, buildNoteContentBlocks, buildNoteContentOutline, getNoteContentHash, parseStoredNoteDocument, projectNoteContent, } from './contentEngine.js';
|
|
13
13
|
import { parseStoredTags } from './repo.js';
|
|
14
|
+
import { resolveUserPath } from '../../utils/requestContext.js';
|
|
14
15
|
const READ_TOKEN_TTL_MS = 30 * 60 * 1000;
|
|
15
16
|
const MAX_STORED_READ_TOKENS = 500;
|
|
16
17
|
const TOKEN_STORE_FILE = 'note-content-read-tokens.json';
|
|
@@ -111,7 +112,7 @@ async function loadTokenStore(nowMs = Date.now()) {
|
|
|
111
112
|
const filePath = tokenStorePath();
|
|
112
113
|
let tokens = [];
|
|
113
114
|
try {
|
|
114
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
115
|
+
const raw = await fs.readFile(resolveUserPath(filePath), 'utf8');
|
|
115
116
|
const parsed = JSON.parse(raw);
|
|
116
117
|
if (Array.isArray(parsed.tokens)) {
|
|
117
118
|
tokens = parsed.tokens.filter((token) => typeof token?.token === 'string' &&
|
|
@@ -6,6 +6,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
|
|
|
6
6
|
import { commandStale, } from '../runtime.js';
|
|
7
7
|
import { getNoteExportItems, } from './repo.js';
|
|
8
8
|
import { parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
9
|
+
import { resolveUserPath } from '../../utils/requestContext.js';
|
|
9
10
|
export function parseExportFormat(value) {
|
|
10
11
|
if (value === undefined || value === null || value === '') {
|
|
11
12
|
return 'markdown';
|
|
@@ -107,7 +108,7 @@ async function pathExists(filePath) {
|
|
|
107
108
|
}
|
|
108
109
|
async function resolveOutputTargets(args) {
|
|
109
110
|
const outputLooksLikeDirectory = /[\\/]$/.test(args.outputPath);
|
|
110
|
-
const outputPath =
|
|
111
|
+
const outputPath = resolveUserPath(args.outputPath);
|
|
111
112
|
if (args.notes.length === 1) {
|
|
112
113
|
if (outputLooksLikeDirectory) {
|
|
113
114
|
return [{
|
|
@@ -160,7 +161,7 @@ export async function writeExportFiles(args) {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
if (args.notes.length > 1) {
|
|
163
|
-
await fs.mkdir(
|
|
164
|
+
await fs.mkdir(resolveUserPath(args.outputPath), { recursive: true, mode: 0o700 });
|
|
164
165
|
}
|
|
165
166
|
else {
|
|
166
167
|
await fs.mkdir(path.dirname(targets[0].path), { recursive: true, mode: 0o700 });
|
|
@@ -43,4 +43,12 @@ export declare function waitForMutationUpload(runtime: CommandRuntime, options?:
|
|
|
43
43
|
durability?: unknown;
|
|
44
44
|
}): Promise<MutationUploadReceipt>;
|
|
45
45
|
export declare function printStaleCacheWarning(runtime: CommandRuntime, message?: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Freshness on the daemon's long-lived stream. The server pushes every change,
|
|
48
|
+
* so a connected, error-free stream is current without a new checkpoint; an
|
|
49
|
+
* idle stream may not produce one before the timeout. After a reconnect (wake,
|
|
50
|
+
* network change) it may have been a dead connection, so then a checkpoint
|
|
51
|
+
* completed after the reconnect is required.
|
|
52
|
+
*/
|
|
53
|
+
export declare function waitForDaemonCurrent(db: AbstractPowerSyncDatabase, _startedAt: string, timeoutMs: number): Promise<boolean>;
|
|
46
54
|
export {};
|
package/dist/commands/runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
|
|
|
8
8
|
import { createTimeoutSignal } from '../utils/abort.js';
|
|
9
9
|
import { DinoxError } from '../utils/errors.js';
|
|
10
10
|
import { printHumanReadableWarning } from '../utils/output.js';
|
|
11
|
+
import { requestContext } from '../utils/requestContext.js';
|
|
11
12
|
import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
12
13
|
import { parseWriteDurability } from './writeDurability.js';
|
|
13
14
|
const PRE_SYNC_TIMEOUT_MS = 10_000;
|
|
@@ -44,6 +45,22 @@ export function resolveWriteDurabilityForRuntime(runtime, value) {
|
|
|
44
45
|
export async function withCommandRuntime(source, run) {
|
|
45
46
|
const startedAtIso = new Date().toISOString();
|
|
46
47
|
const globals = resolveGlobals(source);
|
|
48
|
+
const daemon = requestContext.getStore()?.daemon;
|
|
49
|
+
if (daemon) {
|
|
50
|
+
const offline = Boolean(globals.offline);
|
|
51
|
+
const status = getStatusSnapshot(daemon.db);
|
|
52
|
+
const stale = offline || !status.connected || !status.hasSynced || Boolean(daemon.syncUnconfirmedSince?.());
|
|
53
|
+
return run({
|
|
54
|
+
db: daemon.db,
|
|
55
|
+
config: daemon.config,
|
|
56
|
+
rawConfig: {},
|
|
57
|
+
offline,
|
|
58
|
+
timeoutMs: parseSyncTimeoutMs(globals.syncTimeout) ?? daemon.config.sync.timeoutMs,
|
|
59
|
+
stale,
|
|
60
|
+
requireSync: Boolean(globals.requireSync),
|
|
61
|
+
sync: { firstSyncStale: stale, preSyncIdle: null, recentSync: null, gate: null, startedAtIso },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
47
64
|
const rawConfig = await loadConfig();
|
|
48
65
|
const baseConfig = resolveConfig(rawConfig);
|
|
49
66
|
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
@@ -99,7 +116,7 @@ export async function syncBeforeCommand(runtime, options = {}) {
|
|
|
99
116
|
const deadlineAtMs = Date.now() + timeoutMs;
|
|
100
117
|
const remainingTimeoutMs = () => Math.max(0, deadlineAtMs - Date.now());
|
|
101
118
|
const recentSync = runtime.requireSync
|
|
102
|
-
? await waitForLastSyncedAfter(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
|
|
119
|
+
? await (requestContext.getStore()?.daemon ? waitForDaemonCurrent : waitForLastSyncedAfter)(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
|
|
103
120
|
: true;
|
|
104
121
|
runtime.sync.recentSync = recentSync;
|
|
105
122
|
const downloadIdle = await waitForDownloadIdle(runtime.db, Math.max(1, remainingTimeoutMs()));
|
|
@@ -178,3 +195,34 @@ export function printStaleCacheWarning(runtime, message = 'warning: sync timed o
|
|
|
178
195
|
printHumanReadableWarning(message);
|
|
179
196
|
}
|
|
180
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Freshness on the daemon's long-lived stream. The server pushes every change,
|
|
200
|
+
* so a connected, error-free stream is current without a new checkpoint; an
|
|
201
|
+
* idle stream may not produce one before the timeout. After a reconnect (wake,
|
|
202
|
+
* network change) it may have been a dead connection, so then a checkpoint
|
|
203
|
+
* completed after the reconnect is required.
|
|
204
|
+
*/
|
|
205
|
+
export async function waitForDaemonCurrent(db, _startedAt, timeoutMs) {
|
|
206
|
+
const unconfirmedSince = requestContext.getStore()?.daemon?.syncUnconfirmedSince?.();
|
|
207
|
+
if (unconfirmedSince) {
|
|
208
|
+
return await waitForLastSyncedAfter(db, unconfirmedSince, timeoutMs)
|
|
209
|
+
&& !getStatusSnapshot(db).dataFlowStatus.downloadError;
|
|
210
|
+
}
|
|
211
|
+
const current = () => {
|
|
212
|
+
const status = getStatusSnapshot(db);
|
|
213
|
+
return status.connected && status.hasSynced && !status.dataFlowStatus.downloadError;
|
|
214
|
+
};
|
|
215
|
+
if (current())
|
|
216
|
+
return true;
|
|
217
|
+
const timeout = createTimeoutSignal(timeoutMs);
|
|
218
|
+
try {
|
|
219
|
+
await db.waitForStatus(() => current(), timeout.signal);
|
|
220
|
+
return !timeout.signal.aborted && current();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
timeout.cancel();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -2,3 +2,4 @@ export const FORMAT_OPTIONS = ['--format <yaml|json>', '--json'];
|
|
|
2
2
|
export const OFFLINE_OPTIONS = ['--offline'];
|
|
3
3
|
export const REQUIRE_SYNC_OPTIONS = ['--require-sync'];
|
|
4
4
|
export const SYNC_TIMEOUT_OPTIONS = ['--sync-timeout <ms>'];
|
|
5
|
+
export const REQUEST_ID_OPTIONS = ['--request-id <id>'];
|
|
@@ -33,6 +33,7 @@ const GLOBAL_OPTIONS = [
|
|
|
33
33
|
['`--offline`', 'Skip sync, use local cache only'],
|
|
34
34
|
['`--require-sync`', 'Fail if the local PowerSync cache cannot be confirmed fresh before reading'],
|
|
35
35
|
['`--sync-timeout <ms>`', 'Override default 300000 ms (5 min) sync/connect timeout'],
|
|
36
|
+
['`--request-id <id>`', 'Idempotency key for data commands: rerunning with the same id within 24h replays the stored result'],
|
|
36
37
|
['`--verbose`', 'Enable verbose logging'],
|
|
37
38
|
];
|
|
38
39
|
function buildCommandSignature(schema) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { authCommandGroup } from '../auth/index.js';
|
|
2
2
|
import { boxCommandGroup } from '../boxes/index.js';
|
|
3
|
+
import { commandExecutionTarget } from '../../daemon/forward.js';
|
|
3
4
|
import { commandSchemasFromGroupDescriptor } from '../descriptor/register.js';
|
|
4
5
|
import { standaloneCommandSchemaFromDescriptor } from '../descriptor/register.js';
|
|
5
6
|
import { configCommandGroup } from '../config/index.js';
|
|
@@ -14,6 +15,7 @@ import { promptCommandGroup } from '../prompt/index.js';
|
|
|
14
15
|
import { propertyCommandGroup } from '../properties/index.js';
|
|
15
16
|
import { savedViewCommandGroup } from '../savedViews/index.js';
|
|
16
17
|
import { schemaCommandMetadata } from './commandMetadata.js';
|
|
18
|
+
import { REQUEST_ID_OPTIONS } from './globalOptions.js';
|
|
17
19
|
import { skillsCommandGroup } from '../skills/index.js';
|
|
18
20
|
import { syncCommandDescriptor } from '../sync.js';
|
|
19
21
|
import { storageCommandGroup } from '../storage/index.js';
|
|
@@ -21,6 +23,12 @@ import { tagCommandGroup } from '../tags/index.js';
|
|
|
21
23
|
import { templateCommandGroup } from '../templates/index.js';
|
|
22
24
|
import { todoCommandGroup } from '../todo/index.js';
|
|
23
25
|
import { updateCommandDescriptor } from '../update.js';
|
|
26
|
+
/** Every command that runs inside the daemon accepts an idempotency key. */
|
|
27
|
+
function withRequestId(schema) {
|
|
28
|
+
if (commandExecutionTarget(schema.path.split('.')) !== 'daemon')
|
|
29
|
+
return schema;
|
|
30
|
+
return { ...schema, globalOptions: [...schema.globalOptions, ...REQUEST_ID_OPTIONS] };
|
|
31
|
+
}
|
|
24
32
|
export const COMMAND_SCHEMAS = [
|
|
25
33
|
...commandSchemasFromGroupDescriptor(authCommandGroup),
|
|
26
34
|
...commandSchemasFromGroupDescriptor(configCommandGroup),
|
|
@@ -43,7 +51,7 @@ export const COMMAND_SCHEMAS = [
|
|
|
43
51
|
...commandSchemasFromGroupDescriptor(noteCommandGroup),
|
|
44
52
|
...commandSchemasFromGroupDescriptor(graphCommandGroup),
|
|
45
53
|
...commandSchemasFromGroupDescriptor(todoCommandGroup),
|
|
46
|
-
];
|
|
54
|
+
].map(withRequestId);
|
|
47
55
|
const COMMAND_SCHEMA_MAP = new Map(COMMAND_SCHEMAS.map((schema) => [schema.path, schema]));
|
|
48
56
|
export function listCommandSchemas() {
|
|
49
57
|
return [...COMMAND_SCHEMAS];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Storage commands: inspect/test/upload orchestration with cleanup on partial failure.
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import { DinoxError } from '../../../utils/errors.js';
|
|
4
|
+
import { resolveUserPath } from '../../../utils/requestContext.js';
|
|
4
5
|
import { buildStorageTestPlan, buildStorageUploadPlan, getActiveCustomStorageConfigId, listStoredCustomStorageConfigs, resolveStorageSelection, } from './configs.js';
|
|
5
6
|
import { createStorageCollisionError, createStorageUploadOutcomeUnknownError, defaultDeleteObject, defaultUploadBytes, defaultUploadObject, generateThumbnailBuffer, isObjectCollisionError, loadSharp, } from './remote.js';
|
|
6
7
|
import { normalizeText, } from './shared.js';
|
|
@@ -8,7 +9,8 @@ import { persistUploadedResource } from './resources.js';
|
|
|
8
9
|
import { buildStorageTarget, generateThumbnailKey } from './targets.js';
|
|
9
10
|
export async function inspectStorageUpload(db, input, deps) {
|
|
10
11
|
const statFile = deps?.statFile ?? fsp.stat;
|
|
11
|
-
const
|
|
12
|
+
const suppliedPath = input.filePath.trim();
|
|
13
|
+
const filePath = suppliedPath ? resolveUserPath(suppliedPath) : suppliedPath;
|
|
12
14
|
if (!filePath) {
|
|
13
15
|
throw new DinoxError('File path is required.');
|
|
14
16
|
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
|
5
5
|
import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
|
|
6
6
|
import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../utils/output.js';
|
|
8
|
-
import { getStatusSnapshot, waitForLastSyncedAfter } from '../powersync/runtime.js';
|
|
8
|
+
import { getStatusSnapshot, waitForLastSyncedAfter, waitForUploadQueueEmpty } from '../powersync/runtime.js';
|
|
9
9
|
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
10
10
|
type SyncCommandDependencies = {
|
|
11
11
|
loadConfig: typeof loadConfig;
|
|
@@ -15,6 +15,7 @@ type SyncCommandDependencies = {
|
|
|
15
15
|
getRequestedStructuredOutputFormat: typeof getRequestedStructuredOutputFormat;
|
|
16
16
|
getStatusSnapshot: typeof getStatusSnapshot;
|
|
17
17
|
waitForLastSyncedAfter: typeof waitForLastSyncedAfter;
|
|
18
|
+
waitForUploadQueueEmpty: typeof waitForUploadQueueEmpty;
|
|
18
19
|
runSingleSyncPass: typeof runSingleSyncPass;
|
|
19
20
|
printHumanReadableLine: typeof printHumanReadableLine;
|
|
20
21
|
printSuccess: typeof printSuccess;
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { getPowerSyncDbPath } from '../config/paths.js';
|
|
1
2
|
import { loadConfig } from '../config/store.js';
|
|
2
3
|
import { resolveConfig } from '../config/resolve.js';
|
|
3
4
|
import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
4
5
|
import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
|
|
6
|
+
import { requestContext } from '../utils/requestContext.js';
|
|
5
7
|
import { registerStandaloneCommand } from './descriptor/register.js';
|
|
8
|
+
import { waitForDaemonCurrent } from './runtime.js';
|
|
6
9
|
import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
|
|
7
10
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
8
11
|
import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
9
|
-
import { getStatusSnapshot, waitForLastSyncedAfter, } from '../powersync/runtime.js';
|
|
12
|
+
import { getStatusSnapshot, waitForLastSyncedAfter, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
|
|
10
13
|
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
11
14
|
import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
|
|
12
15
|
const MAX_SYNC_CLEANUP_RESERVE_MS = 5_000;
|
|
@@ -18,6 +21,7 @@ const syncCommandDependencies = {
|
|
|
18
21
|
getRequestedStructuredOutputFormat,
|
|
19
22
|
getStatusSnapshot,
|
|
20
23
|
waitForLastSyncedAfter,
|
|
24
|
+
waitForUploadQueueEmpty,
|
|
21
25
|
runSingleSyncPass,
|
|
22
26
|
printHumanReadableLine,
|
|
23
27
|
printSuccess,
|
|
@@ -27,18 +31,26 @@ export async function executeSyncCommand(context, overrides = {}) {
|
|
|
27
31
|
const dependencies = { ...syncCommandDependencies, ...overrides };
|
|
28
32
|
const startedAtMs = dependencies.now();
|
|
29
33
|
const startedAtIso = new Date(startedAtMs).toISOString();
|
|
30
|
-
const
|
|
34
|
+
const daemon = requestContext.getStore()?.daemon;
|
|
35
|
+
const baseConfig = daemon?.config ?? dependencies.resolveConfig(await dependencies.loadConfig());
|
|
31
36
|
const structuredOutput = dependencies.getRequestedStructuredOutputFormat() !== null;
|
|
32
37
|
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
33
38
|
const deadlineAtMs = startedAtMs + timeoutMs;
|
|
34
39
|
const cleanupReserveMs = Math.min(MAX_SYNC_CLEANUP_RESERVE_MS, Math.max(1, Math.floor(timeoutMs * 0.2)));
|
|
35
40
|
const operationDeadlineAtMs = deadlineAtMs - cleanupReserveMs;
|
|
36
41
|
const remainingTimeoutMs = (reserveMs = 0) => Math.max(1, deadlineAtMs - dependencies.now() - reserveMs);
|
|
37
|
-
const { config } = await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
|
|
42
|
+
const { config } = daemon ? { config: daemon.config } : await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
|
|
38
43
|
timeoutMs: remainingTimeoutMs(),
|
|
39
44
|
});
|
|
40
45
|
const strict = Boolean(context.options.strict);
|
|
41
|
-
const
|
|
46
|
+
const withRuntime = daemon
|
|
47
|
+
? async (_args, run) => {
|
|
48
|
+
const status = dependencies.getStatusSnapshot(daemon.db);
|
|
49
|
+
return run({ db: daemon.db, dbPath: getPowerSyncDbPath(config.auth.userId),
|
|
50
|
+
stale: !status.connected || !status.hasSynced, releaseOwner: async () => { } });
|
|
51
|
+
}
|
|
52
|
+
: dependencies.withExclusivePowerSync;
|
|
53
|
+
const result = await withRuntime({
|
|
42
54
|
config,
|
|
43
55
|
offline: false,
|
|
44
56
|
timeoutMs: remainingTimeoutMs(),
|
|
@@ -49,9 +61,16 @@ export async function executeSyncCommand(context, overrides = {}) {
|
|
|
49
61
|
cleanupFailureMode: 'throw',
|
|
50
62
|
}, async ({ db, dbPath, stale }) => {
|
|
51
63
|
const recentSync = strict
|
|
52
|
-
? await dependencies.waitForLastSyncedAfter(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
|
|
64
|
+
? await (daemon ? waitForDaemonCurrent : dependencies.waitForLastSyncedAfter)(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
|
|
53
65
|
: true;
|
|
54
66
|
const syncPass = await dependencies.runSingleSyncPass(db, remainingTimeoutMs(cleanupReserveMs));
|
|
67
|
+
const uploadQueue = daemon
|
|
68
|
+
? await dependencies.waitForUploadQueueEmpty(db, remainingTimeoutMs(cleanupReserveMs))
|
|
69
|
+
: null;
|
|
70
|
+
if (uploadQueue) {
|
|
71
|
+
syncPass.uploadIdle = uploadQueue.ok;
|
|
72
|
+
syncPass.idle = syncPass.downloadIdle && uploadQueue.ok;
|
|
73
|
+
}
|
|
55
74
|
const status = dependencies.getStatusSnapshot(db);
|
|
56
75
|
const gate = evaluateSyncGate({
|
|
57
76
|
offline: false,
|