@sanity/sdk 3.6.0 → 3.7.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.
@@ -0,0 +1,63 @@
1
+ import {ClientError, CorsOriginError, ServerError} from '@sanity/client'
2
+ import {describe, expect, it} from 'vitest'
3
+
4
+ import {classifySubmissionError} from './submissionErrors'
5
+
6
+ const clientError = (statusCode: number, body: unknown = {}) =>
7
+ new ClientError({statusCode, headers: {}, body})
8
+
9
+ describe('classifySubmissionError', () => {
10
+ it('retries an expired or missing session', () => {
11
+ expect(
12
+ classifySubmissionError(
13
+ clientError(401, {statusCode: 401, error: 'Unauthorized', message: 'Session not found'}),
14
+ ),
15
+ ).toBe('retry')
16
+ })
17
+
18
+ it('retries server errors, timeouts, rate limits and network failures', () => {
19
+ expect(classifySubmissionError(new ServerError({statusCode: 503, headers: {}, body: {}}))).toBe(
20
+ 'retry',
21
+ )
22
+ expect(classifySubmissionError(clientError(408))).toBe('retry')
23
+ expect(classifySubmissionError(clientError(429))).toBe('retry')
24
+ expect(classifySubmissionError(new Error('Request error while attempting to reach'))).toBe(
25
+ 'retry',
26
+ )
27
+ expect(classifySubmissionError('not an error')).toBe('retry')
28
+ })
29
+
30
+ it('retries a transaction that hit contention while committing', () => {
31
+ expect(
32
+ classifySubmissionError(
33
+ clientError(409, {
34
+ error: {type: 'transactionConflictError', description: 'transaction conflict'},
35
+ }),
36
+ ),
37
+ ).toBe('retry')
38
+ })
39
+
40
+ it('treats an already recorded transaction ID as an ack', () => {
41
+ expect(
42
+ classifySubmissionError(
43
+ clientError(409, {
44
+ error: {type: 'transactionAlreadyExistsError', description: 'already exists'},
45
+ }),
46
+ ),
47
+ ).toBe('accepted')
48
+ })
49
+
50
+ it('reverts transactions the server rejected', () => {
51
+ for (const statusCode of [400, 402, 403, 404, 410, 412, 413, 422]) {
52
+ expect(classifySubmissionError(clientError(statusCode))).toBe('revert')
53
+ }
54
+ expect(
55
+ classifySubmissionError(
56
+ clientError(409, {
57
+ error: {type: 'documentRevisionIDDoesNotMatchError', description: 'revision mismatch'},
58
+ }),
59
+ ),
60
+ ).toBe('revert')
61
+ expect(classifySubmissionError(new CorsOriginError({projectId: 'p'}))).toBe('revert')
62
+ })
63
+ })
@@ -0,0 +1,51 @@
1
+ import {ClientError, CorsOriginError} from '@sanity/client'
2
+
3
+ /**
4
+ * What the document store does with an outgoing transaction whose submission
5
+ * failed.
6
+ *
7
+ * - `retry`: the failure is about the connection, the credentials or
8
+ * contention on the server, not the transaction. Keep the transaction and
9
+ * its optimistic local state, and submit it again.
10
+ * - `accepted`: the server already recorded this transaction ID, so an earlier
11
+ * attempt was committed even though its response never arrived.
12
+ * - `revert`: the server rejected the transaction itself. Submitting it again
13
+ * cannot succeed, so drop it and rebuild local state from the server copy.
14
+ *
15
+ * @internal
16
+ */
17
+ export type SubmissionErrorClass = 'retry' | 'accepted' | 'revert'
18
+
19
+ // Mirrors Studio's `TERMINAL_COMMIT_STATUSES` (checkoutPair.ts) without 401. A
20
+ // 401 rejects the credentials (usually an expired session), not the
21
+ // transaction, so the same transaction succeeds once the user re-authenticates.
22
+ const REJECTED_TRANSACTION_STATUS_CODES = new Set([400, 402, 403, 404, 409, 410, 412, 413, 422])
23
+
24
+ function getErrorType(error: ClientError): unknown {
25
+ const details: unknown = error.details
26
+ if (details && typeof details === 'object' && 'type' in details) return details.type
27
+ return undefined
28
+ }
29
+
30
+ /**
31
+ * Classifies a failed transaction submission. Unknown failures (network
32
+ * errors, timeouts, 5xx and 429 responses, and anything that is not a
33
+ * `ClientError`) default to `retry`: wrongly retrying a rejected transaction
34
+ * leaves the document visibly unsynced with the edits still on screen, while
35
+ * wrongly reverting a transient failure silently discards the user's work.
36
+ *
37
+ * @internal
38
+ */
39
+ export function classifySubmissionError(error: unknown): SubmissionErrorClass {
40
+ if (error instanceof CorsOriginError) return 'revert'
41
+ if (!(error instanceof ClientError)) return 'retry'
42
+ if (error.statusCode === 409) {
43
+ const errorType = getErrorType(error)
44
+ if (errorType === 'transactionAlreadyExistsError') return 'accepted'
45
+ // contention while committing, not a verdict on the transaction; the
46
+ // server rolled it back, so the same transaction ID is safe to resend
47
+ if (errorType === 'transactionConflictError') return 'retry'
48
+ }
49
+ if (REJECTED_TRANSACTION_STATUS_CODES.has(error.statusCode)) return 'revert'
50
+ return 'retry'
51
+ }