@livestore/sync-s2 0.4.0-dev.8 → 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/dist/.tsbuildinfo +1 -1
- package/dist/api-schema.d.ts.map +1 -1
- package/dist/api-schema.js +1 -1
- package/dist/api-schema.js.map +1 -1
- package/dist/api-schema.test.js +1 -1
- package/dist/api-schema.test.js.map +1 -1
- package/dist/decode.d.ts +1 -1
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +1 -1
- package/dist/decode.js.map +1 -1
- package/dist/http-client-generated.d.ts +21 -21
- package/dist/http-client-generated.d.ts.map +1 -1
- package/dist/http-client-generated.js +64 -22
- package/dist/http-client-generated.js.map +1 -1
- package/dist/limits.d.ts +41 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +90 -0
- package/dist/limits.js.map +1 -0
- package/dist/limits.test.d.ts +2 -0
- package/dist/limits.test.d.ts.map +1 -0
- package/dist/limits.test.js +30 -0
- package/dist/limits.test.js.map +1 -0
- package/dist/make-s2-url.d.ts.map +1 -1
- package/dist/make-s2-url.js.map +1 -1
- package/dist/mod.d.ts +1 -0
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/s2-proxy-helpers.d.ts +26 -22
- package/dist/s2-proxy-helpers.d.ts.map +1 -1
- package/dist/s2-proxy-helpers.js +35 -33
- package/dist/s2-proxy-helpers.js.map +1 -1
- package/dist/sync-provider.d.ts +4 -4
- package/dist/sync-provider.d.ts.map +1 -1
- package/dist/sync-provider.js +49 -17
- package/dist/sync-provider.js.map +1 -1
- package/package.json +76 -13
- package/src/api-schema.test.ts +3 -1
- package/src/api-schema.ts +2 -1
- package/src/decode.ts +4 -3
- package/src/http-client-generated.ts +74 -53
- package/src/limits.test.ts +44 -0
- package/src/limits.ts +135 -0
- package/src/make-s2-url.ts +1 -0
- package/src/mod.ts +1 -0
- package/src/s2-proxy-helpers.ts +57 -47
- package/src/sync-provider.ts +78 -31
package/src/s2-proxy-helpers.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { LiveStoreEvent } from '@livestore/livestore'
|
|
7
|
+
|
|
7
8
|
import type { PullArgs } from './api-schema.ts'
|
|
9
|
+
import { chunkEventsForS2 } from './limits.ts'
|
|
8
10
|
import { makeS2StreamName } from './make-s2-url.ts'
|
|
9
11
|
|
|
10
12
|
/** Configuration for S2 connections */
|
|
@@ -15,8 +17,20 @@ export interface S2Config {
|
|
|
15
17
|
accountBase?: string
|
|
16
18
|
/** @default 'https://{basin}.b.aws.s2.dev/v1' */
|
|
17
19
|
basinBase?: string
|
|
20
|
+
/**
|
|
21
|
+
* When true, adds `S2-Basin` header to requests. This is required for s2-lite
|
|
22
|
+
* (the open-source self-hosted S2) which uses header-based basin routing instead
|
|
23
|
+
* of subdomain-based routing used by hosted S2.
|
|
24
|
+
* @see https://github.com/s2-streamstore/s2-lite
|
|
25
|
+
*/
|
|
26
|
+
lite?: boolean
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
export const isLiteMode = (config: S2Config): boolean => config.lite === true
|
|
30
|
+
|
|
31
|
+
const getBasinHeader = (config: S2Config): Record<string, string> =>
|
|
32
|
+
isLiteMode(config) === true ? { 's2-basin': config.basin } : {}
|
|
33
|
+
|
|
20
34
|
// URL construction helpers
|
|
21
35
|
export const getBasinUrl = (config: S2Config, path: string): string => {
|
|
22
36
|
const base = config.basinBase ?? `https://${config.basin}.b.aws.s2.dev/v1`
|
|
@@ -31,10 +45,10 @@ export const getAccountUrl = (config: S2Config, path: string): string => {
|
|
|
31
45
|
export const getStreamRecordsUrl = (
|
|
32
46
|
config: S2Config,
|
|
33
47
|
stream: string,
|
|
34
|
-
params?: { seq_num?: number; count?: number; clamp?: boolean },
|
|
48
|
+
params?: { seq_num?: number; count?: number; clamp?: boolean; wait?: number },
|
|
35
49
|
): string => {
|
|
36
50
|
const base = getBasinUrl(config, `/streams/${encodeURIComponent(stream)}/records`)
|
|
37
|
-
if (
|
|
51
|
+
if (params == null) return base
|
|
38
52
|
|
|
39
53
|
const searchParams = new URLSearchParams()
|
|
40
54
|
/** seq_num - The sequence number to start from. See: https://docs.s2.dev/api#seq_num */
|
|
@@ -43,8 +57,11 @@ export const getStreamRecordsUrl = (
|
|
|
43
57
|
if (params.count !== undefined) searchParams.append('count', params.count.toString())
|
|
44
58
|
/** clamp - Whether to clamp the response to the requested count. See: https://docs.s2.dev/api#clamp */
|
|
45
59
|
if (params.clamp !== undefined) searchParams.append('clamp', params.clamp.toString())
|
|
60
|
+
/** wait - How long to wait for new records before returning. See: https://docs.s2.dev/api#wait */
|
|
61
|
+
if (params.wait !== undefined) searchParams.append('wait', params.wait.toString())
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
const searchParamsString = searchParams.toString()
|
|
64
|
+
return searchParamsString.length > 0 ? `${base}?${searchParams}` : base
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
// Header helpers
|
|
@@ -52,14 +69,16 @@ export const getAuthHeaders = (token: string): Record<string, string> => ({
|
|
|
52
69
|
Authorization: `Bearer ${token}`,
|
|
53
70
|
})
|
|
54
71
|
|
|
55
|
-
export const getSSEHeaders = (
|
|
56
|
-
...getAuthHeaders(token),
|
|
72
|
+
export const getSSEHeaders = (config: S2Config): Record<string, string> => ({
|
|
73
|
+
...getAuthHeaders(config.token),
|
|
74
|
+
...getBasinHeader(config),
|
|
57
75
|
accept: 'text/event-stream',
|
|
58
76
|
's2-format': 'raw',
|
|
59
77
|
})
|
|
60
78
|
|
|
61
|
-
export const getPushHeaders = (
|
|
62
|
-
...getAuthHeaders(token),
|
|
79
|
+
export const getPushHeaders = (config: S2Config): Record<string, string> => ({
|
|
80
|
+
...getAuthHeaders(config.token),
|
|
81
|
+
...getBasinHeader(config),
|
|
63
82
|
'content-type': 'application/json',
|
|
64
83
|
's2-format': 'raw',
|
|
65
84
|
})
|
|
@@ -86,6 +105,7 @@ export const ensureStream = async (config: S2Config, stream: string): Promise<vo
|
|
|
86
105
|
method: 'POST',
|
|
87
106
|
headers: {
|
|
88
107
|
...getAuthHeaders(config.token),
|
|
108
|
+
...getBasinHeader(config),
|
|
89
109
|
'content-type': 'application/json',
|
|
90
110
|
},
|
|
91
111
|
body: JSON.stringify({ stream }),
|
|
@@ -111,42 +131,50 @@ export const buildPullRequest = ({
|
|
|
111
131
|
// cursor points to last processed record, seq_num needs to be the next record
|
|
112
132
|
const seq_num = args.s2SeqNum === 'from-start' ? 0 : args.s2SeqNum + 1
|
|
113
133
|
|
|
114
|
-
if (args.live) {
|
|
134
|
+
if (args.live === true) {
|
|
115
135
|
const url = getStreamRecordsUrl(config, streamName, { seq_num, clamp: true })
|
|
116
|
-
return { url, headers: getSSEHeaders(config
|
|
136
|
+
return { url, headers: getSSEHeaders(config) }
|
|
117
137
|
} else {
|
|
118
|
-
// Non-live pulls also stream over SSE
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return { url, headers: getSSEHeaders(config.token) }
|
|
138
|
+
// Non-live pulls also stream over SSE. We ask S2 to return immediately when
|
|
139
|
+
// the tail is reached by setting wait=0 which gives us an explicit
|
|
140
|
+
// end-of-stream without requesting an arbitrarily large page size.
|
|
141
|
+
const url = getStreamRecordsUrl(config, streamName, { seq_num, wait: 0, clamp: true })
|
|
142
|
+
return { url, headers: getSSEHeaders(config) }
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
|
|
127
|
-
export
|
|
146
|
+
export interface S2PushRequest {
|
|
147
|
+
readonly url: string
|
|
148
|
+
readonly method: 'POST'
|
|
149
|
+
readonly headers: Record<string, string>
|
|
150
|
+
readonly body: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds one or more append requests against S2. The helper applies the
|
|
155
|
+
* documented 1 MiB / 1000-record limits via `chunkEventsForS2`, so callers
|
|
156
|
+
* receive a request per compliant chunk instead of hitting 413 responses at
|
|
157
|
+
* runtime.
|
|
158
|
+
*/
|
|
159
|
+
export const buildPushRequests = ({
|
|
128
160
|
config,
|
|
129
161
|
storeId,
|
|
130
162
|
batch,
|
|
131
163
|
}: {
|
|
132
164
|
config: S2Config
|
|
133
165
|
storeId: string
|
|
134
|
-
batch: readonly LiveStoreEvent.
|
|
135
|
-
}): {
|
|
136
|
-
url: string
|
|
137
|
-
method: 'POST'
|
|
138
|
-
headers: Record<string, string>
|
|
139
|
-
/** JSON-encoded batch */
|
|
140
|
-
body: string
|
|
141
|
-
} => {
|
|
166
|
+
batch: readonly LiveStoreEvent.Global.Encoded[]
|
|
167
|
+
}): ReadonlyArray<S2PushRequest> => {
|
|
142
168
|
const streamName = makeS2StreamName(storeId)
|
|
143
169
|
const url = getBasinUrl(config, `/streams/${encodeURIComponent(streamName)}/records`)
|
|
144
|
-
|
|
170
|
+
const chunks = chunkEventsForS2(batch)
|
|
171
|
+
|
|
172
|
+
return chunks.map((chunk) => ({
|
|
145
173
|
url,
|
|
146
|
-
method: 'POST',
|
|
147
|
-
headers: getPushHeaders(config
|
|
148
|
-
body: JSON.stringify(
|
|
149
|
-
}
|
|
174
|
+
method: 'POST' as const,
|
|
175
|
+
headers: getPushHeaders(config),
|
|
176
|
+
body: JSON.stringify({ records: chunk.records }),
|
|
177
|
+
}))
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
// Response helpers
|
|
@@ -176,21 +204,3 @@ export const errorResponse = (message: string, status = 500): Response => {
|
|
|
176
204
|
headers: { 'content-type': 'application/json' },
|
|
177
205
|
})
|
|
178
206
|
}
|
|
179
|
-
|
|
180
|
-
// Batch formatting helper
|
|
181
|
-
export const formatBatchForS2 = (
|
|
182
|
-
batch: readonly LiveStoreEvent.AnyEncodedGlobal[],
|
|
183
|
-
): { records: { body: string }[] } => {
|
|
184
|
-
return {
|
|
185
|
-
records: batch.map((ev) => ({ body: JSON.stringify(ev) })),
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export const asCurl = (request: { url: string; method: string; headers: Record<string, string>; body?: string }) => {
|
|
190
|
-
const url = request.url
|
|
191
|
-
const method = request.method
|
|
192
|
-
const headers = Object.entries(request.headers).map(([key, value]) => `-H "${key}: ${value}"`)
|
|
193
|
-
const body = request.body
|
|
194
|
-
const headersStr = headers.join(' ')
|
|
195
|
-
return `curl -X ${method} ${url} ${headersStr} ${body ? `-d '${body}'` : ''}`
|
|
196
|
-
}
|
package/src/sync-provider.ts
CHANGED
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
* DO NOT couple these systems together or assume 1:1 correspondence.
|
|
29
29
|
*
|
|
30
30
|
* Errors
|
|
31
|
-
* - push →
|
|
31
|
+
* - push → UnknownError on non‑2xx; pull → UnknownError on non‑2xx; ping/connect map timeouts to offline.
|
|
32
32
|
* - The proxy should surface helpful status codes and error bodies.
|
|
33
33
|
*/
|
|
34
|
-
import {
|
|
34
|
+
import { SyncBackend, UnknownError } from '@livestore/common'
|
|
35
35
|
import type { EventSequenceNumber } from '@livestore/common/schema'
|
|
36
36
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
37
37
|
import {
|
|
@@ -47,9 +47,11 @@ import {
|
|
|
47
47
|
Stream,
|
|
48
48
|
SubscriptionRef,
|
|
49
49
|
} from '@livestore/utils/effect'
|
|
50
|
+
|
|
50
51
|
import * as ApiSchema from './api-schema.ts'
|
|
51
52
|
import { decodeReadBatch } from './decode.ts'
|
|
52
53
|
import * as HttpClientGenerated from './http-client-generated.ts'
|
|
54
|
+
import { chunkEventsForS2, S2LimitExceededError } from './limits.ts'
|
|
53
55
|
import type { SyncMetadata } from './types.ts'
|
|
54
56
|
|
|
55
57
|
export interface SyncS2Options {
|
|
@@ -70,14 +72,26 @@ export interface SyncS2Options {
|
|
|
70
72
|
}
|
|
71
73
|
retry?: {
|
|
72
74
|
/** Custom retry schedule for non-live pulls (default: 2 recurs, 100ms spaced) */
|
|
73
|
-
pull?: Schedule.Schedule<number,
|
|
75
|
+
pull?: Schedule.Schedule<number, UnknownError>
|
|
74
76
|
/** Custom retry schedule for pushes (default: 2 recurs, 100ms spaced) */
|
|
75
|
-
push?: Schedule.Schedule<number,
|
|
77
|
+
push?: Schedule.Schedule<number, UnknownError>
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
export const defaultRetry = Schedule.compose(Schedule.recurs(2), Schedule.spaced(100))
|
|
80
82
|
|
|
83
|
+
const getBrowserOrigin = () => {
|
|
84
|
+
if (typeof globalThis !== 'object' || globalThis === null || !('location' in globalThis)) {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { location } = globalThis as typeof globalThis & {
|
|
89
|
+
location?: { origin?: unknown } | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return typeof location?.origin === 'string' ? location.origin : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
export const makeSyncBackend =
|
|
82
96
|
({ endpoint, ping: pingOptions, retry }: SyncS2Options): SyncBackend.SyncBackendConstructor<SyncMetadata> =>
|
|
83
97
|
({ storeId, payload }) =>
|
|
@@ -89,9 +103,9 @@ export const makeSyncBackend =
|
|
|
89
103
|
|
|
90
104
|
const httpClient = yield* HttpClient.HttpClient
|
|
91
105
|
|
|
106
|
+
const browserOrigin = getBrowserOrigin()
|
|
92
107
|
const pullEndpointHasSameOrigin =
|
|
93
|
-
pullEndpoint.startsWith('/') ||
|
|
94
|
-
(globalThis.location !== undefined && globalThis.location.origin === new URL(pullEndpoint).origin)
|
|
108
|
+
pullEndpoint.startsWith('/') || (browserOrigin !== undefined && browserOrigin === new URL(pullEndpoint).origin)
|
|
95
109
|
|
|
96
110
|
const pingTimeout = pingOptions?.requestTimeout ?? 10_000
|
|
97
111
|
|
|
@@ -99,7 +113,7 @@ export const makeSyncBackend =
|
|
|
99
113
|
yield* httpClient.pipe(HttpClient.filterStatusOk).head(pingEndpoint)
|
|
100
114
|
yield* SubscriptionRef.set(isConnected, true)
|
|
101
115
|
}).pipe(
|
|
102
|
-
|
|
116
|
+
UnknownError.mapToUnknownError,
|
|
103
117
|
Effect.timeout(pingTimeout),
|
|
104
118
|
Effect.catchTag('TimeoutException', () => SubscriptionRef.set(isConnected, false)),
|
|
105
119
|
)
|
|
@@ -110,17 +124,16 @@ export const makeSyncBackend =
|
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
// No need to connect if the pull endpoint has the same origin as the current page
|
|
113
|
-
const connect: SyncBackend.SyncBackend<SyncMetadata>['connect'] =
|
|
114
|
-
? Effect.void
|
|
115
|
-
: ping.pipe(UnexpectedError.mapToUnexpectedError)
|
|
127
|
+
const connect: SyncBackend.SyncBackend<SyncMetadata>['connect'] =
|
|
128
|
+
pullEndpointHasSameOrigin === true ? Effect.void : ping.pipe(UnknownError.mapToUnknownError)
|
|
116
129
|
|
|
117
130
|
const runPullSse = (
|
|
118
131
|
cursor: Option.Option<{
|
|
119
|
-
eventSequenceNumber: EventSequenceNumber.
|
|
132
|
+
eventSequenceNumber: EventSequenceNumber.Global.Type
|
|
120
133
|
metadata: Option.Option<SyncMetadata>
|
|
121
134
|
}>,
|
|
122
135
|
live: boolean,
|
|
123
|
-
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>,
|
|
136
|
+
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>, UnknownError> => {
|
|
124
137
|
// Extract S2 seqNum from metadata for SSE cursor
|
|
125
138
|
const s2SeqNum = cursor.pipe(
|
|
126
139
|
Option.flatMap((_) => _.metadata),
|
|
@@ -144,7 +157,7 @@ export const makeSyncBackend =
|
|
|
144
157
|
const evt = msg.event.toLowerCase()
|
|
145
158
|
if (evt === 'ping') return Option.none()
|
|
146
159
|
if (evt === 'error') {
|
|
147
|
-
return yield* new
|
|
160
|
+
return yield* new UnknownError({ cause: new Error(`SSE error: ${msg.data}`) })
|
|
148
161
|
}
|
|
149
162
|
if (evt === 'batch') {
|
|
150
163
|
const readBatch = yield* Schema.decode(Schema.parseJson(HttpClientGenerated.ReadBatch))(msg.data)
|
|
@@ -176,28 +189,30 @@ export const makeSyncBackend =
|
|
|
176
189
|
}),
|
|
177
190
|
),
|
|
178
191
|
Stream.filterMap((_) => _), // filter out Option.none()
|
|
179
|
-
Stream.mapError((cause) =>
|
|
192
|
+
Stream.mapError((cause) =>
|
|
193
|
+
cause._tag === 'UnknownError' ? cause : new UnknownError({ cause }),
|
|
194
|
+
),
|
|
180
195
|
Stream.retry(retry?.pull ?? defaultRetry),
|
|
181
196
|
)
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
const ssePull = (
|
|
185
200
|
startCursor: Option.Option<{
|
|
186
|
-
eventSequenceNumber: EventSequenceNumber.
|
|
201
|
+
eventSequenceNumber: EventSequenceNumber.Global.Type
|
|
187
202
|
metadata: Option.Option<SyncMetadata>
|
|
188
203
|
}>,
|
|
189
|
-
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>,
|
|
204
|
+
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>, UnknownError> => {
|
|
190
205
|
const computeNextCursor = (
|
|
191
206
|
lastItem: Option.Option<SyncBackend.PullResItem<SyncMetadata>>,
|
|
192
207
|
current: Option.Option<{
|
|
193
|
-
eventSequenceNumber: EventSequenceNumber.
|
|
208
|
+
eventSequenceNumber: EventSequenceNumber.Global.Type
|
|
194
209
|
metadata: Option.Option<SyncMetadata>
|
|
195
210
|
}>,
|
|
196
211
|
) =>
|
|
197
212
|
lastItem.pipe(
|
|
198
213
|
Option.flatMap((item) => {
|
|
199
214
|
const lastBatchItem = item.batch.at(-1)
|
|
200
|
-
if (
|
|
215
|
+
if (lastBatchItem == null) return Option.none()
|
|
201
216
|
return Option.some({
|
|
202
217
|
eventSequenceNumber: lastBatchItem.eventEncoded.seqNum,
|
|
203
218
|
metadata: lastBatchItem.metadata,
|
|
@@ -208,11 +223,11 @@ export const makeSyncBackend =
|
|
|
208
223
|
|
|
209
224
|
const loop = (
|
|
210
225
|
cursor: Option.Option<{
|
|
211
|
-
eventSequenceNumber: EventSequenceNumber.
|
|
226
|
+
eventSequenceNumber: EventSequenceNumber.Global.Type
|
|
212
227
|
metadata: Option.Option<SyncMetadata>
|
|
213
228
|
}>,
|
|
214
229
|
isFirst: boolean,
|
|
215
|
-
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>,
|
|
230
|
+
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>, UnknownError> => {
|
|
216
231
|
const sseStream = (live: boolean) =>
|
|
217
232
|
runPullSse(cursor, live).pipe(
|
|
218
233
|
Stream.emitIfEmpty({
|
|
@@ -221,7 +236,7 @@ export const makeSyncBackend =
|
|
|
221
236
|
} as SyncBackend.PullResItem<SyncMetadata>),
|
|
222
237
|
)
|
|
223
238
|
|
|
224
|
-
const stream = isFirst ? sseStream(false) : sseStream(true)
|
|
239
|
+
const stream = isFirst === true ? sseStream(false) : sseStream(true)
|
|
225
240
|
|
|
226
241
|
return stream.pipe(
|
|
227
242
|
// Reconnect from last item if stream
|
|
@@ -235,7 +250,7 @@ export const makeSyncBackend =
|
|
|
235
250
|
return SyncBackend.of({
|
|
236
251
|
connect,
|
|
237
252
|
pull: (cursor, options) => {
|
|
238
|
-
if (options?.live) {
|
|
253
|
+
if (options?.live === true) {
|
|
239
254
|
return ssePull(cursor)
|
|
240
255
|
} else {
|
|
241
256
|
return runPullSse(cursor, false).pipe(
|
|
@@ -247,15 +262,47 @@ export const makeSyncBackend =
|
|
|
247
262
|
}
|
|
248
263
|
},
|
|
249
264
|
push: (batch) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
265
|
+
Effect.gen(function* () {
|
|
266
|
+
const toUnknownError = (cause: unknown): UnknownError => {
|
|
267
|
+
if (cause instanceof UnknownError) {
|
|
268
|
+
return cause
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (cause instanceof S2LimitExceededError) {
|
|
272
|
+
const note =
|
|
273
|
+
cause.limitType === 'record-metered-bytes'
|
|
274
|
+
? `S2 record exceeded ${cause.max} metered bytes (actual: ${cause.actual})`
|
|
275
|
+
: `S2 batch exceeded ${cause.max} (type: ${cause.limitType}, actual: ${cause.actual})`
|
|
276
|
+
|
|
277
|
+
return new UnknownError({
|
|
278
|
+
cause,
|
|
279
|
+
note,
|
|
280
|
+
payload: {
|
|
281
|
+
limitType: cause.limitType,
|
|
282
|
+
max: cause.max,
|
|
283
|
+
actual: cause.actual,
|
|
284
|
+
recordIndex: cause.recordIndex,
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return new UnknownError({ cause })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const chunks = yield* Effect.sync(() => chunkEventsForS2(batch)).pipe(Effect.mapError(toUnknownError))
|
|
293
|
+
|
|
294
|
+
for (const chunk of chunks) {
|
|
295
|
+
yield* HttpClientRequest.schemaBodyJson(ApiSchema.PushPayload)(HttpClientRequest.post(pushEndpoint), {
|
|
296
|
+
storeId,
|
|
297
|
+
batch: chunk.events,
|
|
298
|
+
}).pipe(
|
|
299
|
+
Effect.andThen(httpClient.pipe(HttpClient.filterStatusOk).execute),
|
|
300
|
+
Effect.andThen(HttpClientResponse.schemaBodyJson(ApiSchema.PushResponse)),
|
|
301
|
+
Effect.mapError(toUnknownError),
|
|
302
|
+
Effect.retry(retry?.push ?? defaultRetry),
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}),
|
|
259
306
|
ping,
|
|
260
307
|
isConnected,
|
|
261
308
|
metadata: {
|