@dinoxx/dinox-cli 1.1.1 → 2.0.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.
Files changed (104) hide show
  1. package/README.md +44 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo.d.ts +9 -0
  19. package/dist/commands/notes/repo.js +65 -15
  20. package/dist/commands/notes/searchOutput.d.ts +2 -1
  21. package/dist/commands/notes/searchOutput.js +2 -1
  22. package/dist/commands/notes/searchSql.js +9 -3
  23. package/dist/commands/prompt/index.d.ts +1 -0
  24. package/dist/commands/prompt/index.js +26 -11
  25. package/dist/commands/savedViews/index.d.ts +7 -0
  26. package/dist/commands/savedViews/index.js +458 -0
  27. package/dist/commands/savedViews/model.d.ts +87 -0
  28. package/dist/commands/savedViews/model.js +306 -0
  29. package/dist/commands/savedViews/repo.d.ts +123 -0
  30. package/dist/commands/savedViews/repo.js +512 -0
  31. package/dist/commands/savedViews/semantics.d.ts +46 -0
  32. package/dist/commands/savedViews/semantics.js +738 -0
  33. package/dist/commands/schema/renderSkills.d.ts +2 -0
  34. package/dist/commands/schema/renderSkills.js +69 -10
  35. package/dist/commands/schema/spec.js +4 -0
  36. package/dist/commands/skills/index.d.ts +54 -0
  37. package/dist/commands/skills/index.js +468 -0
  38. package/dist/commands/storage/index.js +2 -2
  39. package/dist/commands/sync.js +2 -1
  40. package/dist/commands/tags/index.js +30 -11
  41. package/dist/commands/tags/operations.d.ts +3 -3
  42. package/dist/commands/tags/operations.js +45 -20
  43. package/dist/commands/templates/model.d.ts +50 -0
  44. package/dist/commands/templates/model.js +239 -0
  45. package/dist/commands/todo/index.js +3 -3
  46. package/dist/commands/update.d.ts +1 -0
  47. package/dist/commands/update.js +46 -9
  48. package/dist/commands/writeConfirmation.d.ts +13 -0
  49. package/dist/commands/writeConfirmation.js +54 -0
  50. package/dist/config/resolve.js +5 -10
  51. package/dist/config/types.d.ts +3 -6
  52. package/dist/constants/links.d.ts +18 -1
  53. package/dist/constants/links.js +26 -1
  54. package/dist/daemon/client.d.ts +2 -2
  55. package/dist/daemon/client.js +106 -52
  56. package/dist/daemon/index.d.ts +5 -3
  57. package/dist/daemon/index.js +28 -5
  58. package/dist/daemon/lifecycle.d.ts +7 -2
  59. package/dist/daemon/lifecycle.js +53 -13
  60. package/dist/daemon/pid.d.ts +1 -1
  61. package/dist/daemon/pid.js +6 -3
  62. package/dist/dinox.js +9 -1
  63. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  64. package/dist/powersync/exclusiveRuntime.js +26 -8
  65. package/dist/powersync/runtime.d.ts +3 -2
  66. package/dist/powersync/runtime.js +19 -3
  67. package/dist/powersync/schema/content.d.ts +1 -78
  68. package/dist/powersync/schema/content.js +1 -78
  69. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  70. package/dist/powersync/schema/electronCanonical.js +1154 -0
  71. package/dist/powersync/schema/index.d.ts +573 -129
  72. package/dist/powersync/schema/index.js +3 -32
  73. package/dist/powersync/schema/local.d.ts +1 -56
  74. package/dist/powersync/schema/local.js +1 -60
  75. package/dist/powersync/schema/note.d.ts +1 -35
  76. package/dist/powersync/schema/note.js +1 -35
  77. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  78. package/dist/powersync/schema/notesExtras.js +1 -71
  79. package/dist/powersync/schema/projects.d.ts +1 -104
  80. package/dist/powersync/schema/projects.js +1 -104
  81. package/dist/powersync/schema/savedViews.d.ts +1 -0
  82. package/dist/powersync/schema/savedViews.js +1 -0
  83. package/dist/powersync/schema/storage.d.ts +1 -24
  84. package/dist/powersync/schema/storage.js +1 -24
  85. package/dist/powersync/schema/tags.d.ts +1 -37
  86. package/dist/powersync/schema/tags.js +1 -37
  87. package/dist/powersync/syncGate.d.ts +20 -2
  88. package/dist/powersync/uploader.d.ts +16 -2
  89. package/dist/powersync/uploader.js +152 -158
  90. package/dist/skills/check.d.ts +1 -0
  91. package/dist/skills/check.js +538 -70
  92. package/dist/skills/retired.d.ts +60 -0
  93. package/dist/skills/retired.js +58 -0
  94. package/dist/utils/deadline.d.ts +8 -0
  95. package/dist/utils/deadline.js +31 -0
  96. package/dist/utils/dryRun.d.ts +20 -1
  97. package/dist/utils/dryRun.js +22 -3
  98. package/dist/utils/errors.d.ts +2 -0
  99. package/dist/utils/errors.js +2 -0
  100. package/dist/utils/processExit.d.ts +1 -0
  101. package/dist/utils/processExit.js +33 -0
  102. package/dist/utils/updateNotice.d.ts +1 -0
  103. package/dist/utils/updateNotice.js +1 -1
  104. package/package.json +8 -5
@@ -1,24 +1 @@
1
- import { column, Table } from '@powersync/common';
2
- export const c_storage = new Table({
3
- id: column.text,
4
- user_id: column.text,
5
- alias_name: column.text,
6
- protocol: column.text,
7
- provider: column.text,
8
- is_active: column.integer,
9
- sort_rank: column.integer,
10
- endpoint: column.text,
11
- bucket: column.text,
12
- region: column.text,
13
- access_key_id: column.text,
14
- secret_access_key: column.text,
15
- force_path_style: column.integer,
16
- path_prefix: column.text,
17
- custom_domain: column.text,
18
- legacy_domains: column.text,
19
- is_private: column.integer,
20
- extra_config: column.text,
21
- created_at: column.text,
22
- updated_at: column.text,
23
- deleted_at: column.text,
24
- });
1
+ export { c_storage } from './electronCanonical.js';
@@ -1,37 +1 @@
1
- import { Table } from '@powersync/common';
2
- export declare const c_tag: Table<{
3
- id: import("@powersync/common").BaseColumnType<string | null>;
4
- tags: import("@powersync/common").BaseColumnType<string | null>;
5
- insert_at: import("@powersync/common").BaseColumnType<string | null>;
6
- extra_data: import("@powersync/common").BaseColumnType<string | null>;
7
- update_at: import("@powersync/common").BaseColumnType<string | null>;
8
- user_id: import("@powersync/common").BaseColumnType<string | null>;
9
- is_del: import("@powersync/common").BaseColumnType<number | null>;
10
- favorite_tags: import("@powersync/common").BaseColumnType<string | null>;
11
- }>;
12
- export declare const c_tag_node: Table<{
13
- id: import("@powersync/common").BaseColumnType<string | null>;
14
- user_id: import("@powersync/common").BaseColumnType<string | null>;
15
- emoji: import("@powersync/common").BaseColumnType<string | null>;
16
- full_path: import("@powersync/common").BaseColumnType<string | null>;
17
- path_key: import("@powersync/common").BaseColumnType<string | null>;
18
- parent_id: import("@powersync/common").BaseColumnType<string | null>;
19
- depth: import("@powersync/common").BaseColumnType<number | null>;
20
- is_actual: import("@powersync/common").BaseColumnType<number | null>;
21
- is_del: import("@powersync/common").BaseColumnType<number | null>;
22
- created_at: import("@powersync/common").BaseColumnType<string | null>;
23
- updated_at: import("@powersync/common").BaseColumnType<string | null>;
24
- meta: import("@powersync/common").BaseColumnType<string | null>;
25
- }>;
26
- export declare const c_tag_preference: Table<{
27
- id: import("@powersync/common").BaseColumnType<string | null>;
28
- user_id: import("@powersync/common").BaseColumnType<string | null>;
29
- tag_id: import("@powersync/common").BaseColumnType<string | null>;
30
- is_favorite: import("@powersync/common").BaseColumnType<number | null>;
31
- is_expanded: import("@powersync/common").BaseColumnType<number | null>;
32
- pinned_at: import("@powersync/common").BaseColumnType<string | null>;
33
- sort_rank: import("@powersync/common").BaseColumnType<number | null>;
34
- pinned_rank: import("@powersync/common").BaseColumnType<number | null>;
35
- created_at: import("@powersync/common").BaseColumnType<string | null>;
36
- updated_at: import("@powersync/common").BaseColumnType<string | null>;
37
- }>;
1
+ export { c_tag_node, c_tag_preference } from './electronCanonical.js';
@@ -1,37 +1 @@
1
- import { column, Table } from '@powersync/common';
2
- export const c_tag = new Table({
3
- id: column.text,
4
- tags: column.text,
5
- insert_at: column.text,
6
- extra_data: column.text,
7
- update_at: column.text,
8
- user_id: column.text,
9
- is_del: column.integer,
10
- favorite_tags: column.text,
11
- });
12
- export const c_tag_node = new Table({
13
- id: column.text,
14
- user_id: column.text,
15
- emoji: column.text,
16
- full_path: column.text,
17
- path_key: column.text,
18
- parent_id: column.text,
19
- depth: column.integer,
20
- is_actual: column.integer,
21
- is_del: column.integer,
22
- created_at: column.text,
23
- updated_at: column.text,
24
- meta: column.text,
25
- });
26
- export const c_tag_preference = new Table({
27
- id: column.text,
28
- user_id: column.text,
29
- tag_id: column.text,
30
- is_favorite: column.integer,
31
- is_expanded: column.integer,
32
- pinned_at: column.text,
33
- sort_rank: column.integer,
34
- pinned_rank: column.integer,
35
- created_at: column.text,
36
- updated_at: column.text,
37
- });
1
+ export { c_tag_node, c_tag_preference } from './electronCanonical.js';
@@ -1,4 +1,22 @@
1
- import type { SyncStatusOptions } from '@powersync/common';
1
+ import type { SyncStatus } from '@powersync/common';
2
+ /**
3
+ * Plain copy of the PowerSync sync status, reported as `status.raw` in JSON output.
4
+ * Keeps the 1.x `SyncStatusOptions` shape (PowerSync 2 made SyncStatus an interface
5
+ * and changed its toJSON() to an internal format).
6
+ */
7
+ export type SyncStatusRaw = {
8
+ connected?: boolean;
9
+ connecting?: boolean;
10
+ hasSynced?: boolean;
11
+ lastSyncedAt?: Date;
12
+ dataFlow?: {
13
+ downloading?: boolean;
14
+ uploading?: boolean;
15
+ downloadError?: Error;
16
+ uploadError?: Error;
17
+ };
18
+ priorityStatusEntries?: SyncStatus['priorityStatusEntries'];
19
+ };
2
20
  import { DinoxError } from '../utils/errors.js';
3
21
  export type SyncGateFailureReason = 'offline' | 'first-sync-timeout' | 'checkpoint-timeout' | 'data-flow-timeout' | 'index-timeout' | 'not-connected' | 'not-synced' | 'download-error';
4
22
  export type SyncGateResult = {
@@ -20,7 +38,7 @@ export type SyncGateInput = {
20
38
  message: string;
21
39
  };
22
40
  };
23
- raw?: SyncStatusOptions;
41
+ raw?: SyncStatusRaw;
24
42
  };
25
43
  };
26
44
  export declare function evaluateSyncGate(input: SyncGateInput): SyncGateResult;
@@ -1,8 +1,22 @@
1
+ import { type CrudTransaction } from '@powersync/common';
1
2
  import type { UploadHandler } from './connector.js';
3
+ export type UploadOperation = {
4
+ op_id: number;
5
+ op: 'PUT' | 'PATCH' | 'DELETE';
6
+ type: string;
7
+ id: string;
8
+ tx_id?: number;
9
+ data?: Record<string, unknown>;
10
+ old?: Record<string, unknown>;
11
+ old_sha256?: Record<string, string>;
12
+ metadata?: string;
13
+ };
14
+ type CrudOperation = CrudTransaction['crud'][number];
15
+ export declare function toUploadOperation(op: CrudOperation): UploadOperation;
2
16
  export declare function createPowerSyncUploadHandler(options: {
3
17
  authorization: string;
4
18
  uploadBaseUrl: string;
5
- uploadV4Path: string;
6
- uploadV2Path: string;
19
+ uploadPath: string;
7
20
  timeoutMs?: number;
8
21
  }): UploadHandler;
22
+ export {};
@@ -1,135 +1,164 @@
1
+ import crypto from 'node:crypto';
1
2
  import { UpdateType } from '@powersync/common';
2
3
  import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
3
4
  import { toAuthorizationHeaderValue } from '../utils/authToken.js';
4
5
  import { sanitizeForTerminal } from '../utils/terminalSanitization.js';
5
- const FATAL_RESPONSE_CODES = [/^22...$/, /^23...$/, /^42501$/];
6
- const legacyUploadTargets = new Set();
7
- function shouldFallbackToLegacy(status) {
8
- return status === 404 || status === 405;
6
+ /**
7
+ * Uploads each PowerSync CRUD transaction as ONE request to the backend v6 batch
8
+ * endpoint (same contract as dinox-electron / dinox-web / dinox-app). The backend
9
+ * applies the batch atomically in arrival order and answers `applied` or
10
+ * `rejected`, so a transaction either lands whole or not at all.
11
+ */
12
+ const NON_RETRYABLE_UPLOAD_HTTP_STATUSES = new Set([400, 409, 422]);
13
+ // Pre-edit values longer than this travel as a SHA-256 so long note bodies are not
14
+ // re-uploaded as conflict-detection bases; the backend hashes its stored text.
15
+ const OLD_VALUE_HASH_THRESHOLD = 256;
16
+ const UPLOAD_OMITTED_COLUMNS_BY_TABLE = new Map([
17
+ // The backend v6 tag-node mapper does not consume this local metadata column.
18
+ ['c_tag_node', new Set(['meta'])],
19
+ // Server-generated scheduled-task columns must not be echoed back.
20
+ ['c_scheduled_task', new Set(['last_run_at', 'last_run_id'])],
21
+ ]);
22
+ const UPLOAD_REQUIRED_INSERT_DEFAULTS_BY_TABLE = new Map([
23
+ // PowerSync omits NULL values from PUT payloads, while the backend requires these.
24
+ ['c_note_template', { content: '', extra_data: '{}' }],
25
+ ]);
26
+ function isRecord(value) {
27
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
9
28
  }
10
29
  function normalizeBaseUrl(baseUrl) {
11
30
  return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
12
31
  }
13
- function legacyUploadTargetKey(baseUrl, uploadV4Path) {
14
- return `${normalizeBaseUrl(baseUrl)}${uploadV4Path}`;
15
- }
16
- async function readErrorBody(response) {
17
- const contentType = response.headers.get('content-type') ?? '';
18
- const text = await response.text();
19
- if (contentType.includes('application/json')) {
20
- try {
21
- const json = JSON.parse(text);
22
- const code = json?.error?.code ?? json?.code;
23
- const message = json?.error?.message ?? json?.message;
24
- return {
25
- code: typeof code === 'string' ? code : undefined,
26
- message: typeof message === 'string' ? message : undefined,
27
- text,
28
- };
29
- }
30
- catch {
31
- return { text };
32
- }
33
- }
34
- return { text };
35
- }
36
- function createUploadError(args) {
37
- const details = args.message ?? args.text;
38
- const message = details
39
- ? `PowerSync upload failed (${args.method} ${args.url}) [${args.status}]: ${details}`
40
- : `PowerSync upload failed (${args.method} ${args.url}) [${args.status}]`;
32
+ function createUploadError(message, extra = {}) {
41
33
  const error = new Error(message);
42
34
  error.name = 'PowerSyncUploadError';
43
- error.code = args.code;
44
- error.status = args.status;
35
+ error.code = extra.code;
36
+ error.status = extra.status;
37
+ error.fatal = extra.fatal;
45
38
  return error;
46
39
  }
47
- function isFatalUploadError(error) {
48
- if (!error || typeof error !== 'object') {
49
- return false;
50
- }
51
- const uploadError = error;
52
- if (uploadError.fatal === true) {
53
- return true;
40
+ function toUploadOperationName(op) {
41
+ switch (op.op) {
42
+ case UpdateType.PUT:
43
+ return 'PUT';
44
+ case UpdateType.PATCH:
45
+ return 'PATCH';
46
+ case UpdateType.DELETE:
47
+ return 'DELETE';
48
+ default:
49
+ throw createUploadError(`Unsupported PowerSync CRUD operation: ${String(op.op)} (${String(op.table)}:${String(op.id ?? '')})`, { code: 'UNSUPPORTED_CRUD_OPERATION', fatal: true });
54
50
  }
55
- return typeof uploadError.code === 'string'
56
- && FATAL_RESPONSE_CODES.some(regex => regex.test(uploadError.code));
57
51
  }
58
- function createUnsupportedCrudOperationError(op) {
59
- const error = new Error(`Unsupported PowerSync CRUD operation: ${String(op.op)} (${String(op.table)}:${String(op.id ?? '')})`);
60
- error.name = 'PowerSyncUploadError';
61
- error.code = 'UNSUPPORTED_CRUD_OPERATION';
62
- error.fatal = true;
63
- return error;
52
+ function omitColumns(table, values) {
53
+ const omitted = UPLOAD_OMITTED_COLUMNS_BY_TABLE.get(table);
54
+ if (!omitted)
55
+ return values;
56
+ const next = { ...values };
57
+ for (const column of omitted)
58
+ delete next[column];
59
+ return next;
64
60
  }
65
- function summarizeCrudOperation(op) {
66
- const opName = UpdateType[op.op] ?? String(op.op);
61
+ /** `old` is the conflict-detection base (trackPrevious); long values move to `old_sha256`. */
62
+ function compactPreviousValues(previous) {
63
+ if (!previous)
64
+ return {};
65
+ const old = {};
66
+ const oldSha256 = {};
67
+ for (const [column, value] of Object.entries(previous)) {
68
+ if (typeof value === 'string' && value.length > OLD_VALUE_HASH_THRESHOLD) {
69
+ oldSha256[column] = crypto.createHash('sha256').update(value, 'utf8').digest('hex');
70
+ }
71
+ else {
72
+ old[column] = value;
73
+ }
74
+ }
67
75
  return {
68
- id: String(op.id ?? ''),
69
- op: opName,
70
- table: String(op.table ?? ''),
76
+ ...(Object.keys(old).length > 0 ? { old } : {}),
77
+ ...(Object.keys(oldSha256).length > 0 ? { old_sha256: oldSha256 } : {}),
71
78
  };
72
79
  }
73
- function attachFatalContext(error, transaction, failedIndex) {
74
- if (!isFatalUploadError(error)) {
75
- return;
80
+ export function toUploadOperation(op) {
81
+ const name = toUploadOperationName(op);
82
+ const table = String(op.table);
83
+ let data = isRecord(op.opData) ? { ...op.opData } : undefined;
84
+ if (name === 'PUT') {
85
+ data ??= {};
86
+ for (const [column, value] of Object.entries(UPLOAD_REQUIRED_INSERT_DEFAULTS_BY_TABLE.get(table) ?? {})) {
87
+ if (data[column] == null)
88
+ data[column] = value;
89
+ }
76
90
  }
77
- error.fatalContext = {
78
- failedIndex,
79
- operations: transaction.crud.map((op) => summarizeCrudOperation(op)),
91
+ return {
92
+ op_id: op.clientId,
93
+ op: name,
94
+ type: table,
95
+ id: String(op.id),
96
+ ...(op.transactionId !== undefined ? { tx_id: op.transactionId } : {}),
97
+ ...(data ? { data: omitColumns(table, data) } : {}),
98
+ // DELETE needs no base: deletes win (PowerSync default), nothing to compare.
99
+ ...(name === 'PATCH' ? compactPreviousValues(op.previousValues) : {}),
100
+ ...(op.metadata !== undefined ? { metadata: op.metadata } : {}),
80
101
  };
81
102
  }
82
- function logFatalUploadFailure(error) {
83
- const context = error.fatalContext;
84
- if (!context) {
85
- process.stderr.write(`${sanitizeForTerminal(`warning: fatal PowerSync upload error (${error.code ?? error.status ?? 'unknown'}); queued operations were retained.`)}\n`);
86
- return;
103
+ function isEmptyPatch(operation) {
104
+ return operation.op === 'PATCH' && (!operation.data || Object.keys(operation.data).length === 0);
105
+ }
106
+ function isUploadResponse(value) {
107
+ if (!isRecord(value))
108
+ return false;
109
+ if (value.status !== 'applied' && value.status !== 'rejected')
110
+ return false;
111
+ return typeof value.operationCount === 'number' && Number.isFinite(value.operationCount);
112
+ }
113
+ async function readResponseBody(response) {
114
+ const text = await response.text();
115
+ try {
116
+ return { json: JSON.parse(text), text };
117
+ }
118
+ catch {
119
+ return { text };
87
120
  }
88
- const failedOperation = context.operations[context.failedIndex];
89
- const failedOperationText = failedOperation
90
- ? `${failedOperation.op}:${failedOperation.table}:${failedOperation.id || '(no-id)'}`
91
- : `index ${context.failedIndex}`;
92
- const succeededBeforeFailure = Math.min(context.failedIndex, context.operations.length);
93
- const replayWarning = succeededBeforeFailure > 0
94
- ? ` ${succeededBeforeFailure} earlier operation(s) already returned success and may be sent again on retry; the legacy upload endpoints do not provide a transaction-level acknowledgement.`
95
- : '';
96
- process.stderr.write(`${sanitizeForTerminal(`warning: fatal PowerSync upload error (${error.code ?? error.status ?? 'unknown'}) at ${failedOperationText}; PowerSync retained the entire ${context.operations.length}-operation transaction.${replayWarning}`)}\n`);
97
121
  }
98
- async function dispatchMutation(options) {
99
- const timeout = typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
100
- ? createTimeoutSignal(options.timeoutMs)
122
+ function errorDetails(json) {
123
+ if (!isRecord(json))
124
+ return {};
125
+ const nested = isRecord(json.error) ? json.error : json;
126
+ return {
127
+ code: typeof nested.code === 'string' ? nested.code : undefined,
128
+ message: typeof nested.message === 'string' ? nested.message : undefined,
129
+ };
130
+ }
131
+ async function postBatch(args) {
132
+ const timeout = typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0
133
+ ? createTimeoutSignal(args.timeoutMs)
101
134
  : null;
102
- const requestSignal = mergeAbortSignals([options.signal, timeout?.signal]);
103
135
  try {
104
- const response = await fetch(options.url, {
105
- method: options.method,
136
+ const response = await fetch(args.url, {
137
+ method: 'POST',
106
138
  headers: {
107
- Authorization: toAuthorizationHeaderValue(options.authorization),
139
+ Authorization: toAuthorizationHeaderValue(args.authorization),
108
140
  'Content-Type': 'application/json',
109
141
  },
110
- body: JSON.stringify(options.payload),
111
- signal: requestSignal,
142
+ body: JSON.stringify({ batch: args.batch }),
143
+ signal: mergeAbortSignals([timeout?.signal]),
112
144
  });
113
- if (response.ok) {
114
- await response.arrayBuffer();
115
- return;
145
+ const body = await readResponseBody(response);
146
+ if (!response.ok) {
147
+ const details = errorDetails(body.json);
148
+ throw createUploadError(`PowerSync upload failed (POST ${args.url}) [${response.status}]: ${details.message ?? body.text}`, {
149
+ code: details.code ?? `HTTP_${response.status}`,
150
+ status: response.status,
151
+ fatal: NON_RETRYABLE_UPLOAD_HTTP_STATUSES.has(response.status),
152
+ });
116
153
  }
117
- const body = await readErrorBody(response);
118
- throw createUploadError({
119
- status: response.status,
120
- method: options.method,
121
- url: options.url,
122
- code: body.code,
123
- message: body.message,
124
- text: body.text,
125
- });
154
+ if (!isUploadResponse(body.json)) {
155
+ throw createUploadError(`Invalid PowerSync v6 response from ${args.url}`);
156
+ }
157
+ return body.json;
126
158
  }
127
159
  catch (error) {
128
- if (timeout?.signal.aborted && !options.signal?.aborted) {
129
- const err = new Error(`PowerSync upload timed out (${options.method} ${options.url}) after ${options.timeoutMs}ms`);
130
- err.name = 'PowerSyncUploadError';
131
- err.status = 408;
132
- throw err;
160
+ if (timeout?.signal.aborted) {
161
+ throw createUploadError(`PowerSync upload timed out (POST ${args.url}) after ${args.timeoutMs}ms`, { status: 408 });
133
162
  }
134
163
  throw error;
135
164
  }
@@ -137,76 +166,41 @@ async function dispatchMutation(options) {
137
166
  timeout?.cancel();
138
167
  }
139
168
  }
140
- async function uploadTransaction(args) {
141
- const baseUrl = normalizeBaseUrl(args.uploadBaseUrl);
142
- const primaryUrl = `${baseUrl}${args.uploadV4Path}`;
143
- const fallbackUrl = `${baseUrl}${args.uploadV2Path}`;
144
- const primaryKey = legacyUploadTargetKey(baseUrl, args.uploadV4Path);
145
- const dispatchPutOrPatch = async (method, payload) => {
146
- if (legacyUploadTargets.has(primaryKey)) {
147
- await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method, payload, timeoutMs: args.timeoutMs });
148
- return;
149
- }
150
- try {
151
- await dispatchMutation({ authorization: args.authorization, url: primaryUrl, method, payload, timeoutMs: args.timeoutMs });
152
- }
153
- catch (error) {
154
- if (shouldFallbackToLegacy(error?.status)) {
155
- legacyUploadTargets.add(primaryKey);
156
- await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method, payload, timeoutMs: args.timeoutMs });
157
- return;
158
- }
159
- throw error;
160
- }
161
- };
162
- for (let index = 0; index < args.transaction.crud.length; index += 1) {
163
- const op = args.transaction.crud[index];
164
- const record = { table: op.table, data: { ...(op.opData ?? {}), id: op.id } };
165
- try {
166
- switch (op.op) {
167
- case UpdateType.PUT:
168
- await dispatchPutOrPatch('PUT', record);
169
- break;
170
- case UpdateType.PATCH:
171
- await dispatchPutOrPatch('PATCH', record);
172
- break;
173
- case UpdateType.DELETE:
174
- await dispatchMutation({ authorization: args.authorization, url: fallbackUrl, method: 'DELETE', payload: record, timeoutMs: args.timeoutMs });
175
- break;
176
- default:
177
- throw createUnsupportedCrudOperationError(op);
178
- }
179
- }
180
- catch (error) {
181
- attachFatalContext(error, args.transaction, index);
182
- throw error;
183
- }
184
- }
169
+ function describeOperation(operation) {
170
+ return operation ? `${operation.op}:${operation.type}:${operation.id}` : '(none)';
185
171
  }
186
172
  export function createPowerSyncUploadHandler(options) {
173
+ const url = `${normalizeBaseUrl(options.uploadBaseUrl)}${options.uploadPath}`;
187
174
  return async (database) => {
188
175
  const transaction = await database.getNextCrudTransaction();
189
176
  if (!transaction) {
190
177
  return;
191
178
  }
192
- try {
193
- await uploadTransaction({
194
- transaction,
195
- authorization: options.authorization,
196
- uploadBaseUrl: options.uploadBaseUrl,
197
- uploadV4Path: options.uploadV4Path,
198
- uploadV2Path: options.uploadV2Path,
199
- timeoutMs: options.timeoutMs,
200
- });
179
+ // Redundant local updates can collapse into empty PATCHes; they carry no state.
180
+ const batch = transaction.crud.map(toUploadOperation).filter(op => !isEmptyPatch(op));
181
+ if (batch.length === 0) {
201
182
  await transaction.complete();
183
+ return;
184
+ }
185
+ let result;
186
+ try {
187
+ result = await postBatch({ url, authorization: options.authorization, batch, timeoutMs: options.timeoutMs });
202
188
  }
203
189
  catch (error) {
204
- if (isFatalUploadError(error)) {
205
- // v4/v2 acknowledge individual operations, so completing here would discard
206
- // the failed suffix; retaining the transaction can replay an accepted prefix.
207
- logFatalUploadFailure(error);
190
+ if (error.fatal) {
191
+ process.stderr.write(`${sanitizeForTerminal(`warning: PowerSync refused the ${batch.length}-operation upload (${error.code}); ` +
192
+ `the local changes were kept in the upload queue. First operation: ${describeOperation(batch[0])}`)}\n`);
208
193
  }
209
194
  throw error;
210
195
  }
196
+ if (result.status === 'rejected') {
197
+ // The backend applied nothing. Keep the transaction so the command reports the
198
+ // failure (`--durability uploaded` must not succeed) instead of dropping edits.
199
+ const error = createUploadError(`PowerSync rejected the upload (${result.code ?? 'REJECTED'}): ${result.message ?? 'no message'}`, { code: result.code ?? 'REJECTED', fatal: true });
200
+ process.stderr.write(`${sanitizeForTerminal(`warning: ${error.message}; ${batch.length} operation(s) were kept in the upload queue. ` +
201
+ `First operation: ${describeOperation(batch[0])}`)}\n`);
202
+ throw error;
203
+ }
204
+ await transaction.complete();
211
205
  };
212
206
  }
@@ -4,3 +4,4 @@ export type SkillCheckIssue = {
4
4
  };
5
5
  export declare function checkSkillContent(skill: string, content: string, skillDir?: string, expectedVersion?: string): SkillCheckIssue[];
6
6
  export declare function checkSkillsDirectory(skillsDir: string): SkillCheckIssue[];
7
+ export declare function checkSkillDirectory(skillDir: string, expectedVersion?: string): SkillCheckIssue[];