@recordtimelabel/core 0.3.3 → 0.4.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,128 @@
1
+ /**
2
+ * Platform-neutral acknowledgement contract for RecordTimeLabel operation
3
+ * envelopes. Keep this module free of app and transport dependencies so the
4
+ * gateway and every client classify partial acknowledgements identically.
5
+ */
6
+
7
+ export const RECORD_TIMELABEL_OPERATION_RESULT_STATUSES = Object.freeze({
8
+ APPLIED: 'applied',
9
+ NOOP: 'noop',
10
+ REJECTED: 'rejected',
11
+ RETRYABLE: 'retryable'
12
+ });
13
+
14
+ const ALLOWED_OPERATION_RESULT_STATUSES = new Set(
15
+ Object.values(RECORD_TIMELABEL_OPERATION_RESULT_STATUSES)
16
+ );
17
+
18
+ const createCountMismatchError = (operations, results) => {
19
+ const error = new Error('operation_result_count_mismatch');
20
+ error.code = 'operation_result_count_mismatch';
21
+ error.operationCount = Array.isArray(operations) ? operations.length : null;
22
+ error.resultCount = Array.isArray(results) ? results.length : null;
23
+ return error;
24
+ };
25
+
26
+ const asObject = (value) => (
27
+ value && typeof value === 'object' && !Array.isArray(value) ? value : {}
28
+ );
29
+
30
+ const normalizeStatus = (result) => {
31
+ if (ALLOWED_OPERATION_RESULT_STATUSES.has(result.status)) {
32
+ return result.status;
33
+ }
34
+ if (result.applied === true) {
35
+ return RECORD_TIMELABEL_OPERATION_RESULT_STATUSES.APPLIED;
36
+ }
37
+ if (result.retryable === true) {
38
+ return RECORD_TIMELABEL_OPERATION_RESULT_STATUSES.RETRYABLE;
39
+ }
40
+ return RECORD_TIMELABEL_OPERATION_RESULT_STATUSES.REJECTED;
41
+ };
42
+
43
+ const isSuccessfulEnvelope = (response) => {
44
+ if (!response || typeof response !== 'object' || Array.isArray(response)) return false;
45
+ if (
46
+ response.success === false ||
47
+ response.ok === false ||
48
+ response.error ||
49
+ response.code === 'error'
50
+ ) return false;
51
+ return response.success === true || response.ok === true || (
52
+ response.success === undefined && response.ok === undefined
53
+ );
54
+ };
55
+
56
+ /**
57
+ * Normalize one positional result for every submitted operation.
58
+ *
59
+ * Unknown result properties are retained for forward compatibility, while
60
+ * all contract fields below are replaced with their normalized values.
61
+ */
62
+ export const normalizeRecordTimeLabelOperationResults = (operations, results) => {
63
+ if (!Array.isArray(operations) || !Array.isArray(results) || operations.length !== results.length) {
64
+ throw createCountMismatchError(operations, results);
65
+ }
66
+
67
+ return operations.map((rawOperation, index) => {
68
+ const operation = asObject(rawOperation);
69
+ const result = asObject(results[index]);
70
+ const status = normalizeStatus(result);
71
+ const operationId = result.operationId ?? result.id ?? operation.id ?? null;
72
+ const id = result.id ?? operationId;
73
+ const retryAfterMs = result.retryAfterMs;
74
+
75
+ return {
76
+ ...result,
77
+ operationId,
78
+ id,
79
+ type: result.type ?? operation.type ?? null,
80
+ applied: result.applied === true,
81
+ status,
82
+ retryable: status === RECORD_TIMELABEL_OPERATION_RESULT_STATUSES.RETRYABLE,
83
+ reason: typeof result.reason === 'string' ? result.reason : null,
84
+ retryAfterMs: typeof retryAfterMs === 'number' && Number.isFinite(retryAfterMs) && retryAfterMs > 0
85
+ ? retryAfterMs
86
+ : null
87
+ };
88
+ });
89
+ };
90
+
91
+ /**
92
+ * Normalize a successful operation envelope. During the compatibility
93
+ * window, a successful legacy envelope with no operationResults property is
94
+ * treated as acknowledging every submitted operation.
95
+ */
96
+ export const normalizeRecordTimeLabelEnvelopeResponse = (operations, response) => {
97
+ if (!Array.isArray(operations)) {
98
+ throw createCountMismatchError(operations, response?.operationResults);
99
+ }
100
+
101
+ const hasExplicitResults = Boolean(
102
+ response &&
103
+ typeof response === 'object' &&
104
+ Object.prototype.hasOwnProperty.call(response, 'operationResults')
105
+ );
106
+ if (hasExplicitResults) {
107
+ return normalizeRecordTimeLabelOperationResults(operations, response.operationResults);
108
+ }
109
+ if (!isSuccessfulEnvelope(response)) {
110
+ throw createCountMismatchError(operations, null);
111
+ }
112
+
113
+ return normalizeRecordTimeLabelOperationResults(
114
+ operations,
115
+ operations.map((operation) => ({
116
+ operationId: asObject(operation).id ?? null,
117
+ id: asObject(operation).id ?? null,
118
+ applied: true,
119
+ status: RECORD_TIMELABEL_OPERATION_RESULT_STATUSES.APPLIED
120
+ }))
121
+ );
122
+ };
123
+
124
+ export default {
125
+ RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
126
+ normalizeRecordTimeLabelOperationResults,
127
+ normalizeRecordTimeLabelEnvelopeResponse
128
+ };