@syncular/server 0.46.3 → 0.47.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/errors.js +9 -0
- package/dist/push.js +27 -4
- package/package.json +2 -2
- package/src/errors.ts +9 -0
- package/src/push.ts +28 -2
package/dist/errors.js
CHANGED
|
@@ -139,6 +139,15 @@ export const ERROR_CATALOG = {
|
|
|
139
139
|
recommendedAction: 'fixRequest',
|
|
140
140
|
httpStatus: 400,
|
|
141
141
|
},
|
|
142
|
+
// §6.8: the whole-commit validator threw an exception that is not a
|
|
143
|
+
// deliberate rejection. A host fault; the thrown message rides the §6.3.1
|
|
144
|
+
// details and the original reaches the host's `onError`.
|
|
145
|
+
'sync.validator_error': {
|
|
146
|
+
category: 'internal',
|
|
147
|
+
retryable: false,
|
|
148
|
+
recommendedAction: 'inspectServer',
|
|
149
|
+
httpStatus: 500,
|
|
150
|
+
},
|
|
142
151
|
'sync.missing_scopes': {
|
|
143
152
|
category: 'internal',
|
|
144
153
|
retryable: false,
|
package/dist/push.js
CHANGED
|
@@ -20,7 +20,7 @@ import { retainRollbackFailure } from './storage-errors.js';
|
|
|
20
20
|
*/
|
|
21
21
|
import { decodeRow, decodeSparseRow, encodeRow, parseBlobRef, } from '@syncular/core';
|
|
22
22
|
import { clockOf, limitsOf } from './context.js';
|
|
23
|
-
import { SyncError } from './errors.js';
|
|
23
|
+
import { reportError, SyncError } from './errors.js';
|
|
24
24
|
import { emitEvent } from './events.js';
|
|
25
25
|
import { prepareReactions, toNewReactions, } from './reactions.js';
|
|
26
26
|
import { authorizeWrite, renderScopeValue, storedScopesForRow } from './scopes.js';
|
|
@@ -699,7 +699,7 @@ function commitValidationReader(tx, schema) {
|
|
|
699
699
|
queryAuthoritative: (query) => transactionQuery(tx, query),
|
|
700
700
|
};
|
|
701
701
|
}
|
|
702
|
-
async function runCommitValidator(validator, tx, schema, clientId, clientCommitId, actorId, partition, operations) {
|
|
702
|
+
async function runCommitValidator(validator, onError, tx, schema, clientId, clientCommitId, actorId, partition, operations) {
|
|
703
703
|
if (validator === undefined)
|
|
704
704
|
return undefined;
|
|
705
705
|
try {
|
|
@@ -722,7 +722,30 @@ async function runCommitValidator(validator, tx, schema, clientId, clientCommitI
|
|
|
722
722
|
if (error instanceof ValidationRejection) {
|
|
723
723
|
return errorRecord(operations[0]?.opIndex ?? 0, error.code, error.message, false, error.details);
|
|
724
724
|
}
|
|
725
|
-
|
|
725
|
+
// §6.8: a throw that is not a deliberate rejection is a host fault, not a
|
|
726
|
+
// rule refusal. The host's observation hook receives the original (its
|
|
727
|
+
// stack included); the client gets a distinct code and, in the §6.3.1
|
|
728
|
+
// details, the bounded thrown message. Stacks and thrown values stay off
|
|
729
|
+
// the wire.
|
|
730
|
+
reportError(error, onError, 'sync');
|
|
731
|
+
let text = '';
|
|
732
|
+
let bytes = 0;
|
|
733
|
+
const encoder = new TextEncoder();
|
|
734
|
+
const thrown = error instanceof Error ? error.message : String(error);
|
|
735
|
+
for (const char of thrown.replace(/\p{Cc}+/gu, ' ').trim()) {
|
|
736
|
+
const size = encoder.encode(char).length;
|
|
737
|
+
if (bytes + size > 256)
|
|
738
|
+
break;
|
|
739
|
+
bytes += size;
|
|
740
|
+
text += char;
|
|
741
|
+
}
|
|
742
|
+
text = text.trimEnd();
|
|
743
|
+
return errorRecord(operations[0]?.opIndex ?? 0, 'sync.validator_error', 'whole-commit validator failed', false, {
|
|
744
|
+
reason: 'validator_threw',
|
|
745
|
+
...(text === ''
|
|
746
|
+
? {}
|
|
747
|
+
: { references: { validator: 'commit', message: text } }),
|
|
748
|
+
});
|
|
726
749
|
}
|
|
727
750
|
return undefined;
|
|
728
751
|
}
|
|
@@ -1129,7 +1152,7 @@ export async function processPushOperationsWithTrace(ctx, schema, resolved, clie
|
|
|
1129
1152
|
}
|
|
1130
1153
|
}
|
|
1131
1154
|
if (terminated === undefined) {
|
|
1132
|
-
const commitReject = await runCommitValidator(commitValidator, tx, schema, clientId, clientCommitId, ctx.actorId, partition, validatedOperations);
|
|
1155
|
+
const commitReject = await runCommitValidator(commitValidator, ctx.onError, tx, schema, clientId, clientCommitId, ctx.actorId, partition, validatedOperations);
|
|
1133
1156
|
if (commitReject?.kind === 'terminate') {
|
|
1134
1157
|
// An appended §6.11 cascade operation carries a synthetic opIndex;
|
|
1135
1158
|
// surface the originating client operation instead.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"verify:node": "cd ../.. && bun build ./packages/server/test/sqlite-runtime/verify-node.mjs --target=node --conditions=bun --outfile=./packages/server/.verify-node.built.mjs && node ./packages/server/.verify-node.built.mjs"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@syncular/core": "0.
|
|
71
|
+
"@syncular/core": "0.47.0"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@electric-sql/pglite": "^0.5.4"
|
package/src/errors.ts
CHANGED
|
@@ -147,6 +147,15 @@ export const ERROR_CATALOG: Readonly<Record<string, ErrorCatalogEntry>> = {
|
|
|
147
147
|
recommendedAction: 'fixRequest',
|
|
148
148
|
httpStatus: 400,
|
|
149
149
|
},
|
|
150
|
+
// §6.8: the whole-commit validator threw an exception that is not a
|
|
151
|
+
// deliberate rejection. A host fault; the thrown message rides the §6.3.1
|
|
152
|
+
// details and the original reaches the host's `onError`.
|
|
153
|
+
'sync.validator_error': {
|
|
154
|
+
category: 'internal',
|
|
155
|
+
retryable: false,
|
|
156
|
+
recommendedAction: 'inspectServer',
|
|
157
|
+
httpStatus: 500,
|
|
158
|
+
},
|
|
150
159
|
'sync.missing_scopes': {
|
|
151
160
|
category: 'internal',
|
|
152
161
|
retryable: false,
|
package/src/push.ts
CHANGED
|
@@ -35,7 +35,7 @@ import type { BlobStore } from './blob-store';
|
|
|
35
35
|
import type { SyncRequestContext } from './context';
|
|
36
36
|
import { clockOf, limitsOf } from './context';
|
|
37
37
|
import type { CrdtMergerRegistry } from './crdt-merger';
|
|
38
|
-
import { SyncError } from './errors';
|
|
38
|
+
import { reportError, SyncError, type SyncularErrorHandler } from './errors';
|
|
39
39
|
import { emitEvent } from './events';
|
|
40
40
|
import {
|
|
41
41
|
prepareReactions,
|
|
@@ -1116,6 +1116,7 @@ function commitValidationReader(
|
|
|
1116
1116
|
|
|
1117
1117
|
async function runCommitValidator(
|
|
1118
1118
|
validator: CommitValidator | undefined,
|
|
1119
|
+
onError: SyncularErrorHandler | undefined,
|
|
1119
1120
|
tx: StorageTransaction,
|
|
1120
1121
|
schema: CompiledSchema,
|
|
1121
1122
|
clientId: string,
|
|
@@ -1160,10 +1161,34 @@ async function runCommitValidator(
|
|
|
1160
1161
|
error.details,
|
|
1161
1162
|
);
|
|
1162
1163
|
}
|
|
1164
|
+
// §6.8: a throw that is not a deliberate rejection is a host fault, not a
|
|
1165
|
+
// rule refusal. The host's observation hook receives the original (its
|
|
1166
|
+
// stack included); the client gets a distinct code and, in the §6.3.1
|
|
1167
|
+
// details, the bounded thrown message. Stacks and thrown values stay off
|
|
1168
|
+
// the wire.
|
|
1169
|
+
reportError(error, onError, 'sync');
|
|
1170
|
+
let text = '';
|
|
1171
|
+
let bytes = 0;
|
|
1172
|
+
const encoder = new TextEncoder();
|
|
1173
|
+
const thrown = error instanceof Error ? error.message : String(error);
|
|
1174
|
+
for (const char of thrown.replace(/\p{Cc}+/gu, ' ').trim()) {
|
|
1175
|
+
const size = encoder.encode(char).length;
|
|
1176
|
+
if (bytes + size > 256) break;
|
|
1177
|
+
bytes += size;
|
|
1178
|
+
text += char;
|
|
1179
|
+
}
|
|
1180
|
+
text = text.trimEnd();
|
|
1163
1181
|
return errorRecord(
|
|
1164
1182
|
operations[0]?.opIndex ?? 0,
|
|
1165
|
-
'sync.
|
|
1183
|
+
'sync.validator_error',
|
|
1166
1184
|
'whole-commit validator failed',
|
|
1185
|
+
false,
|
|
1186
|
+
{
|
|
1187
|
+
reason: 'validator_threw',
|
|
1188
|
+
...(text === ''
|
|
1189
|
+
? {}
|
|
1190
|
+
: { references: { validator: 'commit', message: text } }),
|
|
1191
|
+
},
|
|
1167
1192
|
);
|
|
1168
1193
|
}
|
|
1169
1194
|
return undefined;
|
|
@@ -1769,6 +1794,7 @@ export async function processPushOperationsWithTrace(
|
|
|
1769
1794
|
if (terminated === undefined) {
|
|
1770
1795
|
const commitReject = await runCommitValidator(
|
|
1771
1796
|
commitValidator,
|
|
1797
|
+
ctx.onError,
|
|
1772
1798
|
tx,
|
|
1773
1799
|
schema,
|
|
1774
1800
|
clientId,
|