@recordtimelabel/core 0.3.2 → 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.
- package/README.md +72 -2
- package/package.json +5 -2
- package/src/compat.js +8 -0
- package/src/firestore-v2.js +27 -0
- package/src/index.js +1345 -59
- package/src/protocol.js +128 -0
package/src/protocol.js
ADDED
|
@@ -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
|
+
};
|