@gscdump/sdk 1.4.0 → 1.4.1
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/analytics-client.d.mts +1 -1
- package/dist/analytics-client.mjs +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/errors.d.mts +21 -1
- package/dist/index.d.mts +1 -1
- package/dist/{_chunks/request.mjs → request.mjs} +1 -1
- package/dist/search-console-stage.mjs +1 -1
- package/dist/site-triage.mjs +1 -1
- package/dist/utf8.mjs +14 -0
- package/dist/v1/http.d.mts +126 -0
- package/dist/v1/http.mjs +418 -0
- package/dist/v1/index.d.mts +2 -232
- package/dist/v1/index.mjs +2 -1439
- package/dist/v1/realtime.d.mts +109 -0
- package/dist/v1/realtime.mjs +1012 -0
- package/dist/webhook.d.mts +1 -1
- package/package.json +5 -5
- package/dist/_chunks/errors.d.mts +0 -22
- /package/dist/{_chunks/request.d.mts → request.d.mts} +0 -0
- /package/dist/{_chunks/search-console-signals.mjs → search-console-signals.mjs} +0 -0
package/dist/v1/index.mjs
CHANGED
|
@@ -1,1440 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
tag = "GscdumpV1Error";
|
|
4
|
-
code;
|
|
5
|
-
status;
|
|
6
|
-
requestId;
|
|
7
|
-
retryable;
|
|
8
|
-
details;
|
|
9
|
-
cause;
|
|
10
|
-
constructor(options) {
|
|
11
|
-
super(options.message);
|
|
12
|
-
this.name = "GscdumpV1Error";
|
|
13
|
-
this.code = options.code;
|
|
14
|
-
this.status = options.status;
|
|
15
|
-
this.requestId = options.requestId;
|
|
16
|
-
this.retryable = options.retryable;
|
|
17
|
-
this.details = options.details;
|
|
18
|
-
this.cause = options.cause;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
function isGscdumpV1Error(error) {
|
|
22
|
-
return error instanceof GscdumpV1Error;
|
|
23
|
-
}
|
|
24
|
-
const DEFAULT_API_ROOT = "https://gscdump.com/api";
|
|
25
|
-
const API_PREFIX_RE = /^\/api(?=\/)/;
|
|
26
|
-
const TRAILING_SLASH_RE = /\/+$/;
|
|
27
|
-
const LEADING_SLASH_RE = /^\/+/;
|
|
28
|
-
function validationDetails(cause) {
|
|
29
|
-
if (typeof cause === "object" && cause !== null && "issues" in cause) return { issues: cause.issues };
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
function requestValidationError(operationId, location, cause) {
|
|
33
|
-
return new GscdumpV1Error({
|
|
34
|
-
code: "request_validation",
|
|
35
|
-
message: `${operationId}: invalid request ${location}`,
|
|
36
|
-
retryable: false,
|
|
37
|
-
details: {
|
|
38
|
-
location,
|
|
39
|
-
...validationDetails(cause)
|
|
40
|
-
},
|
|
41
|
-
cause
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
function responseValidationError(operationId, status, requestId, cause) {
|
|
45
|
-
return new GscdumpV1Error({
|
|
46
|
-
code: "response_validation",
|
|
47
|
-
message: `${operationId}: response ${status} did not match the v1 contract`,
|
|
48
|
-
status,
|
|
49
|
-
requestId,
|
|
50
|
-
retryable: false,
|
|
51
|
-
details: validationDetails(cause),
|
|
52
|
-
cause
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function clampInteger(value, fallback, min, max) {
|
|
56
|
-
if (value === void 0) return fallback;
|
|
57
|
-
if (!Number.isFinite(value)) throw new TypeError("Retry configuration values must be finite numbers.");
|
|
58
|
-
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
59
|
-
}
|
|
60
|
-
function resolveRetryOptions(options) {
|
|
61
|
-
const baseDelayMs = clampInteger(options?.baseDelayMs, 250, 0, 6e4);
|
|
62
|
-
const maxDelayMs = clampInteger(options?.maxDelayMs, 2e3, baseDelayMs, 3e5);
|
|
63
|
-
return {
|
|
64
|
-
maxAttempts: clampInteger(options?.maxAttempts, 3, 1, 5),
|
|
65
|
-
baseDelayMs,
|
|
66
|
-
maxDelayMs
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function resolveApiUrl(apiRoot, contractPath) {
|
|
70
|
-
const relative = contractPath.replace(API_PREFIX_RE, "").replace(LEADING_SLASH_RE, "");
|
|
71
|
-
return `${apiRoot.replace(TRAILING_SLASH_RE, "")}/${relative}`;
|
|
72
|
-
}
|
|
73
|
-
function appendQueryValue(search, key, value) {
|
|
74
|
-
if (value === void 0) return;
|
|
75
|
-
if (Array.isArray(value)) {
|
|
76
|
-
for (const item of value) appendQueryValue(search, key, item);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
80
|
-
search.append(key, value === null ? "null" : String(value));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
search.append(key, JSON.stringify(value));
|
|
84
|
-
}
|
|
85
|
-
function appendQuery(url, query) {
|
|
86
|
-
if (query === void 0 || query === null) return url;
|
|
87
|
-
if (typeof query !== "object" || Array.isArray(query)) throw new TypeError("A query schema must produce an object.");
|
|
88
|
-
const search = new URLSearchParams();
|
|
89
|
-
for (const key of Object.keys(query).sort()) appendQueryValue(search, key, query[key]);
|
|
90
|
-
const serialized = search.toString();
|
|
91
|
-
return serialized ? `${url}?${serialized}` : url;
|
|
92
|
-
}
|
|
93
|
-
function assertAbsentLocation(operationId, location, value) {
|
|
94
|
-
if (value !== void 0) throw requestValidationError(operationId, location, /* @__PURE__ */ new TypeError(`${location} is not accepted by this operation.`));
|
|
95
|
-
}
|
|
96
|
-
function parseLocation(operation, location, value) {
|
|
97
|
-
const schema = operation.request[location];
|
|
98
|
-
if (schema === null) {
|
|
99
|
-
assertAbsentLocation(operation.id, location, value);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
return schema.parse(location === "headers" && value === void 0 ? {} : value);
|
|
104
|
-
} catch (cause) {
|
|
105
|
-
throw requestValidationError(operation.id, location, cause);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function prepareRequest(surface, operation, input, options) {
|
|
109
|
-
let path;
|
|
110
|
-
if (operation.request.params === null) {
|
|
111
|
-
assertAbsentLocation(operation.id, "params", input.params);
|
|
112
|
-
path = buildHttpOperationPath(surface, operation);
|
|
113
|
-
} else try {
|
|
114
|
-
path = buildHttpOperationPath(surface, operation, input.params);
|
|
115
|
-
} catch (cause) {
|
|
116
|
-
throw requestValidationError(operation.id, "params", cause);
|
|
117
|
-
}
|
|
118
|
-
const inputHeaders = input.headers;
|
|
119
|
-
const headerRequestId = inputHeaders?.["x-request-id"];
|
|
120
|
-
if (options.requestId && headerRequestId && options.requestId !== headerRequestId) throw requestValidationError(operation.id, "headers", /* @__PURE__ */ new TypeError("requestId conflicts with headers.x-request-id."));
|
|
121
|
-
const requestHeaders = options.requestId ? {
|
|
122
|
-
...inputHeaders ?? {},
|
|
123
|
-
"x-request-id": options.requestId
|
|
124
|
-
} : input.headers;
|
|
125
|
-
return {
|
|
126
|
-
path,
|
|
127
|
-
query: parseLocation(operation, "query", input.query),
|
|
128
|
-
headers: parseLocation(operation, "headers", requestHeaders),
|
|
129
|
-
body: parseLocation(operation, "body", input.body)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
async function resolveCredential(resolver) {
|
|
133
|
-
let credential;
|
|
134
|
-
try {
|
|
135
|
-
credential = typeof resolver === "function" ? await resolver() : resolver;
|
|
136
|
-
} catch (cause) {
|
|
137
|
-
throw new GscdumpV1Error({
|
|
138
|
-
code: "credential_resolution",
|
|
139
|
-
message: "Could not resolve the v1 Bearer credential.",
|
|
140
|
-
retryable: false,
|
|
141
|
-
details: {},
|
|
142
|
-
cause
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
if (typeof credential !== "string" || !credential.trim()) throw new GscdumpV1Error({
|
|
146
|
-
code: "credential_resolution",
|
|
147
|
-
message: "The v1 Bearer credential cannot be empty.",
|
|
148
|
-
retryable: false,
|
|
149
|
-
details: {}
|
|
150
|
-
});
|
|
151
|
-
return credential;
|
|
152
|
-
}
|
|
153
|
-
async function resolveBaseHeaders(resolver) {
|
|
154
|
-
return typeof resolver === "function" ? await resolver() : resolver;
|
|
155
|
-
}
|
|
156
|
-
async function buildRequestHeaders(options, operation, prepared, executeOptions) {
|
|
157
|
-
try {
|
|
158
|
-
const [baseHeaders, credential] = await Promise.all([resolveBaseHeaders(options.headers), resolveCredential(options.credential)]);
|
|
159
|
-
const headers = new Headers(baseHeaders);
|
|
160
|
-
for (const [key, value] of Object.entries(prepared.headers)) if (value !== void 0) headers.set(key, String(value));
|
|
161
|
-
headers.delete("x-api-key");
|
|
162
|
-
headers.set("accept", "application/json");
|
|
163
|
-
headers.set("authorization", `Bearer ${credential}`);
|
|
164
|
-
if (executeOptions.idempotencyKey) headers.set("idempotency-key", executeOptions.idempotencyKey);
|
|
165
|
-
if (prepared.body !== void 0) headers.set("content-type", "application/json");
|
|
166
|
-
return headers;
|
|
167
|
-
} catch (cause) {
|
|
168
|
-
if (cause instanceof GscdumpV1Error) throw cause;
|
|
169
|
-
throw requestValidationError(operation.id, "headers", cause);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
function isAbortError(error, signal) {
|
|
173
|
-
return signal?.aborted === true || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
174
|
-
}
|
|
175
|
-
function abortedRequestError(operation, executeOptions, cause = executeOptions.signal?.reason) {
|
|
176
|
-
return new GscdumpV1Error({
|
|
177
|
-
code: "aborted",
|
|
178
|
-
message: `${operation.id}: request aborted`,
|
|
179
|
-
requestId: executeOptions.requestId,
|
|
180
|
-
retryable: false,
|
|
181
|
-
details: {},
|
|
182
|
-
cause
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
async function waitForRetry(ms, operation, executeOptions) {
|
|
186
|
-
try {
|
|
187
|
-
await sleep(ms, executeOptions.signal);
|
|
188
|
-
} catch (cause) {
|
|
189
|
-
if (isAbortError(cause, executeOptions.signal)) throw abortedRequestError(operation, executeOptions, cause);
|
|
190
|
-
throw cause;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function awaitWithAbort(promise, operation, executeOptions) {
|
|
194
|
-
const signal = executeOptions.signal;
|
|
195
|
-
if (!signal) return promise;
|
|
196
|
-
if (signal.aborted) return Promise.reject(abortedRequestError(operation, executeOptions));
|
|
197
|
-
return new Promise((resolve, reject) => {
|
|
198
|
-
function onAbort() {
|
|
199
|
-
reject(abortedRequestError(operation, executeOptions));
|
|
200
|
-
}
|
|
201
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
202
|
-
promise.then((value) => {
|
|
203
|
-
signal.removeEventListener("abort", onAbort);
|
|
204
|
-
resolve(value);
|
|
205
|
-
}, (cause) => {
|
|
206
|
-
signal.removeEventListener("abort", onAbort);
|
|
207
|
-
reject(cause);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
function parseRetryAfter(value, now) {
|
|
212
|
-
if (value === null) return void 0;
|
|
213
|
-
const seconds = Number(value);
|
|
214
|
-
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1e3);
|
|
215
|
-
const date = Date.parse(value);
|
|
216
|
-
if (Number.isNaN(date)) return void 0;
|
|
217
|
-
return Math.max(0, date - now);
|
|
218
|
-
}
|
|
219
|
-
function retryDelay(attempt, retryAfter, options) {
|
|
220
|
-
const exponential = Math.min(options.maxDelayMs, options.baseDelayMs * 2 ** Math.max(0, attempt - 1));
|
|
221
|
-
return Math.max(exponential, parseRetryAfter(retryAfter, Date.now()) ?? 0);
|
|
222
|
-
}
|
|
223
|
-
function sleep(ms, signal) {
|
|
224
|
-
if (signal?.aborted) return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
225
|
-
return new Promise((resolve, reject) => {
|
|
226
|
-
const handle = setTimeout(() => {
|
|
227
|
-
signal?.removeEventListener("abort", onAbort);
|
|
228
|
-
resolve();
|
|
229
|
-
}, ms);
|
|
230
|
-
function onAbort() {
|
|
231
|
-
clearTimeout(handle);
|
|
232
|
-
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
233
|
-
}
|
|
234
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
async function readJson(response) {
|
|
238
|
-
const text = await response.text();
|
|
239
|
-
if (!text) return null;
|
|
240
|
-
return JSON.parse(text);
|
|
241
|
-
}
|
|
242
|
-
function requestIdFrom(response) {
|
|
243
|
-
return response.headers.get("x-request-id") ?? void 0;
|
|
244
|
-
}
|
|
245
|
-
function operationLookup(protocol) {
|
|
246
|
-
const operations = /* @__PURE__ */ new Map();
|
|
247
|
-
for (const surface of Object.values(protocol.surfaces)) for (const operation of Object.values(surface.operations)) operations.set(operation.id, {
|
|
248
|
-
operation,
|
|
249
|
-
surface
|
|
250
|
-
});
|
|
251
|
-
return operations;
|
|
252
|
-
}
|
|
253
|
-
function createGscdumpV1Client(options) {
|
|
254
|
-
const protocol = createGscdumpV1Protocol();
|
|
255
|
-
const operations = operationLookup(protocol);
|
|
256
|
-
const retryOptions = resolveRetryOptions(options.retry);
|
|
257
|
-
const apiRoot = options.apiRoot ?? DEFAULT_API_ROOT;
|
|
258
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
259
|
-
if (typeof fetchImpl !== "function") throw new TypeError("createGscdumpV1Client requires a fetch implementation in this runtime.");
|
|
260
|
-
const execute = executeUntyped;
|
|
261
|
-
async function executeUntyped(operationId, input, executeOptions = {}) {
|
|
262
|
-
const entry = operations.get(operationId);
|
|
263
|
-
if (!entry) throw new GscdumpV1Error({
|
|
264
|
-
code: "request_validation",
|
|
265
|
-
message: `Unknown gscdump v1 operation: ${operationId}`,
|
|
266
|
-
retryable: false,
|
|
267
|
-
details: { operationId }
|
|
268
|
-
});
|
|
269
|
-
const { operation, surface } = entry;
|
|
270
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) throw requestValidationError(operation.id, "input", /* @__PURE__ */ new TypeError("input must be an object."));
|
|
271
|
-
const prepared = prepareRequest(surface, operation, input, executeOptions);
|
|
272
|
-
let url;
|
|
273
|
-
try {
|
|
274
|
-
url = appendQuery(resolveApiUrl(apiRoot, prepared.path), prepared.query);
|
|
275
|
-
} catch (cause) {
|
|
276
|
-
throw requestValidationError(operation.id, "query", cause);
|
|
277
|
-
}
|
|
278
|
-
const canRetry = operation.semantics.retry === "idempotent";
|
|
279
|
-
for (let attempt = 1; attempt <= retryOptions.maxAttempts; attempt++) {
|
|
280
|
-
if (executeOptions.signal?.aborted) throw abortedRequestError(operation, executeOptions);
|
|
281
|
-
const headers = await awaitWithAbort(buildRequestHeaders(options, operation, prepared, executeOptions), operation, executeOptions);
|
|
282
|
-
if (executeOptions.signal?.aborted) throw abortedRequestError(operation, executeOptions);
|
|
283
|
-
let response;
|
|
284
|
-
try {
|
|
285
|
-
response = await fetchImpl(url, {
|
|
286
|
-
method: operation.method,
|
|
287
|
-
headers,
|
|
288
|
-
body: prepared.body === void 0 ? void 0 : JSON.stringify(prepared.body),
|
|
289
|
-
signal: executeOptions.signal
|
|
290
|
-
});
|
|
291
|
-
} catch (cause) {
|
|
292
|
-
if (isAbortError(cause, executeOptions.signal)) throw abortedRequestError(operation, executeOptions, cause);
|
|
293
|
-
if (canRetry && attempt < retryOptions.maxAttempts) {
|
|
294
|
-
await waitForRetry(retryDelay(attempt, null, retryOptions), operation, executeOptions);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
throw new GscdumpV1Error({
|
|
298
|
-
code: "network_error",
|
|
299
|
-
message: `${operation.id}: network request failed`,
|
|
300
|
-
requestId: executeOptions.requestId,
|
|
301
|
-
retryable: canRetry,
|
|
302
|
-
details: { attempts: attempt },
|
|
303
|
-
cause
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
const requestId = requestIdFrom(response) ?? executeOptions.requestId;
|
|
307
|
-
let payload;
|
|
308
|
-
try {
|
|
309
|
-
payload = await readJson(response);
|
|
310
|
-
} catch (cause) {
|
|
311
|
-
if (isAbortError(cause, executeOptions.signal)) throw abortedRequestError(operation, executeOptions, cause);
|
|
312
|
-
throw responseValidationError(operation.id, response.status, requestId, cause);
|
|
313
|
-
}
|
|
314
|
-
if (executeOptions.signal?.aborted) throw abortedRequestError(operation, executeOptions);
|
|
315
|
-
const responseContract = operation.responses[response.status];
|
|
316
|
-
if (responseContract) try {
|
|
317
|
-
return responseContract.client.parse(payload);
|
|
318
|
-
} catch (cause) {
|
|
319
|
-
throw responseValidationError(operation.id, response.status, requestId, cause);
|
|
320
|
-
}
|
|
321
|
-
if (response.ok) throw new GscdumpV1Error({
|
|
322
|
-
code: "protocol_error",
|
|
323
|
-
message: `${operation.id}: undeclared success status ${response.status}`,
|
|
324
|
-
status: response.status,
|
|
325
|
-
requestId,
|
|
326
|
-
retryable: false,
|
|
327
|
-
details: { status: response.status }
|
|
328
|
-
});
|
|
329
|
-
let envelope;
|
|
330
|
-
try {
|
|
331
|
-
envelope = protocol.schemas.errorEnvelope.client.parse(payload);
|
|
332
|
-
} catch (cause) {
|
|
333
|
-
throw responseValidationError(operation.id, response.status, requestId, cause);
|
|
334
|
-
}
|
|
335
|
-
if (!operation.errors.includes(envelope.error.code)) throw new GscdumpV1Error({
|
|
336
|
-
code: "protocol_error",
|
|
337
|
-
message: `${operation.id}: undeclared error code ${envelope.error.code}`,
|
|
338
|
-
status: response.status,
|
|
339
|
-
requestId: envelope.error.requestId,
|
|
340
|
-
retryable: false,
|
|
341
|
-
details: { code: envelope.error.code }
|
|
342
|
-
});
|
|
343
|
-
const apiError = new GscdumpV1Error({
|
|
344
|
-
code: envelope.error.code,
|
|
345
|
-
message: envelope.error.message,
|
|
346
|
-
status: response.status,
|
|
347
|
-
requestId: envelope.error.requestId,
|
|
348
|
-
retryable: envelope.error.retryable,
|
|
349
|
-
details: envelope.error.details ?? {}
|
|
350
|
-
});
|
|
351
|
-
if (canRetry && apiError.retryable && attempt < retryOptions.maxAttempts) {
|
|
352
|
-
await waitForRetry(retryDelay(attempt, response.headers.get("retry-after"), retryOptions), operation, executeOptions);
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
throw apiError;
|
|
356
|
-
}
|
|
357
|
-
throw new GscdumpV1Error({
|
|
358
|
-
code: "network_error",
|
|
359
|
-
message: `${operationId}: retry budget exhausted`,
|
|
360
|
-
retryable: false,
|
|
361
|
-
details: {}
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
execute,
|
|
366
|
-
getUserLifecycle: (input, executeOptions) => execute("partner.users.lifecycle.get", input, executeOptions),
|
|
367
|
-
listAvailableSites: (input, executeOptions) => execute("partner.users.sites.available.list", input, executeOptions),
|
|
368
|
-
createSite: (input, executeOptions) => execute("partner.users.sites.create", input, executeOptions),
|
|
369
|
-
createUser: (input, executeOptions) => execute("partner.users.create", input, executeOptions),
|
|
370
|
-
updateUserTokens: (input, executeOptions) => execute("partner.users.tokens.update", input, executeOptions),
|
|
371
|
-
getSiteIndexing: (input, executeOptions) => execute("partner.sites.indexing.get", input, executeOptions),
|
|
372
|
-
listSiteIndexingUrls: (input, executeOptions) => execute("partner.sites.indexing.urls.list", input, executeOptions),
|
|
373
|
-
getSiteIndexingDiagnostics: (input, executeOptions) => execute("partner.sites.indexing.diagnostics.get", input, executeOptions),
|
|
374
|
-
getSiteSitemaps: (input, executeOptions) => execute("partner.sites.sitemaps.get", input, executeOptions),
|
|
375
|
-
getSiteSitemapChanges: (input, executeOptions) => execute("partner.sites.sitemaps.changes.get", input, executeOptions),
|
|
376
|
-
getSiteAnalysis: (input, executeOptions) => execute("partner.sites.analysis.get", input, executeOptions),
|
|
377
|
-
getSiteAnalysisBundle: (input, executeOptions) => execute("partner.sites.analysis.bundle.get", input, executeOptions),
|
|
378
|
-
deleteSite: (input, executeOptions) => execute("partner.sites.delete", input, executeOptions),
|
|
379
|
-
getCanonicalMismatches: (input, executeOptions) => execute("partner.sites.canonical.mismatches.get", input, executeOptions),
|
|
380
|
-
inspectSiteUrls: (input, executeOptions) => execute("partner.sites.indexing.inspect.create", input, executeOptions),
|
|
381
|
-
recoverSitePermission: (input, executeOptions) => execute("partner.sites.permission.recover", input, executeOptions),
|
|
382
|
-
queryKeywordSparklines: (input, executeOptions) => execute("partner.sites.keyword.sparklines.query", input, executeOptions),
|
|
383
|
-
getQueryTrend: (input, executeOptions) => execute("partner.sites.query.trend.get", input, executeOptions),
|
|
384
|
-
getPageTrend: (input, executeOptions) => execute("partner.sites.page.trend.get", input, executeOptions),
|
|
385
|
-
getContentVelocity: (input, executeOptions) => execute("partner.sites.content.velocity.get", input, executeOptions),
|
|
386
|
-
getCtrCurve: (input, executeOptions) => execute("partner.sites.ctr.curve.get", input, executeOptions),
|
|
387
|
-
getDarkTraffic: (input, executeOptions) => execute("partner.sites.dark.traffic.get", input, executeOptions),
|
|
388
|
-
getDeviceGap: (input, executeOptions) => execute("partner.sites.device.gap.get", input, executeOptions),
|
|
389
|
-
getKeywordBreadth: (input, executeOptions) => execute("partner.sites.keyword.breadth.get", input, executeOptions),
|
|
390
|
-
getPositionDistribution: (input, executeOptions) => execute("partner.sites.position.distribution.get", input, executeOptions),
|
|
391
|
-
getTopAssociation: (input, executeOptions) => execute("partner.sites.top.association.get", input, executeOptions),
|
|
392
|
-
getIndexPercent: (input, executeOptions) => execute("partner.sites.index.percent.get", input, executeOptions),
|
|
393
|
-
createSitemapAction: (input, executeOptions) => execute("partner.sites.sitemaps.action.create", input, executeOptions),
|
|
394
|
-
querySitemapMembership: (input, executeOptions) => execute("partner.sites.sitemaps.membership.query", input, executeOptions),
|
|
395
|
-
createTeam: (input, executeOptions) => execute("partner.teams.create", input, executeOptions),
|
|
396
|
-
renameTeam: (input, executeOptions) => execute("partner.teams.rename", input, executeOptions),
|
|
397
|
-
deleteTeam: (input, executeOptions) => execute("partner.teams.delete", input, executeOptions),
|
|
398
|
-
listTeamMembers: (input, executeOptions) => execute("partner.teams.members.list", input, executeOptions),
|
|
399
|
-
addTeamMember: (input, executeOptions) => execute("partner.teams.members.add", input, executeOptions),
|
|
400
|
-
updateTeamMemberRole: (input, executeOptions) => execute("partner.teams.members.role.update", input, executeOptions),
|
|
401
|
-
removeTeamMember: (input, executeOptions) => execute("partner.teams.members.remove", input, executeOptions),
|
|
402
|
-
updateSiteTeam: (input, executeOptions) => execute("partner.sites.team.update", input, executeOptions),
|
|
403
|
-
getTeamCatalog: (input, executeOptions) => execute("partner.teams.catalog.get", input, executeOptions),
|
|
404
|
-
bindTeamCatalog: (input, executeOptions) => execute("partner.teams.catalog.bind", input, executeOptions),
|
|
405
|
-
getSiteIntIdCrosswalk: (input, executeOptions) => execute("partner.users.sites.crosswalk.get", input, executeOptions),
|
|
406
|
-
deleteUser: (input, executeOptions) => execute("partner.users.delete", input, executeOptions),
|
|
407
|
-
createVerificationToken: (input, executeOptions) => execute("partner.users.verification.token.create", input, executeOptions),
|
|
408
|
-
addAndVerifySite: (input, executeOptions) => execute("partner.users.sites.verify.create", input, executeOptions),
|
|
409
|
-
queryCrossSource: (input, executeOptions) => execute("partner.sites.cross.source.query", input, executeOptions),
|
|
410
|
-
enrichKeywords: (input, executeOptions) => execute("partner.keywords.enrich.query", input, executeOptions),
|
|
411
|
-
queryAnalyticsRows: (input, executeOptions) => execute("analytics.rows.query", input, executeOptions),
|
|
412
|
-
queryAnalyticsReport: (input, executeOptions) => execute("analytics.reports.query", input, executeOptions),
|
|
413
|
-
queryAnalyticsReportDetail: (input, executeOptions) => execute("analytics.reports.detail.query", input, executeOptions),
|
|
414
|
-
getRealtimeStreamHead: (input = {}, executeOptions) => execute("realtime.stream.head.get", input, executeOptions),
|
|
415
|
-
createRealtimeTicket: (input, executeOptions) => execute("realtime.tickets.create", input, executeOptions)
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
function utf8Size(value) {
|
|
419
|
-
let bytes = 0;
|
|
420
|
-
for (let index = 0; index < value.length; index++) {
|
|
421
|
-
const code = value.charCodeAt(index);
|
|
422
|
-
if (code <= 127) bytes++;
|
|
423
|
-
else if (code <= 2047) bytes += 2;
|
|
424
|
-
else if (code >= 55296 && code <= 56319 && index + 1 < value.length && value.charCodeAt(index + 1) >= 56320 && value.charCodeAt(index + 1) <= 57343) {
|
|
425
|
-
bytes += 4;
|
|
426
|
-
index++;
|
|
427
|
-
} else bytes += 3;
|
|
428
|
-
}
|
|
429
|
-
return bytes;
|
|
430
|
-
}
|
|
431
|
-
const GSCDUMP_REALTIME_V1_SDK_VERSION = "1.4.0";
|
|
432
|
-
var GscdumpRealtimeV1Error = class extends Error {
|
|
433
|
-
tag = "GscdumpRealtimeV1Error";
|
|
434
|
-
code;
|
|
435
|
-
retryable;
|
|
436
|
-
terminal;
|
|
437
|
-
details;
|
|
438
|
-
cause;
|
|
439
|
-
constructor(options) {
|
|
440
|
-
super(options.message);
|
|
441
|
-
this.name = "GscdumpRealtimeV1Error";
|
|
442
|
-
this.code = options.code;
|
|
443
|
-
this.retryable = options.retryable;
|
|
444
|
-
this.terminal = options.terminal;
|
|
445
|
-
this.details = options.details ?? {};
|
|
446
|
-
this.cause = options.cause;
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
var StaleEpochError = class extends Error {
|
|
450
|
-
constructor() {
|
|
451
|
-
super("The realtime connection epoch is no longer active.");
|
|
452
|
-
this.name = "StaleEpochError";
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
function createMemoryCursorStore() {
|
|
456
|
-
let value = null;
|
|
457
|
-
return {
|
|
458
|
-
load: () => value,
|
|
459
|
-
save: (cursor) => {
|
|
460
|
-
value = { ...cursor };
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
function defaultRuntime() {
|
|
465
|
-
return {
|
|
466
|
-
createSocket(url, protocols) {
|
|
467
|
-
if (typeof globalThis.WebSocket !== "function") throw new GscdumpRealtimeV1Error({
|
|
468
|
-
code: "runtime_unavailable",
|
|
469
|
-
message: "This runtime does not provide WebSocket; inject a realtime runtime port.",
|
|
470
|
-
retryable: false,
|
|
471
|
-
terminal: true
|
|
472
|
-
});
|
|
473
|
-
return new globalThis.WebSocket(url, [...protocols]);
|
|
474
|
-
},
|
|
475
|
-
setTimeout: (handler, ms) => globalThis.setTimeout(handler, ms),
|
|
476
|
-
clearTimeout: (handle) => globalThis.clearTimeout(handle),
|
|
477
|
-
random: () => Math.random(),
|
|
478
|
-
now: () => Date.now()
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
const UTF8_DECODER = new TextDecoder();
|
|
482
|
-
const REALTIME_EVENT_NAMES = new Set(REALTIME_V1_EVENT_NAMES);
|
|
483
|
-
const REALTIME_RESOURCE_TYPES = new Set(REALTIME_V1_RESOURCE_TYPES);
|
|
484
|
-
const APPLIED_EVENT_CACHE_MAX = 1e4;
|
|
485
|
-
async function messageText(raw) {
|
|
486
|
-
if (typeof raw === "string") return raw;
|
|
487
|
-
if (raw instanceof ArrayBuffer) return UTF8_DECODER.decode(raw);
|
|
488
|
-
if (ArrayBuffer.isView(raw)) return UTF8_DECODER.decode(new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength));
|
|
489
|
-
if (typeof Blob !== "undefined" && raw instanceof Blob) return raw.text();
|
|
490
|
-
throw new TypeError("Realtime frames must be text or UTF-8 binary data.");
|
|
491
|
-
}
|
|
492
|
-
function compareSequence(left, right) {
|
|
493
|
-
const a = BigInt(left);
|
|
494
|
-
const b = BigInt(right);
|
|
495
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
496
|
-
}
|
|
497
|
-
function nextSequence(sequence) {
|
|
498
|
-
return (BigInt(sequence) + 1n).toString();
|
|
499
|
-
}
|
|
500
|
-
function laterCursor(left, right) {
|
|
501
|
-
return compareSequence(left.sequence, right.sequence) >= 0 ? left : right;
|
|
502
|
-
}
|
|
503
|
-
function sameCursor(left, right) {
|
|
504
|
-
return left?.streamId === right?.streamId && left?.sequence === right?.sequence;
|
|
505
|
-
}
|
|
506
|
-
function parseErrorDetails(cause) {
|
|
507
|
-
if (typeof cause === "object" && cause !== null && "issues" in cause) return { issues: cause.issues };
|
|
508
|
-
return {};
|
|
509
|
-
}
|
|
510
|
-
function safeRandom(random) {
|
|
511
|
-
const value = random();
|
|
512
|
-
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : .5;
|
|
513
|
-
}
|
|
514
|
-
function createGscdumpRealtimeV1Client(options) {
|
|
515
|
-
const protocol = createGscdumpV1Protocol();
|
|
516
|
-
const runtime = options.runtime ?? defaultRuntime();
|
|
517
|
-
const cursorStore = options.cursorStore ?? createMemoryCursorStore();
|
|
518
|
-
const sdkVersion = options.sdkVersion ?? "1.4.0";
|
|
519
|
-
if (!sdkVersion) throw new TypeError("sdkVersion cannot be empty.");
|
|
520
|
-
let running = false;
|
|
521
|
-
let epoch = 0;
|
|
522
|
-
let reconnectTimer;
|
|
523
|
-
let heartbeatTimer;
|
|
524
|
-
let rotationTimer;
|
|
525
|
-
let current = null;
|
|
526
|
-
let immediateExpiredTicketRetry = true;
|
|
527
|
-
let cursor = null;
|
|
528
|
-
let streamId = null;
|
|
529
|
-
let head = null;
|
|
530
|
-
let attempt = 0;
|
|
531
|
-
let transport = "idle";
|
|
532
|
-
let freshness = "unknown";
|
|
533
|
-
let lastError = null;
|
|
534
|
-
const eventFailures = /* @__PURE__ */ new Map();
|
|
535
|
-
const appliedEventIds = /* @__PURE__ */ new Set();
|
|
536
|
-
const appliedEventOrder = [];
|
|
537
|
-
let appliedEventCursor = 0;
|
|
538
|
-
function getSnapshot() {
|
|
539
|
-
return {
|
|
540
|
-
transport,
|
|
541
|
-
freshness,
|
|
542
|
-
attempt,
|
|
543
|
-
streamId,
|
|
544
|
-
cursor: cursor ? { ...cursor } : null,
|
|
545
|
-
head: head ? { ...head } : null,
|
|
546
|
-
error: lastError
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
function observe(observation) {
|
|
550
|
-
if (!options.onObservation) return;
|
|
551
|
-
try {
|
|
552
|
-
Promise.resolve(options.onObservation(observation)).catch(() => {});
|
|
553
|
-
} catch {}
|
|
554
|
-
}
|
|
555
|
-
function emitState() {
|
|
556
|
-
observe({
|
|
557
|
-
type: "state",
|
|
558
|
-
state: getSnapshot()
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
function setState(nextTransport, nextFreshness = freshness) {
|
|
562
|
-
transport = nextTransport;
|
|
563
|
-
freshness = nextFreshness;
|
|
564
|
-
emitState();
|
|
565
|
-
}
|
|
566
|
-
function reportError(error) {
|
|
567
|
-
lastError = error;
|
|
568
|
-
observe({
|
|
569
|
-
type: "error",
|
|
570
|
-
error,
|
|
571
|
-
state: getSnapshot()
|
|
572
|
-
});
|
|
573
|
-
emitState();
|
|
574
|
-
}
|
|
575
|
-
function clearTimer(handle) {
|
|
576
|
-
if (handle !== void 0) runtime.clearTimeout(handle);
|
|
577
|
-
}
|
|
578
|
-
function clearConnectionTimers() {
|
|
579
|
-
clearTimer(heartbeatTimer);
|
|
580
|
-
clearTimer(rotationTimer);
|
|
581
|
-
heartbeatTimer = void 0;
|
|
582
|
-
rotationTimer = void 0;
|
|
583
|
-
}
|
|
584
|
-
function ensureEpoch(context) {
|
|
585
|
-
if (!running || current !== context || context.closed || context.epoch !== epoch) throw new StaleEpochError();
|
|
586
|
-
}
|
|
587
|
-
function closeSocket(context, code, reason) {
|
|
588
|
-
if (context.closed) return;
|
|
589
|
-
context.closed = true;
|
|
590
|
-
if (current === context) {
|
|
591
|
-
current = null;
|
|
592
|
-
epoch++;
|
|
593
|
-
}
|
|
594
|
-
clearConnectionTimers();
|
|
595
|
-
try {
|
|
596
|
-
context.socket.close(code, reason);
|
|
597
|
-
} catch {}
|
|
598
|
-
}
|
|
599
|
-
function reconnectBackoff() {
|
|
600
|
-
const exponent = Math.max(0, attempt - 1);
|
|
601
|
-
const cap = Math.min(GSCDUMP_REALTIME_CONNECTION_POLICY.reconnectMaxDelayMs, GSCDUMP_REALTIME_CONNECTION_POLICY.reconnectBaseDelayMs * 2 ** exponent);
|
|
602
|
-
return cap / 2 + safeRandom(runtime.random) * cap / 2;
|
|
603
|
-
}
|
|
604
|
-
function scheduleReconnect(context, reason, minimumDelayMs = 0, immediate = false) {
|
|
605
|
-
if (!running) return;
|
|
606
|
-
if (context) closeSocket(context, GSCDUMP_REALTIME_CLOSE_CODES.internalError, reason);
|
|
607
|
-
clearTimer(reconnectTimer);
|
|
608
|
-
attempt++;
|
|
609
|
-
const delay = immediate ? minimumDelayMs : Math.max(minimumDelayMs, reconnectBackoff());
|
|
610
|
-
setState("waiting", freshness === "unknown" || freshness === "degraded" ? freshness : "stale");
|
|
611
|
-
reconnectTimer = runtime.setTimeout(() => {
|
|
612
|
-
reconnectTimer = void 0;
|
|
613
|
-
beginAttempt();
|
|
614
|
-
}, delay);
|
|
615
|
-
}
|
|
616
|
-
function terminal(error, context = current) {
|
|
617
|
-
running = false;
|
|
618
|
-
clearTimer(reconnectTimer);
|
|
619
|
-
reconnectTimer = void 0;
|
|
620
|
-
if (context) closeSocket(context, GSCDUMP_REALTIME_CLOSE_CODES.normal, "terminal");
|
|
621
|
-
freshness = "degraded";
|
|
622
|
-
transport = "terminal";
|
|
623
|
-
reportError(error);
|
|
624
|
-
}
|
|
625
|
-
function assertStream(value, context, label) {
|
|
626
|
-
if (value.streamId !== context.ticket.head.streamId) throw new GscdumpRealtimeV1Error({
|
|
627
|
-
code: "protocol_error",
|
|
628
|
-
message: `${label} did not match the ticket's exact stream.`,
|
|
629
|
-
retryable: false,
|
|
630
|
-
terminal: true,
|
|
631
|
-
details: {
|
|
632
|
-
expected: context.ticket.head.streamId,
|
|
633
|
-
received: value.streamId
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
function sendJson(context, value) {
|
|
638
|
-
ensureEpoch(context);
|
|
639
|
-
const serialized = JSON.stringify(value);
|
|
640
|
-
if (utf8Size(serialized) > GSCDUMP_REALTIME_LIMITS.clientFrameMaxBytes) throw new GscdumpRealtimeV1Error({
|
|
641
|
-
code: "protocol_error",
|
|
642
|
-
message: "The generated client frame exceeded the v1 size limit.",
|
|
643
|
-
retryable: false,
|
|
644
|
-
terminal: true
|
|
645
|
-
});
|
|
646
|
-
try {
|
|
647
|
-
context.socket.send(serialized);
|
|
648
|
-
} catch (cause) {
|
|
649
|
-
throw new GscdumpRealtimeV1Error({
|
|
650
|
-
code: "socket_error",
|
|
651
|
-
message: "Could not send a realtime client frame.",
|
|
652
|
-
retryable: true,
|
|
653
|
-
terminal: false,
|
|
654
|
-
cause
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
function sendAck(context, appliedCursor) {
|
|
659
|
-
sendJson(context, protocol.schemas.ackFrame.parse({
|
|
660
|
-
type: "ack",
|
|
661
|
-
cursor: appliedCursor
|
|
662
|
-
}));
|
|
663
|
-
}
|
|
664
|
-
function eventDedupeKey(stream, id) {
|
|
665
|
-
return `${stream}:${id}`;
|
|
666
|
-
}
|
|
667
|
-
function rememberEvent(stream, id) {
|
|
668
|
-
const key = eventDedupeKey(stream, id);
|
|
669
|
-
if (appliedEventIds.has(key)) return;
|
|
670
|
-
appliedEventIds.add(key);
|
|
671
|
-
if (appliedEventOrder.length < APPLIED_EVENT_CACHE_MAX) appliedEventOrder.push(key);
|
|
672
|
-
else {
|
|
673
|
-
const oldest = appliedEventOrder[appliedEventCursor];
|
|
674
|
-
if (oldest) appliedEventIds.delete(oldest);
|
|
675
|
-
appliedEventOrder[appliedEventCursor] = key;
|
|
676
|
-
appliedEventCursor = (appliedEventCursor + 1) % APPLIED_EVENT_CACHE_MAX;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
function eventFailureKey(event) {
|
|
680
|
-
return `${event.cursor?.streamId ?? "ephemeral"}:${event.id}:${event.cursor?.sequence ?? "ephemeral"}`;
|
|
681
|
-
}
|
|
682
|
-
function eventForRequiredEffect(event) {
|
|
683
|
-
return REALTIME_EVENT_NAMES.has(event.name) && event.eventVersion === 1 ? event : {
|
|
684
|
-
...event,
|
|
685
|
-
data: null
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
function eventAdvisories(event) {
|
|
689
|
-
if (!REALTIME_EVENT_NAMES.has(event.name)) observe({
|
|
690
|
-
type: "advisory",
|
|
691
|
-
code: "unknown_event",
|
|
692
|
-
event
|
|
693
|
-
});
|
|
694
|
-
else if (event.eventVersion !== 1) observe({
|
|
695
|
-
type: "advisory",
|
|
696
|
-
code: "unsupported_event_version",
|
|
697
|
-
event
|
|
698
|
-
});
|
|
699
|
-
if (event.changes.some((change) => !REALTIME_RESOURCE_TYPES.has(change.type))) observe({
|
|
700
|
-
type: "advisory",
|
|
701
|
-
code: "unknown_resource",
|
|
702
|
-
event
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
async function saveCursor(context, nextCursor) {
|
|
706
|
-
ensureEpoch(context);
|
|
707
|
-
await cursorStore.save(nextCursor);
|
|
708
|
-
ensureEpoch(context);
|
|
709
|
-
}
|
|
710
|
-
async function performResync(context, request) {
|
|
711
|
-
ensureEpoch(context);
|
|
712
|
-
setState("handshaking", "resyncing");
|
|
713
|
-
try {
|
|
714
|
-
await options.resync(request);
|
|
715
|
-
ensureEpoch(context);
|
|
716
|
-
} catch (cause) {
|
|
717
|
-
if (cause instanceof StaleEpochError || !running || current !== context || context.closed) return;
|
|
718
|
-
terminal(new GscdumpRealtimeV1Error({
|
|
719
|
-
code: "resync_failed",
|
|
720
|
-
message: `Realtime resync failed (${request.reason}).`,
|
|
721
|
-
retryable: false,
|
|
722
|
-
terminal: true,
|
|
723
|
-
details: {
|
|
724
|
-
reason: request.reason,
|
|
725
|
-
resumeAfter: request.resumeAfter
|
|
726
|
-
},
|
|
727
|
-
cause
|
|
728
|
-
}), context);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
try {
|
|
732
|
-
await saveCursor(context, request.resumeAfter);
|
|
733
|
-
} catch (cause) {
|
|
734
|
-
if (cause instanceof StaleEpochError || !running || current !== context || context.closed) return;
|
|
735
|
-
terminal(new GscdumpRealtimeV1Error({
|
|
736
|
-
code: "cursor_store_failed",
|
|
737
|
-
message: "Realtime resync completed, but its resume cursor could not be persisted.",
|
|
738
|
-
retryable: false,
|
|
739
|
-
terminal: true,
|
|
740
|
-
details: { resumeAfter: request.resumeAfter },
|
|
741
|
-
cause
|
|
742
|
-
}), context);
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
cursor = { ...request.resumeAfter };
|
|
746
|
-
eventFailures.clear();
|
|
747
|
-
emitState();
|
|
748
|
-
scheduleReconnect(context, "resync-complete", 0, true);
|
|
749
|
-
}
|
|
750
|
-
async function handleEventFailure(context, event, code, cause) {
|
|
751
|
-
if (cause instanceof StaleEpochError) return;
|
|
752
|
-
ensureEpoch(context);
|
|
753
|
-
const key = eventFailureKey(event);
|
|
754
|
-
const failures = (eventFailures.get(key) ?? 0) + 1;
|
|
755
|
-
eventFailures.set(key, failures);
|
|
756
|
-
const error = new GscdumpRealtimeV1Error({
|
|
757
|
-
code,
|
|
758
|
-
message: code === "effect_failed" ? `The required effect rejected for durable event ${event.id}.` : `The cursor store rejected durable event ${event.id}.`,
|
|
759
|
-
retryable: failures < GSCDUMP_REALTIME_ACK_POLICY.effectRetryBeforeUnsafeResync,
|
|
760
|
-
terminal: false,
|
|
761
|
-
details: {
|
|
762
|
-
eventId: event.id,
|
|
763
|
-
sequence: event.cursor?.sequence,
|
|
764
|
-
failures
|
|
765
|
-
},
|
|
766
|
-
cause
|
|
767
|
-
});
|
|
768
|
-
freshness = "degraded";
|
|
769
|
-
reportError(error);
|
|
770
|
-
if (failures < GSCDUMP_REALTIME_ACK_POLICY.effectRetryBeforeUnsafeResync) {
|
|
771
|
-
scheduleReconnect(context, code);
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
const eventCursor = event.cursor;
|
|
775
|
-
if (!eventCursor) return;
|
|
776
|
-
const resumeAfter = context.head ? laterCursor(context.head, eventCursor) : eventCursor;
|
|
777
|
-
await performResync(context, {
|
|
778
|
-
reason: "effect_application_failed",
|
|
779
|
-
source: "client",
|
|
780
|
-
streamId: eventCursor.streamId,
|
|
781
|
-
previousCursor: cursor,
|
|
782
|
-
resumeAfter,
|
|
783
|
-
failedEvent: event,
|
|
784
|
-
cause
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
async function applyDurableEvent(context, event) {
|
|
788
|
-
ensureEpoch(context);
|
|
789
|
-
assertStream(event.cursor, context, "event cursor");
|
|
790
|
-
if (context.replayThrough && compareSequence(event.cursor.sequence, context.replayThrough.sequence) > 0) throw new GscdumpRealtimeV1Error({
|
|
791
|
-
code: "protocol_error",
|
|
792
|
-
message: "A live event arrived before the declared replay completed.",
|
|
793
|
-
retryable: false,
|
|
794
|
-
terminal: true
|
|
795
|
-
});
|
|
796
|
-
if (!context.replaying && (!context.head || compareSequence(event.cursor.sequence, context.head.sequence) > 0)) {
|
|
797
|
-
context.head = { ...event.cursor };
|
|
798
|
-
head = { ...event.cursor };
|
|
799
|
-
}
|
|
800
|
-
if (!cursor) {
|
|
801
|
-
await performResync(context, {
|
|
802
|
-
reason: "initial_cursor_missing",
|
|
803
|
-
source: "client",
|
|
804
|
-
streamId: event.cursor.streamId,
|
|
805
|
-
previousCursor: null,
|
|
806
|
-
resumeAfter: context.head ? laterCursor(context.head, event.cursor) : event.cursor
|
|
807
|
-
});
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
assertStream(cursor, context, "stored cursor");
|
|
811
|
-
if (compareSequence(event.cursor.sequence, cursor.sequence) <= 0) {
|
|
812
|
-
sendAck(context, cursor);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
if (event.cursor.sequence !== nextSequence(cursor.sequence)) {
|
|
816
|
-
const resumeAfter = context.head ? laterCursor(context.head, event.cursor) : event.cursor;
|
|
817
|
-
await performResync(context, {
|
|
818
|
-
reason: "sequence_gap",
|
|
819
|
-
source: "client",
|
|
820
|
-
streamId: event.cursor.streamId,
|
|
821
|
-
previousCursor: cursor,
|
|
822
|
-
resumeAfter
|
|
823
|
-
});
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
setState(context.replaying ? "replaying" : "live", "applying");
|
|
827
|
-
const effectEvent = eventForRequiredEffect(event);
|
|
828
|
-
if (!appliedEventIds.has(eventDedupeKey(event.cursor.streamId, event.id))) try {
|
|
829
|
-
await options.applyEvent(effectEvent);
|
|
830
|
-
ensureEpoch(context);
|
|
831
|
-
} catch (cause) {
|
|
832
|
-
await handleEventFailure(context, event, "effect_failed", cause);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
try {
|
|
836
|
-
await saveCursor(context, event.cursor);
|
|
837
|
-
} catch (cause) {
|
|
838
|
-
await handleEventFailure(context, event, "cursor_store_failed", cause);
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
cursor = { ...event.cursor };
|
|
842
|
-
rememberEvent(event.cursor.streamId, event.id);
|
|
843
|
-
eventFailures.delete(eventFailureKey(event));
|
|
844
|
-
sendAck(context, cursor);
|
|
845
|
-
freshness = context.replaying ? "stale" : "fresh";
|
|
846
|
-
transport = context.replaying ? "replaying" : "live";
|
|
847
|
-
emitState();
|
|
848
|
-
eventAdvisories(effectEvent);
|
|
849
|
-
observe({
|
|
850
|
-
type: "event",
|
|
851
|
-
event: effectEvent,
|
|
852
|
-
cursor: { ...cursor }
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
function scheduleHeartbeat(context) {
|
|
856
|
-
clearTimer(heartbeatTimer);
|
|
857
|
-
heartbeatTimer = runtime.setTimeout(() => {
|
|
858
|
-
heartbeatTimer = void 0;
|
|
859
|
-
if (!running || current !== context || context.closed) return;
|
|
860
|
-
const staleFor = runtime.now() - context.lastPongAt;
|
|
861
|
-
if (staleFor >= GSCDUMP_REALTIME_CONNECTION_POLICY.staleAfterMs) {
|
|
862
|
-
const error = new GscdumpRealtimeV1Error({
|
|
863
|
-
code: "heartbeat_stale",
|
|
864
|
-
message: "Realtime heartbeat became stale.",
|
|
865
|
-
retryable: true,
|
|
866
|
-
terminal: false,
|
|
867
|
-
details: { staleFor }
|
|
868
|
-
});
|
|
869
|
-
freshness = "stale";
|
|
870
|
-
reportError(error);
|
|
871
|
-
scheduleReconnect(context, "heartbeat-stale");
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
try {
|
|
875
|
-
context.socket.send(GSCDUMP_REALTIME_PING);
|
|
876
|
-
} catch (cause) {
|
|
877
|
-
reportError(new GscdumpRealtimeV1Error({
|
|
878
|
-
code: "socket_error",
|
|
879
|
-
message: "Could not send the realtime heartbeat.",
|
|
880
|
-
retryable: true,
|
|
881
|
-
terminal: false,
|
|
882
|
-
cause
|
|
883
|
-
}));
|
|
884
|
-
scheduleReconnect(context, "heartbeat-send");
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
scheduleHeartbeat(context);
|
|
888
|
-
}, GSCDUMP_REALTIME_CONNECTION_POLICY.heartbeatIntervalMs);
|
|
889
|
-
}
|
|
890
|
-
function scheduleRotation(context, expiresAt) {
|
|
891
|
-
clearTimer(rotationTimer);
|
|
892
|
-
const delay = Math.max(0, Date.parse(expiresAt) - runtime.now() - GSCDUMP_REALTIME_CONNECTION_POLICY.connectionRefreshBeforeExpiryMs);
|
|
893
|
-
rotationTimer = runtime.setTimeout(() => {
|
|
894
|
-
rotationTimer = void 0;
|
|
895
|
-
if (running && current === context && !context.closed) scheduleReconnect(context, "connection-expiry-rotation", 0, true);
|
|
896
|
-
}, delay);
|
|
897
|
-
}
|
|
898
|
-
async function handleReady(context, frame) {
|
|
899
|
-
ensureEpoch(context);
|
|
900
|
-
if (context.ready) throw new GscdumpRealtimeV1Error({
|
|
901
|
-
code: "protocol_error",
|
|
902
|
-
message: "The server sent more than one ready frame.",
|
|
903
|
-
retryable: false,
|
|
904
|
-
terminal: true
|
|
905
|
-
});
|
|
906
|
-
assertStream(frame, context, "ready frame");
|
|
907
|
-
assertStream(frame.head, context, "ready head");
|
|
908
|
-
assertStream(frame.replayFloor, context, "ready replay floor");
|
|
909
|
-
if (compareSequence(frame.head.sequence, context.ticket.head.sequence) < 0) throw new GscdumpRealtimeV1Error({
|
|
910
|
-
code: "protocol_error",
|
|
911
|
-
message: "The ready head regressed behind the ticket head.",
|
|
912
|
-
retryable: false,
|
|
913
|
-
terminal: true,
|
|
914
|
-
details: {
|
|
915
|
-
ticketHead: context.ticket.head,
|
|
916
|
-
readyHead: frame.head
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
|
-
const expiresAt = Date.parse(frame.expiresAt);
|
|
920
|
-
if (!Number.isFinite(expiresAt) || expiresAt <= runtime.now() || expiresAt - runtime.now() > context.ticket.maxConnectionSeconds * 1e3) throw new GscdumpRealtimeV1Error({
|
|
921
|
-
code: "protocol_error",
|
|
922
|
-
message: "The ready frame carried an invalid signed connection expiry.",
|
|
923
|
-
retryable: false,
|
|
924
|
-
terminal: true,
|
|
925
|
-
details: { expiresAt: frame.expiresAt }
|
|
926
|
-
});
|
|
927
|
-
context.ready = true;
|
|
928
|
-
context.head = { ...frame.head };
|
|
929
|
-
context.lastPongAt = runtime.now();
|
|
930
|
-
head = { ...frame.head };
|
|
931
|
-
streamId = frame.streamId;
|
|
932
|
-
attempt = 0;
|
|
933
|
-
lastError = null;
|
|
934
|
-
scheduleHeartbeat(context);
|
|
935
|
-
scheduleRotation(context, frame.expiresAt);
|
|
936
|
-
if (!cursor) {
|
|
937
|
-
await performResync(context, {
|
|
938
|
-
reason: "initial_cursor_missing",
|
|
939
|
-
source: "client",
|
|
940
|
-
streamId: frame.streamId,
|
|
941
|
-
previousCursor: null,
|
|
942
|
-
resumeAfter: frame.head
|
|
943
|
-
});
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
if (cursor.streamId !== frame.streamId) {
|
|
947
|
-
await performResync(context, {
|
|
948
|
-
reason: "stream_mismatch",
|
|
949
|
-
source: "client",
|
|
950
|
-
streamId: frame.streamId,
|
|
951
|
-
previousCursor: cursor,
|
|
952
|
-
resumeAfter: frame.head
|
|
953
|
-
});
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
if (compareSequence(cursor.sequence, frame.head.sequence) > 0) {
|
|
957
|
-
await performResync(context, {
|
|
958
|
-
reason: "sequence_gap",
|
|
959
|
-
source: "client",
|
|
960
|
-
streamId: frame.streamId,
|
|
961
|
-
previousCursor: cursor,
|
|
962
|
-
resumeAfter: frame.head
|
|
963
|
-
});
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
if (compareSequence(cursor.sequence, frame.replayFloor.sequence) < 0) {
|
|
967
|
-
await performResync(context, {
|
|
968
|
-
reason: "cursor_expired",
|
|
969
|
-
source: "client",
|
|
970
|
-
streamId: frame.streamId,
|
|
971
|
-
previousCursor: cursor,
|
|
972
|
-
resumeAfter: frame.head
|
|
973
|
-
});
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (sameCursor(cursor, frame.head)) {
|
|
977
|
-
context.replaying = false;
|
|
978
|
-
setState("live", "fresh");
|
|
979
|
-
} else {
|
|
980
|
-
context.replaying = true;
|
|
981
|
-
setState("replaying", "stale");
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
async function processFrame(context, frame) {
|
|
985
|
-
ensureEpoch(context);
|
|
986
|
-
if (frame.type === "ready") {
|
|
987
|
-
await handleReady(context, frame);
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
if (frame.type === "error") {
|
|
991
|
-
const retryable = !(frame.error.code === "invalid_frame" || frame.error.code === "protocol_mismatch" || frame.error.code === "policy_violation") && frame.error.retryable;
|
|
992
|
-
const error = new GscdumpRealtimeV1Error({
|
|
993
|
-
code: frame.error.code === "internal_error" || frame.error.code === "overloaded" ? "socket_error" : "protocol_error",
|
|
994
|
-
message: frame.error.message,
|
|
995
|
-
retryable,
|
|
996
|
-
terminal: !retryable,
|
|
997
|
-
details: {
|
|
998
|
-
serverCode: frame.error.code,
|
|
999
|
-
retryAfter: frame.error.retryAfter
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
reportError(error);
|
|
1003
|
-
if (retryable) {
|
|
1004
|
-
context.retryAfterMs = frame.error.retryAfter === void 0 ? void 0 : frame.error.retryAfter * 1e3;
|
|
1005
|
-
scheduleReconnect(context, "server-error", context.retryAfterMs);
|
|
1006
|
-
} else terminal(error, context);
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
if (!context.ready) throw new GscdumpRealtimeV1Error({
|
|
1010
|
-
code: "protocol_error",
|
|
1011
|
-
message: `The server sent ${frame.type} before ready.`,
|
|
1012
|
-
retryable: false,
|
|
1013
|
-
terminal: true
|
|
1014
|
-
});
|
|
1015
|
-
if (frame.type === "replay.begin") {
|
|
1016
|
-
assertStream(frame.through, context, "replay through cursor");
|
|
1017
|
-
if (frame.after) assertStream(frame.after, context, "replay after cursor");
|
|
1018
|
-
if (context.replayThrough) throw new GscdumpRealtimeV1Error({
|
|
1019
|
-
code: "protocol_error",
|
|
1020
|
-
message: "The server sent a nested replay.begin frame.",
|
|
1021
|
-
retryable: false,
|
|
1022
|
-
terminal: true
|
|
1023
|
-
});
|
|
1024
|
-
if (!sameCursor(frame.after, cursor)) {
|
|
1025
|
-
await performResync(context, {
|
|
1026
|
-
reason: "sequence_gap",
|
|
1027
|
-
source: "client",
|
|
1028
|
-
streamId: context.ticket.head.streamId,
|
|
1029
|
-
previousCursor: cursor,
|
|
1030
|
-
resumeAfter: frame.through
|
|
1031
|
-
});
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
if (!context.head || !sameCursor(frame.through, context.head)) throw new GscdumpRealtimeV1Error({
|
|
1035
|
-
code: "protocol_error",
|
|
1036
|
-
message: "The replay boundary did not match the ready head.",
|
|
1037
|
-
retryable: false,
|
|
1038
|
-
terminal: true
|
|
1039
|
-
});
|
|
1040
|
-
context.replaying = true;
|
|
1041
|
-
context.replayThrough = { ...frame.through };
|
|
1042
|
-
setState("replaying", "stale");
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
if (frame.type === "replay.end") {
|
|
1046
|
-
assertStream(frame.through, context, "replay end cursor");
|
|
1047
|
-
if (!context.replaying || !context.replayThrough || !sameCursor(context.replayThrough, frame.through) || !sameCursor(cursor, frame.through)) {
|
|
1048
|
-
await performResync(context, {
|
|
1049
|
-
reason: "sequence_gap",
|
|
1050
|
-
source: "client",
|
|
1051
|
-
streamId: frame.through.streamId,
|
|
1052
|
-
previousCursor: cursor,
|
|
1053
|
-
resumeAfter: context.head ? laterCursor(context.head, frame.through) : frame.through
|
|
1054
|
-
});
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
context.replaying = false;
|
|
1058
|
-
context.replayThrough = null;
|
|
1059
|
-
setState("live", "fresh");
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
if (frame.type === "resync.required") {
|
|
1063
|
-
assertStream(frame.scope, context, "resync scope");
|
|
1064
|
-
assertStream(frame.resumeAfter, context, "resync cursor");
|
|
1065
|
-
const minimumSequence = context.head && cursor ? laterCursor(context.head, cursor).sequence : context.head?.sequence ?? cursor?.sequence;
|
|
1066
|
-
if (minimumSequence !== void 0 && compareSequence(frame.resumeAfter.sequence, minimumSequence) < 0) throw new GscdumpRealtimeV1Error({
|
|
1067
|
-
code: "protocol_error",
|
|
1068
|
-
message: "The resync cursor regressed behind observed stream state.",
|
|
1069
|
-
retryable: false,
|
|
1070
|
-
terminal: true,
|
|
1071
|
-
details: {
|
|
1072
|
-
minimumSequence,
|
|
1073
|
-
resumeAfter: frame.resumeAfter
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1076
|
-
await performResync(context, {
|
|
1077
|
-
reason: frame.reason,
|
|
1078
|
-
source: "server",
|
|
1079
|
-
streamId: frame.scope.streamId,
|
|
1080
|
-
previousCursor: cursor,
|
|
1081
|
-
resumeAfter: frame.resumeAfter
|
|
1082
|
-
});
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
if (frame.type === "batch") {
|
|
1086
|
-
if (context.replaying && !context.replayThrough) throw new GscdumpRealtimeV1Error({
|
|
1087
|
-
code: "protocol_error",
|
|
1088
|
-
message: "A replay batch arrived before replay.begin.",
|
|
1089
|
-
retryable: false,
|
|
1090
|
-
terminal: true
|
|
1091
|
-
});
|
|
1092
|
-
for (const event of frame.events) {
|
|
1093
|
-
ensureEpoch(context);
|
|
1094
|
-
await applyDurableEvent(context, event);
|
|
1095
|
-
}
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
if (frame.type === "event") {
|
|
1099
|
-
if (frame.delivery === "ephemeral") {
|
|
1100
|
-
const observedEvent = eventForRequiredEffect(frame);
|
|
1101
|
-
eventAdvisories(observedEvent);
|
|
1102
|
-
observe({
|
|
1103
|
-
type: "event",
|
|
1104
|
-
event: observedEvent,
|
|
1105
|
-
cursor: cursor ? { ...cursor } : null
|
|
1106
|
-
});
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
if (context.replaying && !context.replayThrough) throw new GscdumpRealtimeV1Error({
|
|
1110
|
-
code: "protocol_error",
|
|
1111
|
-
message: "A durable replay event arrived before replay.begin.",
|
|
1112
|
-
retryable: false,
|
|
1113
|
-
terminal: true
|
|
1114
|
-
});
|
|
1115
|
-
await applyDurableEvent(context, frame);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
async function processRawMessage(context, raw) {
|
|
1119
|
-
ensureEpoch(context);
|
|
1120
|
-
const text = await messageText(raw);
|
|
1121
|
-
ensureEpoch(context);
|
|
1122
|
-
if (text === GSCDUMP_REALTIME_PONG) {
|
|
1123
|
-
context.lastPongAt = runtime.now();
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
const textBytes = utf8Size(text);
|
|
1127
|
-
if (textBytes > GSCDUMP_REALTIME_LIMITS.outboundFrameMaxBytes) throw new GscdumpRealtimeV1Error({
|
|
1128
|
-
code: "protocol_error",
|
|
1129
|
-
message: "A server frame exceeded the v1 outbound frame limit.",
|
|
1130
|
-
retryable: false,
|
|
1131
|
-
terminal: true
|
|
1132
|
-
});
|
|
1133
|
-
let rawFrame;
|
|
1134
|
-
try {
|
|
1135
|
-
rawFrame = JSON.parse(text);
|
|
1136
|
-
} catch (cause) {
|
|
1137
|
-
throw new GscdumpRealtimeV1Error({
|
|
1138
|
-
code: "protocol_error",
|
|
1139
|
-
message: "The server sent malformed realtime JSON.",
|
|
1140
|
-
retryable: false,
|
|
1141
|
-
terminal: true,
|
|
1142
|
-
cause
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
const parsed = protocol.schemas.serverFrame.safeParse(rawFrame);
|
|
1146
|
-
if (!parsed.success) throw new GscdumpRealtimeV1Error({
|
|
1147
|
-
code: "protocol_error",
|
|
1148
|
-
message: "The server frame did not match the v1 protocol.",
|
|
1149
|
-
retryable: false,
|
|
1150
|
-
terminal: true,
|
|
1151
|
-
details: parseErrorDetails(parsed.error),
|
|
1152
|
-
cause: parsed.error
|
|
1153
|
-
});
|
|
1154
|
-
if (parsed.data.type === "event") {
|
|
1155
|
-
if (textBytes > (parsed.data.delivery === "durable" ? GSCDUMP_REALTIME_LIMITS.durableEventMaxBytes : GSCDUMP_REALTIME_LIMITS.ephemeralEventMaxBytes)) throw new GscdumpRealtimeV1Error({
|
|
1156
|
-
code: "protocol_error",
|
|
1157
|
-
message: `A ${parsed.data.delivery} event exceeded its v1 size limit.`,
|
|
1158
|
-
retryable: false,
|
|
1159
|
-
terminal: true
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
if (parsed.data.type === "batch") {
|
|
1163
|
-
for (const event of parsed.data.events) if (utf8Size(JSON.stringify(event)) > GSCDUMP_REALTIME_LIMITS.durableEventMaxBytes) throw new GscdumpRealtimeV1Error({
|
|
1164
|
-
code: "protocol_error",
|
|
1165
|
-
message: "A durable event inside a batch exceeded its v1 size limit.",
|
|
1166
|
-
retryable: false,
|
|
1167
|
-
terminal: true
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
await processFrame(context, parsed.data);
|
|
1171
|
-
}
|
|
1172
|
-
function handleFrameQueueError(context, cause) {
|
|
1173
|
-
if (cause instanceof StaleEpochError || !running || current !== context) return;
|
|
1174
|
-
const error = cause instanceof GscdumpRealtimeV1Error ? cause : new GscdumpRealtimeV1Error({
|
|
1175
|
-
code: "protocol_error",
|
|
1176
|
-
message: "Realtime frame processing failed.",
|
|
1177
|
-
retryable: false,
|
|
1178
|
-
terminal: true,
|
|
1179
|
-
cause
|
|
1180
|
-
});
|
|
1181
|
-
if (error.terminal) {
|
|
1182
|
-
terminal(error, context);
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
reportError(error);
|
|
1186
|
-
scheduleReconnect(context, error.code);
|
|
1187
|
-
}
|
|
1188
|
-
async function loadCursor(ticket) {
|
|
1189
|
-
let stored;
|
|
1190
|
-
try {
|
|
1191
|
-
stored = await cursorStore.load();
|
|
1192
|
-
} catch (cause) {
|
|
1193
|
-
throw new GscdumpRealtimeV1Error({
|
|
1194
|
-
code: "cursor_store_failed",
|
|
1195
|
-
message: "Could not load the realtime cursor.",
|
|
1196
|
-
retryable: false,
|
|
1197
|
-
terminal: true,
|
|
1198
|
-
cause
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
if (stored === null) return null;
|
|
1202
|
-
const parsed = protocol.schemas.cursor.safeParse(stored);
|
|
1203
|
-
if (!parsed.success) {
|
|
1204
|
-
reportError(new GscdumpRealtimeV1Error({
|
|
1205
|
-
code: "cursor_store_failed",
|
|
1206
|
-
message: "The stored realtime cursor was invalid; a full resync is required.",
|
|
1207
|
-
retryable: true,
|
|
1208
|
-
terminal: false,
|
|
1209
|
-
details: parseErrorDetails(parsed.error),
|
|
1210
|
-
cause: parsed.error
|
|
1211
|
-
}));
|
|
1212
|
-
return null;
|
|
1213
|
-
}
|
|
1214
|
-
if (parsed.data.streamId !== ticket.head.streamId) return null;
|
|
1215
|
-
return parsed.data;
|
|
1216
|
-
}
|
|
1217
|
-
async function provideFreshTicket() {
|
|
1218
|
-
let raw;
|
|
1219
|
-
try {
|
|
1220
|
-
raw = await options.ticketProvider();
|
|
1221
|
-
} catch (cause) {
|
|
1222
|
-
throw new GscdumpRealtimeV1Error({
|
|
1223
|
-
code: "ticket_provider_failed",
|
|
1224
|
-
message: "The realtime ticket provider rejected.",
|
|
1225
|
-
retryable: true,
|
|
1226
|
-
terminal: false,
|
|
1227
|
-
cause
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
const parsed = protocol.schemas.ticketResponse.client.safeParse(raw);
|
|
1231
|
-
if (!parsed.success) throw new GscdumpRealtimeV1Error({
|
|
1232
|
-
code: "ticket_invalid",
|
|
1233
|
-
message: "The realtime ticket response did not match the v1 contract.",
|
|
1234
|
-
retryable: false,
|
|
1235
|
-
terminal: true,
|
|
1236
|
-
details: parseErrorDetails(parsed.error),
|
|
1237
|
-
cause: parsed.error
|
|
1238
|
-
});
|
|
1239
|
-
if (Date.parse(parsed.data.data.expiresAt) <= runtime.now()) throw new GscdumpRealtimeV1Error({
|
|
1240
|
-
code: "ticket_invalid",
|
|
1241
|
-
message: "The realtime ticket had already expired locally.",
|
|
1242
|
-
retryable: true,
|
|
1243
|
-
terminal: false,
|
|
1244
|
-
details: { localExpiry: true }
|
|
1245
|
-
});
|
|
1246
|
-
return parsed.data.data;
|
|
1247
|
-
}
|
|
1248
|
-
async function beginAttempt() {
|
|
1249
|
-
if (!running) return;
|
|
1250
|
-
const attemptEpoch = ++epoch;
|
|
1251
|
-
setState("ticketing", freshness === "fresh" ? "stale" : freshness);
|
|
1252
|
-
let ticket;
|
|
1253
|
-
try {
|
|
1254
|
-
ticket = await provideFreshTicket();
|
|
1255
|
-
} catch (cause) {
|
|
1256
|
-
if (!running || epoch !== attemptEpoch) return;
|
|
1257
|
-
const error = cause instanceof GscdumpRealtimeV1Error ? cause : new GscdumpRealtimeV1Error({
|
|
1258
|
-
code: "ticket_provider_failed",
|
|
1259
|
-
message: "Could not obtain a realtime ticket.",
|
|
1260
|
-
retryable: true,
|
|
1261
|
-
terminal: false,
|
|
1262
|
-
cause
|
|
1263
|
-
});
|
|
1264
|
-
if (error.code === "ticket_invalid" && error.details.localExpiry === true && immediateExpiredTicketRetry) {
|
|
1265
|
-
immediateExpiredTicketRetry = false;
|
|
1266
|
-
beginAttempt();
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
reportError(error);
|
|
1270
|
-
if (error.terminal) terminal(error, null);
|
|
1271
|
-
else scheduleReconnect(null, error.code);
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
immediateExpiredTicketRetry = true;
|
|
1275
|
-
if (!running || epoch !== attemptEpoch) return;
|
|
1276
|
-
try {
|
|
1277
|
-
cursor = await loadCursor(ticket);
|
|
1278
|
-
} catch (cause) {
|
|
1279
|
-
if (!running || epoch !== attemptEpoch) return;
|
|
1280
|
-
terminal(cause, null);
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
if (!running || epoch !== attemptEpoch) return;
|
|
1284
|
-
streamId = ticket.head.streamId;
|
|
1285
|
-
head = { ...ticket.head };
|
|
1286
|
-
emitState();
|
|
1287
|
-
let socket;
|
|
1288
|
-
try {
|
|
1289
|
-
socket = runtime.createSocket(ticket.socketUrl, [ticket.protocol, ticket.ticket]);
|
|
1290
|
-
} catch (cause) {
|
|
1291
|
-
terminal(cause instanceof GscdumpRealtimeV1Error ? cause : new GscdumpRealtimeV1Error({
|
|
1292
|
-
code: "runtime_unavailable",
|
|
1293
|
-
message: "Could not create the realtime WebSocket.",
|
|
1294
|
-
retryable: false,
|
|
1295
|
-
terminal: true,
|
|
1296
|
-
cause
|
|
1297
|
-
}), null);
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
const context = {
|
|
1301
|
-
epoch: attemptEpoch,
|
|
1302
|
-
socket,
|
|
1303
|
-
ticket,
|
|
1304
|
-
accepted: false,
|
|
1305
|
-
ready: false,
|
|
1306
|
-
closed: false,
|
|
1307
|
-
replaying: false,
|
|
1308
|
-
replayThrough: null,
|
|
1309
|
-
lastPongAt: runtime.now(),
|
|
1310
|
-
head: null,
|
|
1311
|
-
retryAfterMs: void 0,
|
|
1312
|
-
frameQueue: Promise.resolve()
|
|
1313
|
-
};
|
|
1314
|
-
current = context;
|
|
1315
|
-
setState("connecting", freshness);
|
|
1316
|
-
socket.onopen = () => {
|
|
1317
|
-
if (!running || current !== context || context.closed) return;
|
|
1318
|
-
context.accepted = true;
|
|
1319
|
-
if (socket.protocol !== GSCDUMP_REALTIME_SUBPROTOCOL) {
|
|
1320
|
-
terminal(new GscdumpRealtimeV1Error({
|
|
1321
|
-
code: "protocol_error",
|
|
1322
|
-
message: "The WebSocket did not select gscdump.v1.",
|
|
1323
|
-
retryable: false,
|
|
1324
|
-
terminal: true,
|
|
1325
|
-
details: { selectedProtocol: socket.protocol }
|
|
1326
|
-
}), context);
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
setState("handshaking", cursor ? "stale" : "unknown");
|
|
1330
|
-
try {
|
|
1331
|
-
const hello = protocol.schemas.helloFrame.parse({
|
|
1332
|
-
type: "hello",
|
|
1333
|
-
protocolVersion: protocol.constants.realtimeProtocolVersion,
|
|
1334
|
-
sdkVersion,
|
|
1335
|
-
resume: cursor
|
|
1336
|
-
});
|
|
1337
|
-
sendJson(context, hello);
|
|
1338
|
-
} catch (cause) {
|
|
1339
|
-
handleFrameQueueError(context, cause);
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
socket.onmessage = (event) => {
|
|
1343
|
-
if (!running || current !== context || context.closed) return;
|
|
1344
|
-
const work = context.frameQueue.then(() => processRawMessage(context, event.data));
|
|
1345
|
-
context.frameQueue = work.catch((cause) => {
|
|
1346
|
-
handleFrameQueueError(context, cause);
|
|
1347
|
-
});
|
|
1348
|
-
};
|
|
1349
|
-
socket.onerror = (cause) => {
|
|
1350
|
-
if (!running || current !== context || context.closed) return;
|
|
1351
|
-
const error = new GscdumpRealtimeV1Error({
|
|
1352
|
-
code: context.accepted ? "socket_error" : "upgrade_rejected",
|
|
1353
|
-
message: context.accepted ? "The accepted realtime socket emitted an opaque error." : "The realtime WebSocket upgrade was rejected or failed opaquely.",
|
|
1354
|
-
retryable: true,
|
|
1355
|
-
terminal: false,
|
|
1356
|
-
cause
|
|
1357
|
-
});
|
|
1358
|
-
reportError(error);
|
|
1359
|
-
scheduleReconnect(context, error.code);
|
|
1360
|
-
};
|
|
1361
|
-
socket.onclose = (event) => {
|
|
1362
|
-
if (context.closed || current !== context) return;
|
|
1363
|
-
current = null;
|
|
1364
|
-
epoch++;
|
|
1365
|
-
clearConnectionTimers();
|
|
1366
|
-
const code = event.code ?? 1006;
|
|
1367
|
-
if (!running) return;
|
|
1368
|
-
if (!context.accepted) {
|
|
1369
|
-
reportError(new GscdumpRealtimeV1Error({
|
|
1370
|
-
code: "upgrade_rejected",
|
|
1371
|
-
message: "The realtime WebSocket upgrade failed opaquely.",
|
|
1372
|
-
retryable: true,
|
|
1373
|
-
terminal: false,
|
|
1374
|
-
details: { closeCode: code }
|
|
1375
|
-
}));
|
|
1376
|
-
scheduleReconnect(null, "upgrade-rejected");
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
if (code === GSCDUMP_REALTIME_CLOSE_CODES.normal) {
|
|
1380
|
-
running = false;
|
|
1381
|
-
setState("stopped", "unknown");
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
if (code === GSCDUMP_REALTIME_CLOSE_CODES.protocolError || code === GSCDUMP_REALTIME_CLOSE_CODES.policyViolation) {
|
|
1385
|
-
terminal(new GscdumpRealtimeV1Error({
|
|
1386
|
-
code: "protocol_error",
|
|
1387
|
-
message: `The realtime server closed with terminal code ${code}.`,
|
|
1388
|
-
retryable: false,
|
|
1389
|
-
terminal: true,
|
|
1390
|
-
details: {
|
|
1391
|
-
closeCode: code,
|
|
1392
|
-
reason: event.reason
|
|
1393
|
-
}
|
|
1394
|
-
}), null);
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
if (code === GSCDUMP_REALTIME_CLOSE_CODES.maximumLifetime) {
|
|
1398
|
-
scheduleReconnect(null, "maximum-lifetime", 0, true);
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
if (code === 1006 || code === GSCDUMP_REALTIME_CLOSE_CODES.internalError || code === GSCDUMP_REALTIME_CLOSE_CODES.serviceRestart || code === GSCDUMP_REALTIME_CLOSE_CODES.overloadedOrAckLag) {
|
|
1402
|
-
scheduleReconnect(null, `close-${code}`, context.retryAfterMs);
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
terminal(new GscdumpRealtimeV1Error({
|
|
1406
|
-
code: "protocol_error",
|
|
1407
|
-
message: `The realtime server used an unsupported close code ${code}.`,
|
|
1408
|
-
retryable: false,
|
|
1409
|
-
terminal: true,
|
|
1410
|
-
details: {
|
|
1411
|
-
closeCode: code,
|
|
1412
|
-
reason: event.reason
|
|
1413
|
-
}
|
|
1414
|
-
}), null);
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
async function start() {
|
|
1418
|
-
if (running) return;
|
|
1419
|
-
running = true;
|
|
1420
|
-
attempt = 0;
|
|
1421
|
-
lastError = null;
|
|
1422
|
-
immediateExpiredTicketRetry = true;
|
|
1423
|
-
freshness = cursor ? "stale" : "unknown";
|
|
1424
|
-
await beginAttempt();
|
|
1425
|
-
}
|
|
1426
|
-
function stop() {
|
|
1427
|
-
running = false;
|
|
1428
|
-
clearTimer(reconnectTimer);
|
|
1429
|
-
reconnectTimer = void 0;
|
|
1430
|
-
if (current) closeSocket(current, GSCDUMP_REALTIME_CLOSE_CODES.normal, "manual");
|
|
1431
|
-
else epoch++;
|
|
1432
|
-
setState("stopped", "unknown");
|
|
1433
|
-
}
|
|
1434
|
-
return {
|
|
1435
|
-
start,
|
|
1436
|
-
stop,
|
|
1437
|
-
getSnapshot
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1
|
+
import { GscdumpV1Error, createGscdumpV1Client, isGscdumpV1Error } from "./http.mjs";
|
|
2
|
+
import { GSCDUMP_REALTIME_V1_SDK_VERSION, GscdumpRealtimeV1Error, createGscdumpRealtimeV1Client } from "./realtime.mjs";
|
|
1440
3
|
export { GSCDUMP_REALTIME_V1_SDK_VERSION, GscdumpRealtimeV1Error, GscdumpV1Error, createGscdumpRealtimeV1Client, createGscdumpV1Client, isGscdumpV1Error };
|