eventsourcingdb 1.10.0 → 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.
- package/README.md +198 -24
- package/dist/Client.d.ts +2 -1
- package/dist/Client.js +79 -439
- package/dist/DbApiError.d.ts +17 -0
- package/dist/DbApiError.js +24 -0
- package/dist/ErrorCode.d.ts +68 -0
- package/dist/ErrorCode.js +70 -0
- package/dist/EventType.d.ts +2 -2
- package/dist/OutcomeUnknownError.d.ts +6 -0
- package/dist/OutcomeUnknownError.js +9 -0
- package/dist/ReadSubjectsOptions.d.ts +4 -0
- package/dist/Transport.d.ts +25 -0
- package/dist/detectApiVersion.d.ts +7 -0
- package/dist/detectApiVersion.js +47 -0
- package/dist/http/assertEventSourcingDb.d.ts +2 -0
- package/dist/http/assertEventSourcingDb.js +14 -0
- package/dist/http/createDbApiErrorFromResponse.d.ts +4 -0
- package/dist/http/createDbApiErrorFromResponse.js +20 -0
- package/dist/http/createStreamEndedError.d.ts +2 -0
- package/dist/http/createStreamEndedError.js +6 -0
- package/dist/http/isAbortError.d.ts +2 -0
- package/dist/http/isAbortError.js +2 -0
- package/dist/http/parseRetryAfter.d.ts +2 -0
- package/dist/http/parseRetryAfter.js +14 -0
- package/dist/http/runWrite.d.ts +6 -0
- package/dist/http/runWrite.js +25 -0
- package/dist/http/trackSending.d.ts +7 -0
- package/dist/http/trackSending.js +46 -0
- package/dist/http/waitUnlessAborted.d.ts +2 -0
- package/dist/http/waitUnlessAborted.js +21 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/stall/StallGuard.d.ts +0 -1
- package/dist/stall/StallGuard.js +3 -9
- package/dist/stall/streamWithStallGuard.d.ts +6 -0
- package/dist/stall/streamWithStallGuard.js +25 -0
- package/dist/v1/V1Transport.d.ts +23 -0
- package/dist/v1/V1Transport.js +339 -0
- package/dist/v1/getV1ErrorCode.d.ts +4 -0
- package/dist/v1/getV1ErrorCode.js +65 -0
- package/dist/v1/isV1Refusal.d.ts +3 -0
- package/dist/v1/isV1Refusal.js +8 -0
- package/dist/v1/stream/StreamRow.js +1 -0
- package/dist/v1/stream/StreamSubject.js +1 -0
- package/dist/{stream → v1/stream}/isStreamCloudEvent.js +5 -5
- package/dist/{stream → v1/stream}/isStreamError.js +2 -2
- package/dist/{stream → v1/stream}/isStreamEventType.js +3 -3
- package/dist/{stream → v1/stream}/isStreamHeartbeat.js +1 -1
- package/dist/{stream → v1/stream}/isStreamRow.js +1 -1
- package/dist/{stream → v1/stream}/isStreamSubject.js +2 -2
- package/dist/v2/V2Event.d.ts +16 -0
- package/dist/v2/V2Event.js +1 -0
- package/dist/v2/V2Item.d.ts +6 -0
- package/dist/v2/V2Item.js +1 -0
- package/dist/v2/V2Transport.d.ts +23 -0
- package/dist/v2/V2Transport.js +302 -0
- package/dist/v2/convertV2Item.d.ts +6 -0
- package/dist/v2/convertV2Item.js +52 -0
- package/dist/v2/isV2Refusal.d.ts +3 -0
- package/dist/v2/isV2Refusal.js +12 -0
- package/dist/v2/parseV2Item.d.ts +3 -0
- package/dist/v2/parseV2Item.js +30 -0
- package/dist/v2/readV2Answer.d.ts +4 -0
- package/dist/v2/readV2Answer.js +14 -0
- package/dist/v2/readV2Stream.d.ts +8 -0
- package/dist/v2/readV2Stream.js +34 -0
- package/dist/v2/toV2RequestBody.d.ts +8 -0
- package/dist/v2/toV2RequestBody.js +46 -0
- package/package.json +1 -1
- package/dist/HeartbeatTimeoutError.d.ts +0 -4
- package/dist/HeartbeatTimeoutError.js +0 -9
- /package/dist/{stream/StreamCloudEvent.js → ReadSubjectsOptions.js} +0 -0
- /package/dist/{stream/StreamError.js → Transport.js} +0 -0
- /package/dist/{stream → v1/stream}/StreamCloudEvent.d.ts +0 -0
- /package/dist/{stream/StreamEventType.js → v1/stream/StreamCloudEvent.js} +0 -0
- /package/dist/{stream → v1/stream}/StreamError.d.ts +0 -0
- /package/dist/{stream/StreamHeartbeat.js → v1/stream/StreamError.js} +0 -0
- /package/dist/{stream → v1/stream}/StreamEventType.d.ts +0 -0
- /package/dist/{stream/StreamRow.js → v1/stream/StreamEventType.js} +0 -0
- /package/dist/{stream → v1/stream}/StreamHeartbeat.d.ts +0 -0
- /package/dist/{stream/StreamSubject.js → v1/stream/StreamHeartbeat.js} +0 -0
- /package/dist/{stream → v1/stream}/StreamRow.d.ts +0 -0
- /package/dist/{stream → v1/stream}/StreamSubject.d.ts +0 -0
- /package/dist/{stream → v1/stream}/isStreamCloudEvent.d.ts +0 -0
- /package/dist/{stream → v1/stream}/isStreamError.d.ts +0 -0
- /package/dist/{stream → v1/stream}/isStreamEventType.d.ts +0 -0
- /package/dist/{stream → v1/stream}/isStreamHeartbeat.d.ts +0 -0
- /package/dist/{stream → v1/stream}/isStreamRow.d.ts +0 -0
- /package/dist/{stream → v1/stream}/isStreamSubject.d.ts +0 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createStreamEndedError } from '../http/createStreamEndedError.js';
|
|
2
|
+
import { runWrite } from '../http/runWrite.js';
|
|
3
|
+
import { runWithStallGuard } from '../stall/runWithStallGuard.js';
|
|
4
|
+
import { streamWithStallGuard } from '../stall/streamWithStallGuard.js';
|
|
5
|
+
import { hasShapeOf } from '../types/hasShapeOf.js';
|
|
6
|
+
import { isRecord } from '../types/isRecord.js';
|
|
7
|
+
import { isString } from '../types/isString.js';
|
|
8
|
+
import { convertV2ItemToEvent, convertV2ItemToEventType } from './convertV2Item.js';
|
|
9
|
+
import { isV2Refusal } from './isV2Refusal.js';
|
|
10
|
+
import { readV2Answer } from './readV2Answer.js';
|
|
11
|
+
import { readV2Stream } from './readV2Stream.js';
|
|
12
|
+
import { toObserveEventsBody, toReadEventsBody, toWriteEventsBody } from './toV2RequestBody.js';
|
|
13
|
+
// The v2 API reads with the method QUERY of RFC 10008, which, unlike GET,
|
|
14
|
+
// carries a body.
|
|
15
|
+
const methodQuery = 'QUERY';
|
|
16
|
+
const isEventOfType = (item, eventType) => item.type === 'event' &&
|
|
17
|
+
hasShapeOf(item.data, {
|
|
18
|
+
type: isString,
|
|
19
|
+
}) &&
|
|
20
|
+
item.data.type === eventType;
|
|
21
|
+
// Reads the answer of a write, a stream that closes with an end item once the
|
|
22
|
+
// write is done, and returns its items before that. Only an abort of the
|
|
23
|
+
// caller ends it early without failing, which then fails with the reason of
|
|
24
|
+
// the abort, since the write may have been stored nonetheless.
|
|
25
|
+
const readWriteAnswer = async (response, stallGuard, { signal, action }) => {
|
|
26
|
+
const items = [];
|
|
27
|
+
for await (const item of readV2Stream(response, stallGuard, { signal, action, hasEnd: true })) {
|
|
28
|
+
if (item.type === 'end') {
|
|
29
|
+
return items;
|
|
30
|
+
}
|
|
31
|
+
items.push(item);
|
|
32
|
+
}
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
throw createStreamEndedError(action);
|
|
35
|
+
};
|
|
36
|
+
// Sends the requests through the v2 API, which EventSourcingDB offers since
|
|
37
|
+
// version 1.3.
|
|
38
|
+
class V2Transport {
|
|
39
|
+
#url;
|
|
40
|
+
#apiToken;
|
|
41
|
+
#stallTimeout;
|
|
42
|
+
#getUrl(path) {
|
|
43
|
+
return new URL(path, this.#url).toString();
|
|
44
|
+
}
|
|
45
|
+
constructor({ url, apiToken, stallTimeout }) {
|
|
46
|
+
this.#url = url;
|
|
47
|
+
this.#apiToken = apiToken;
|
|
48
|
+
this.#stallTimeout = stallTimeout;
|
|
49
|
+
}
|
|
50
|
+
async ping(signal) {
|
|
51
|
+
const url = this.#getUrl('/api/v2/system/ping');
|
|
52
|
+
const stallTimeout = this.#stallTimeout;
|
|
53
|
+
await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
|
|
54
|
+
const response = await stallGuard.send(url, {
|
|
55
|
+
method: 'GET',
|
|
56
|
+
});
|
|
57
|
+
const item = await readV2Answer(response, stallGuard, 'ping');
|
|
58
|
+
if (!isEventOfType(item, 'io.eventsourcingdb.api.ping-received')) {
|
|
59
|
+
throw new Error('Failed to ping.');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async verifyApiToken(signal) {
|
|
64
|
+
const url = this.#getUrl('/api/v2/system/verify-token');
|
|
65
|
+
const stallTimeout = this.#stallTimeout;
|
|
66
|
+
await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
|
|
67
|
+
const response = await stallGuard.send(url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const item = await readV2Answer(response, stallGuard, 'verify API token');
|
|
74
|
+
if (!isEventOfType(item, 'io.eventsourcingdb.api.token-verified')) {
|
|
75
|
+
throw new Error('Failed to verify API token.');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async writeEvents(events, preconditions, signal) {
|
|
80
|
+
const url = this.#getUrl('/api/v2/data/write-events');
|
|
81
|
+
const stallTimeout = this.#stallTimeout;
|
|
82
|
+
const body = JSON.stringify(toWriteEventsBody(events, preconditions));
|
|
83
|
+
const action = 'write events';
|
|
84
|
+
return await runWrite({ signal, isRefusal: isV2Refusal }, async () => await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
|
|
85
|
+
const response = await stallGuard.send(url, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
89
|
+
'content-type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
body,
|
|
92
|
+
});
|
|
93
|
+
const items = await readWriteAnswer(response, stallGuard, { signal, action });
|
|
94
|
+
return items.map(item => {
|
|
95
|
+
if (item.type !== 'event') {
|
|
96
|
+
throw new Error(`Failed to ${action}, got an unexpected item.`);
|
|
97
|
+
}
|
|
98
|
+
return convertV2ItemToEvent(item, action);
|
|
99
|
+
});
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
readEvents(subject, options, signal) {
|
|
103
|
+
const url = this.#getUrl('/api/v2/data/read-events');
|
|
104
|
+
const apiToken = this.#apiToken;
|
|
105
|
+
const stallTimeout = this.#stallTimeout;
|
|
106
|
+
const action = 'read events';
|
|
107
|
+
return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) {
|
|
108
|
+
const response = await stallGuard.send(url, {
|
|
109
|
+
method: methodQuery,
|
|
110
|
+
headers: {
|
|
111
|
+
authorization: `Bearer ${apiToken}`,
|
|
112
|
+
'content-type': 'application/json',
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(toReadEventsBody(subject, options)),
|
|
115
|
+
});
|
|
116
|
+
for await (const item of readV2Stream(response, stallGuard, {
|
|
117
|
+
signal,
|
|
118
|
+
action,
|
|
119
|
+
hasEnd: true,
|
|
120
|
+
})) {
|
|
121
|
+
if (item.type === 'end') {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (item.type === 'event') {
|
|
125
|
+
yield convertV2ItemToEvent(item, action);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Failed to ${action}, got an unexpected item.`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
runEventQlQuery(query, signal) {
|
|
133
|
+
const url = this.#getUrl('/api/v2/data/run-eventql-query');
|
|
134
|
+
const apiToken = this.#apiToken;
|
|
135
|
+
const stallTimeout = this.#stallTimeout;
|
|
136
|
+
const action = 'run EventQL query';
|
|
137
|
+
return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) {
|
|
138
|
+
const response = await stallGuard.send(url, {
|
|
139
|
+
method: methodQuery,
|
|
140
|
+
headers: {
|
|
141
|
+
authorization: `Bearer ${apiToken}`,
|
|
142
|
+
'content-type': 'application/json',
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({ query }),
|
|
145
|
+
});
|
|
146
|
+
for await (const item of readV2Stream(response, stallGuard, {
|
|
147
|
+
signal,
|
|
148
|
+
action,
|
|
149
|
+
hasEnd: true,
|
|
150
|
+
})) {
|
|
151
|
+
if (item.type === 'end') {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// A row wraps its value, which may be any JSON value, null included.
|
|
155
|
+
if (item.type === 'row' && isRecord(item.data) && 'value' in item.data) {
|
|
156
|
+
yield item.data.value;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Failed to ${action}, got an unexpected item.`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Observing never ends on its own, so its stream has no end item, and fails
|
|
164
|
+
// if it ends nonetheless.
|
|
165
|
+
observeEvents(subject, options, signal) {
|
|
166
|
+
const url = this.#getUrl('/api/v2/data/observe-events');
|
|
167
|
+
const apiToken = this.#apiToken;
|
|
168
|
+
const stallTimeout = this.#stallTimeout;
|
|
169
|
+
const action = 'observe events';
|
|
170
|
+
return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) {
|
|
171
|
+
const response = await stallGuard.send(url, {
|
|
172
|
+
method: methodQuery,
|
|
173
|
+
headers: {
|
|
174
|
+
authorization: `Bearer ${apiToken}`,
|
|
175
|
+
'content-type': 'application/json',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(toObserveEventsBody(subject, options)),
|
|
178
|
+
});
|
|
179
|
+
for await (const item of readV2Stream(response, stallGuard, {
|
|
180
|
+
signal,
|
|
181
|
+
action,
|
|
182
|
+
hasEnd: false,
|
|
183
|
+
})) {
|
|
184
|
+
if (item.type === 'event') {
|
|
185
|
+
yield convertV2ItemToEvent(item, action);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`Failed to ${action}, got an unexpected item.`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async registerEventSchema(eventType, schema, signal) {
|
|
193
|
+
const url = this.#getUrl('/api/v2/data/register-event-schema');
|
|
194
|
+
const stallTimeout = this.#stallTimeout;
|
|
195
|
+
const body = JSON.stringify({
|
|
196
|
+
type: eventType,
|
|
197
|
+
schema,
|
|
198
|
+
});
|
|
199
|
+
const action = 'register event schema';
|
|
200
|
+
await runWrite({ signal, isRefusal: isV2Refusal }, async () => await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
|
|
201
|
+
const response = await stallGuard.send(url, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
205
|
+
'content-type': 'application/json',
|
|
206
|
+
},
|
|
207
|
+
body,
|
|
208
|
+
});
|
|
209
|
+
const items = await readWriteAnswer(response, stallGuard, { signal, action });
|
|
210
|
+
const [item, ...otherItems] = items;
|
|
211
|
+
if (item === undefined ||
|
|
212
|
+
otherItems.length > 0 ||
|
|
213
|
+
!isEventOfType(item, 'io.eventsourcingdb.api.event-schema-registered')) {
|
|
214
|
+
throw new Error(`Failed to ${action}, got an unexpected answer.`);
|
|
215
|
+
}
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
readSubjects(subject, { recursive }, signal) {
|
|
219
|
+
const url = this.#getUrl('/api/v2/data/read-subjects');
|
|
220
|
+
const apiToken = this.#apiToken;
|
|
221
|
+
const stallTimeout = this.#stallTimeout;
|
|
222
|
+
const action = 'read subjects';
|
|
223
|
+
return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) {
|
|
224
|
+
const response = await stallGuard.send(url, {
|
|
225
|
+
method: methodQuery,
|
|
226
|
+
headers: {
|
|
227
|
+
authorization: `Bearer ${apiToken}`,
|
|
228
|
+
'content-type': 'application/json',
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
subject,
|
|
232
|
+
options: { recursive },
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
for await (const item of readV2Stream(response, stallGuard, {
|
|
236
|
+
signal,
|
|
237
|
+
action,
|
|
238
|
+
hasEnd: true,
|
|
239
|
+
})) {
|
|
240
|
+
if (item.type === 'end') {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (item.type === 'subject' && isRecord(item.data) && isString(item.data.subject)) {
|
|
244
|
+
yield item.data.subject;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Failed to ${action}, got an unexpected item.`);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async readEventType(eventType, signal) {
|
|
252
|
+
const url = this.#getUrl('/api/v2/data/read-event-type');
|
|
253
|
+
const stallTimeout = this.#stallTimeout;
|
|
254
|
+
const action = 'read event type';
|
|
255
|
+
return await runWithStallGuard({ stallTimeout, signal }, async (stallGuard) => {
|
|
256
|
+
const response = await stallGuard.send(url, {
|
|
257
|
+
method: methodQuery,
|
|
258
|
+
headers: {
|
|
259
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
260
|
+
'content-type': 'application/json',
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
type: eventType,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
const item = await readV2Answer(response, stallGuard, action);
|
|
267
|
+
if (item.type !== 'eventType') {
|
|
268
|
+
throw new Error(`Failed to ${action}, got an unexpected answer.`);
|
|
269
|
+
}
|
|
270
|
+
return convertV2ItemToEventType(item, action);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
readEventTypes(signal) {
|
|
274
|
+
const url = this.#getUrl('/api/v2/data/read-event-types');
|
|
275
|
+
const apiToken = this.#apiToken;
|
|
276
|
+
const stallTimeout = this.#stallTimeout;
|
|
277
|
+
const action = 'read event types';
|
|
278
|
+
return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) {
|
|
279
|
+
const response = await stallGuard.send(url, {
|
|
280
|
+
method: methodQuery,
|
|
281
|
+
headers: {
|
|
282
|
+
authorization: `Bearer ${apiToken}`,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
for await (const item of readV2Stream(response, stallGuard, {
|
|
286
|
+
signal,
|
|
287
|
+
action,
|
|
288
|
+
hasEnd: true,
|
|
289
|
+
})) {
|
|
290
|
+
if (item.type === 'end') {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (item.type === 'eventType') {
|
|
294
|
+
yield convertV2ItemToEventType(item, action);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
throw new Error(`Failed to ${action}, got an unexpected item.`);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export { V2Transport };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Event } from '../Event.js';
|
|
2
|
+
import type { EventType } from '../EventType.js';
|
|
3
|
+
import type { V2Item } from './V2Item.js';
|
|
4
|
+
declare const convertV2ItemToEvent: (item: V2Item, action: string) => Event;
|
|
5
|
+
declare const convertV2ItemToEventType: (item: V2Item, action: string) => EventType;
|
|
6
|
+
export { convertV2ItemToEvent, convertV2ItemToEventType };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { convertCloudEventToEvent } from '../convertCloudEventToEvent.js';
|
|
2
|
+
import { getRawJsonMember } from '../json/getRawJsonMember.js';
|
|
3
|
+
import { hasShapeOf } from '../types/hasShapeOf.js';
|
|
4
|
+
import { isBoolean } from '../types/isBoolean.js';
|
|
5
|
+
import { isRecord } from '../types/isRecord.js';
|
|
6
|
+
import { isString } from '../types/isString.js';
|
|
7
|
+
import { isStringOrNull } from '../types/isStringOrNull.js';
|
|
8
|
+
const isV2Event = (value) => hasShapeOf(value, {
|
|
9
|
+
specversion: isString,
|
|
10
|
+
id: isString,
|
|
11
|
+
time: isString,
|
|
12
|
+
source: isString,
|
|
13
|
+
subject: isString,
|
|
14
|
+
type: isString,
|
|
15
|
+
datacontenttype: isString,
|
|
16
|
+
data: isRecord,
|
|
17
|
+
hash: isString,
|
|
18
|
+
predecessorhash: isString,
|
|
19
|
+
traceparent: isStringOrNull,
|
|
20
|
+
tracestate: isStringOrNull,
|
|
21
|
+
signature: isStringOrNull,
|
|
22
|
+
});
|
|
23
|
+
const isV2EventType = (value) => hasShapeOf(value, {
|
|
24
|
+
type: isString,
|
|
25
|
+
hasEvents: isBoolean,
|
|
26
|
+
schema: (schema) => schema === null || isRecord(schema),
|
|
27
|
+
});
|
|
28
|
+
// The hash of an event covers its data exactly as the server sent it, so the
|
|
29
|
+
// event keeps the text of its data from the text of the item.
|
|
30
|
+
const convertV2ItemToEvent = (item, action) => {
|
|
31
|
+
if (!isV2Event(item.data)) {
|
|
32
|
+
throw new Error(`Failed to ${action}, got an unexpected event.`);
|
|
33
|
+
}
|
|
34
|
+
const { traceparent, tracestate, ...cloudEvent } = item.data;
|
|
35
|
+
return convertCloudEventToEvent({
|
|
36
|
+
...cloudEvent,
|
|
37
|
+
...(traceparent === null ? {} : { traceparent }),
|
|
38
|
+
...(tracestate === null ? {} : { tracestate }),
|
|
39
|
+
}, getRawJsonMember(item.text, 'data'));
|
|
40
|
+
};
|
|
41
|
+
const convertV2ItemToEventType = (item, action) => {
|
|
42
|
+
if (!isV2EventType(item.data)) {
|
|
43
|
+
throw new Error(`Failed to ${action}, got an unexpected event type.`);
|
|
44
|
+
}
|
|
45
|
+
const { type, hasEvents, schema } = item.data;
|
|
46
|
+
return {
|
|
47
|
+
type,
|
|
48
|
+
hasEvents,
|
|
49
|
+
...(schema === null ? {} : { schema }),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export { convertV2ItemToEvent, convertV2ItemToEventType };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ErrorCode } from '../ErrorCode.js';
|
|
2
|
+
// The statuses with which the v2 API refuses a request before handling it.
|
|
3
|
+
const refusingStatuses = new Set([400, 401, 408, 413, 429, 503]);
|
|
4
|
+
// Tells whether the v2 API refused a write before storing anything. It does so
|
|
5
|
+
// with every error item, except for a failed store, and with some statuses.
|
|
6
|
+
const isV2Refusal = (error) => {
|
|
7
|
+
if (error.status === 200) {
|
|
8
|
+
return error.code !== ErrorCode.storeFailed;
|
|
9
|
+
}
|
|
10
|
+
return refusingStatuses.has(error.status);
|
|
11
|
+
};
|
|
12
|
+
export { isV2Refusal };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DbApiError } from '../DbApiError.js';
|
|
2
|
+
import { hasShapeOf } from '../types/hasShapeOf.js';
|
|
3
|
+
import { isRecord } from '../types/isRecord.js';
|
|
4
|
+
import { isString } from '../types/isString.js';
|
|
5
|
+
const isV2Envelope = (value) => hasShapeOf(value, {
|
|
6
|
+
type: isString,
|
|
7
|
+
data: (data) => data !== undefined,
|
|
8
|
+
});
|
|
9
|
+
const isV2ErrorData = (value) => hasShapeOf(value, {
|
|
10
|
+
code: isString,
|
|
11
|
+
description: isString,
|
|
12
|
+
details: isRecord,
|
|
13
|
+
});
|
|
14
|
+
// Parses an answer or an item of a stream of the v2 API, each of which wraps
|
|
15
|
+
// its data in an envelope that names its type. An error item fails with the
|
|
16
|
+
// error that it describes, which the server reports at the status 200.
|
|
17
|
+
const parseV2Item = (value, text, action) => {
|
|
18
|
+
if (!isV2Envelope(value)) {
|
|
19
|
+
throw new Error(`Failed to ${action}, got an unexpected answer.`);
|
|
20
|
+
}
|
|
21
|
+
if (value.type === 'error') {
|
|
22
|
+
if (!isV2ErrorData(value.data)) {
|
|
23
|
+
throw new Error(`Failed to ${action}, got an unexpected error.`);
|
|
24
|
+
}
|
|
25
|
+
const { code, description, details } = value.data;
|
|
26
|
+
throw new DbApiError({ status: 200, code, description, details });
|
|
27
|
+
}
|
|
28
|
+
return { type: value.type, data: value.data, text };
|
|
29
|
+
};
|
|
30
|
+
export { parseV2Item };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { assertEventSourcingDb } from '../http/assertEventSourcingDb.js';
|
|
2
|
+
import { createDbApiErrorFromResponse } from '../http/createDbApiErrorFromResponse.js';
|
|
3
|
+
import { readResponseText } from '../stall/readResponseText.js';
|
|
4
|
+
import { parseV2Item } from './parseV2Item.js';
|
|
5
|
+
// Reads the single answer of a request to the v2 API.
|
|
6
|
+
const readV2Answer = async (response, stallGuard, action) => {
|
|
7
|
+
assertEventSourcingDb(response);
|
|
8
|
+
if (response.status !== 200) {
|
|
9
|
+
throw await createDbApiErrorFromResponse(response, stallGuard);
|
|
10
|
+
}
|
|
11
|
+
const text = await readResponseText(response, stallGuard);
|
|
12
|
+
return parseV2Item(JSON.parse(text), text, action);
|
|
13
|
+
};
|
|
14
|
+
export { readV2Answer };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StallGuard } from '../stall/StallGuard.js';
|
|
2
|
+
import type { V2Item } from './V2Item.js';
|
|
3
|
+
declare const readV2Stream: (response: Response, stallGuard: StallGuard, { signal, action, hasEnd }: {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
action: string;
|
|
6
|
+
hasEnd: boolean;
|
|
7
|
+
}) => AsyncGenerator<V2Item, void, void>;
|
|
8
|
+
export { readV2Stream };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assertEventSourcingDb } from '../http/assertEventSourcingDb.js';
|
|
2
|
+
import { createDbApiErrorFromResponse } from '../http/createDbApiErrorFromResponse.js';
|
|
3
|
+
import { createStreamEndedError } from '../http/createStreamEndedError.js';
|
|
4
|
+
import { readNdJsonStream } from '../ndjson/readNdJsonStream.js';
|
|
5
|
+
import { parseV2Item } from './parseV2Item.js';
|
|
6
|
+
// Reads the items of a stream of the v2 API. It skips heartbeats and fails
|
|
7
|
+
// with the error of an error item. A stream that has an end closes with an
|
|
8
|
+
// end item, which it yields as well, and a stream that has none, such as
|
|
9
|
+
// observing events, never ends on its own. So a stream that ends otherwise
|
|
10
|
+
// broke off and fails, unless the caller aborted it.
|
|
11
|
+
const readV2Stream = async function* (response, stallGuard, { signal, action, hasEnd }) {
|
|
12
|
+
assertEventSourcingDb(response);
|
|
13
|
+
if (response.status !== 200) {
|
|
14
|
+
throw await createDbApiErrorFromResponse(response, stallGuard);
|
|
15
|
+
}
|
|
16
|
+
if (!response.body) {
|
|
17
|
+
throw new Error(`Failed to ${action}.`);
|
|
18
|
+
}
|
|
19
|
+
for await (const { value, text } of readNdJsonStream(response.body, stallGuard.signal, stallGuard)) {
|
|
20
|
+
const item = parseV2Item(value, text, action);
|
|
21
|
+
if (item.type === 'heartbeat') {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
yield item;
|
|
25
|
+
if (hasEnd && item.type === 'end') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
stallGuard.throwIfStalled();
|
|
30
|
+
if (!signal?.aborted) {
|
|
31
|
+
throw createStreamEndedError(action);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export { readV2Stream };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EventCandidate } from '../EventCandidate.js';
|
|
2
|
+
import type { ObserveEventsOptions } from '../ObserveEventsOptions.js';
|
|
3
|
+
import type { Precondition } from '../Precondition.js';
|
|
4
|
+
import type { ReadEventsOptions } from '../ReadEventsOptions.js';
|
|
5
|
+
declare const toWriteEventsBody: (events: EventCandidate[], preconditions: Precondition[]) => Record<string, unknown>;
|
|
6
|
+
declare const toReadEventsBody: (subject: string, options: ReadEventsOptions) => Record<string, unknown>;
|
|
7
|
+
declare const toObserveEventsBody: (subject: string, options: ObserveEventsOptions) => Record<string, unknown>;
|
|
8
|
+
export { toObserveEventsBody, toReadEventsBody, toWriteEventsBody };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The v2 API refuses unknown fields and null, so these build the bodies of its
|
|
2
|
+
// requests from the fields it knows, and leave out the ones without a value.
|
|
3
|
+
// JSON.stringify leaves out a field whose value is undefined.
|
|
4
|
+
const toBound = (bound) => bound ? { id: bound.id, type: bound.type } : undefined;
|
|
5
|
+
const toFromLatestEvent = (fromLatestEvent) => fromLatestEvent
|
|
6
|
+
? {
|
|
7
|
+
subject: fromLatestEvent.subject,
|
|
8
|
+
type: fromLatestEvent.type,
|
|
9
|
+
ifEventIsMissing: fromLatestEvent.ifEventIsMissing,
|
|
10
|
+
}
|
|
11
|
+
: undefined;
|
|
12
|
+
const toWriteEventsBody = (events, preconditions) => ({
|
|
13
|
+
events: events.map(event => ({
|
|
14
|
+
source: event.source,
|
|
15
|
+
subject: event.subject,
|
|
16
|
+
type: event.type,
|
|
17
|
+
data: event.data,
|
|
18
|
+
traceparent: event.traceparent ?? undefined,
|
|
19
|
+
tracestate: event.tracestate ?? undefined,
|
|
20
|
+
})),
|
|
21
|
+
preconditions: preconditions.length === 0
|
|
22
|
+
? undefined
|
|
23
|
+
: preconditions.map(precondition => ({
|
|
24
|
+
type: precondition.type,
|
|
25
|
+
payload: precondition.payload,
|
|
26
|
+
})),
|
|
27
|
+
});
|
|
28
|
+
const toReadEventsBody = (subject, options) => ({
|
|
29
|
+
subject,
|
|
30
|
+
options: {
|
|
31
|
+
recursive: options.recursive,
|
|
32
|
+
order: options.order ?? undefined,
|
|
33
|
+
lowerBound: toBound(options.lowerBound),
|
|
34
|
+
upperBound: toBound(options.upperBound),
|
|
35
|
+
fromLatestEvent: toFromLatestEvent(options.fromLatestEvent),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const toObserveEventsBody = (subject, options) => ({
|
|
39
|
+
subject,
|
|
40
|
+
options: {
|
|
41
|
+
recursive: options.recursive,
|
|
42
|
+
lowerBound: toBound(options.lowerBound),
|
|
43
|
+
fromLatestEvent: toFromLatestEvent(options.fromLatestEvent),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
export { toObserveEventsBody, toReadEventsBody, toWriteEventsBody };
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { defaultStallTimeout } from './stall/defaultStallTimeout.js';
|
|
2
|
-
import { formatDuration } from './stall/formatDuration.js';
|
|
3
|
-
class HeartbeatTimeoutError extends Error {
|
|
4
|
-
constructor(stallTimeout = defaultStallTimeout) {
|
|
5
|
-
super(`No event and no heartbeat arrived for ${formatDuration(stallTimeout)}.`);
|
|
6
|
-
this.name = 'HeartbeatTimeoutError';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
export { HeartbeatTimeoutError };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|