@reopt-ai/data-contract 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunkHRKELGGHcjs = require('./chunk-HRKELGGH.cjs');
4
+
5
+
6
+
7
+
8
+
9
+ var _chunkDHSMXB54cjs = require('./chunk-DHSMXB54.cjs');
10
+
11
+
12
+
13
+
14
+
15
+
16
+ var _chunkZQVRSCTWcjs = require('./chunk-ZQVRSCTW.cjs');
17
+
18
+ // src/client.ts
19
+ var TRACK_PATH = "/api/track";
20
+ var DEFAULTS = {
21
+ timeoutMs: 1e4,
22
+ maxRetries: 2,
23
+ baseDelayMs: 500,
24
+ maxDelayMs: 2e4
25
+ };
26
+ function normalizeBaseUrl(baseUrl) {
27
+ return baseUrl.replace(/\/+$/, "");
28
+ }
29
+ function parseRetryAfter(header) {
30
+ if (!header) return void 0;
31
+ const seconds = Number(header);
32
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
33
+ const at = Date.parse(header);
34
+ if (Number.isFinite(at)) return Math.max(0, at - Date.now());
35
+ return void 0;
36
+ }
37
+ function sleep(ms, signal) {
38
+ return new Promise((resolve, reject) => {
39
+ if (_optionalChain([signal, 'optionalAccess', _ => _.aborted])) {
40
+ reject(_nullishCoalesce(signal.reason, () => ( new Error("Aborted"))));
41
+ return;
42
+ }
43
+ const timer = setTimeout(() => {
44
+ _optionalChain([signal, 'optionalAccess', _2 => _2.removeEventListener, 'call', _3 => _3("abort", onAbort)]);
45
+ resolve();
46
+ }, ms);
47
+ const onAbort = () => {
48
+ clearTimeout(timer);
49
+ reject(_nullishCoalesce(_optionalChain([signal, 'optionalAccess', _4 => _4.reason]), () => ( new Error("Aborted"))));
50
+ };
51
+ _optionalChain([signal, 'optionalAccess', _5 => _5.addEventListener, 'call', _6 => _6("abort", onAbort, { once: true })]);
52
+ });
53
+ }
54
+ function backoffDelay(attempt, retry) {
55
+ const exponential = retry.baseDelayMs * 2 ** attempt;
56
+ const capped = Math.min(exponential, retry.maxDelayMs);
57
+ return Math.round(capped * (0.5 + Math.random() * 0.5));
58
+ }
59
+ function toApiError(status, rawBody, retryAfterMs, requestId) {
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(rawBody);
63
+ } catch (e) {
64
+ return new (0, _chunkZQVRSCTWcjs.DataApiError)({
65
+ message: `reopt-data responded ${status} with a non-JSON body`,
66
+ status,
67
+ code: status >= 500 ? "internal_error" : "unknown_error",
68
+ requestId,
69
+ retryAfterMs,
70
+ details: rawBody.slice(0, 2e3)
71
+ });
72
+ }
73
+ const body = _nullishCoalesce(parsed, () => ( {}));
74
+ const code = typeof body.code === "string" ? body.code : status >= 500 ? "internal_error" : "unknown_error";
75
+ const message = typeof body.message === "string" && body.message || typeof body.error === "string" && body.error || `reopt-data responded ${status}`;
76
+ return new (0, _chunkZQVRSCTWcjs.DataApiError)({
77
+ message,
78
+ status,
79
+ code,
80
+ requestId: _nullishCoalesce((typeof body.requestId === "string" ? body.requestId : void 0), () => ( requestId)),
81
+ retryAfterMs,
82
+ details: _nullishCoalesce(body.errors, () => ( void 0))
83
+ });
84
+ }
85
+ function createDataClient(options) {
86
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
87
+ const doFetch = _nullishCoalesce(options.fetch, () => ( globalThis.fetch));
88
+ if (typeof doFetch !== "function") {
89
+ throw new TypeError("createDataClient: no fetch implementation available \u2014 pass options.fetch");
90
+ }
91
+ const timeoutMs = _nullishCoalesce(options.timeoutMs, () => ( DEFAULTS.timeoutMs));
92
+ const retry = {
93
+ maxRetries: _nullishCoalesce(_optionalChain([options, 'access', _7 => _7.retry, 'optionalAccess', _8 => _8.maxRetries]), () => ( DEFAULTS.maxRetries)),
94
+ baseDelayMs: _nullishCoalesce(_optionalChain([options, 'access', _9 => _9.retry, 'optionalAccess', _10 => _10.baseDelayMs]), () => ( DEFAULTS.baseDelayMs)),
95
+ maxDelayMs: _nullishCoalesce(_optionalChain([options, 'access', _11 => _11.retry, 'optionalAccess', _12 => _12.maxDelayMs]), () => ( DEFAULTS.maxDelayMs)),
96
+ respectRetryAfter: _nullishCoalesce(_optionalChain([options, 'access', _13 => _13.retry, 'optionalAccess', _14 => _14.respectRetryAfter]), () => ( true))
97
+ };
98
+ const userAgent = options.userAgent ? `reopt-data-contract/${_chunkZQVRSCTWcjs.CONTRACT_VERSION} ${options.userAgent}` : `reopt-data-contract/${_chunkZQVRSCTWcjs.CONTRACT_VERSION}`;
99
+ async function postOnce(path, body, signal) {
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(
102
+ () => controller.abort(new Error(`reopt-data request timed out after ${timeoutMs}ms`)),
103
+ timeoutMs
104
+ );
105
+ const onCallerAbort = () => controller.abort(_nullishCoalesce(_optionalChain([signal, 'optionalAccess', _15 => _15.reason]), () => ( new Error("Aborted"))));
106
+ _optionalChain([signal, 'optionalAccess', _16 => _16.addEventListener, 'call', _17 => _17("abort", onCallerAbort, { once: true })]);
107
+ try {
108
+ return await doFetch(`${baseUrl}${path}`, {
109
+ method: "POST",
110
+ headers: {
111
+ "content-type": "application/json",
112
+ "user-agent": userAgent,
113
+ [_chunkZQVRSCTWcjs.CLIENT_ID_HEADER]: options.clientId,
114
+ [_chunkZQVRSCTWcjs.CLIENT_SECRET_HEADER]: options.clientSecret
115
+ },
116
+ body: JSON.stringify(body),
117
+ signal: controller.signal
118
+ });
119
+ } finally {
120
+ clearTimeout(timer);
121
+ _optionalChain([signal, 'optionalAccess', _18 => _18.removeEventListener, 'call', _19 => _19("abort", onCallerAbort)]);
122
+ }
123
+ }
124
+ async function post(path, body, parse, signal) {
125
+ let lastError;
126
+ for (let attempt = 0; attempt <= retry.maxRetries; attempt++) {
127
+ let response;
128
+ try {
129
+ response = await postOnce(path, body, signal);
130
+ } catch (cause) {
131
+ if (_optionalChain([signal, 'optionalAccess', _20 => _20.aborted])) throw cause;
132
+ lastError = new (0, _chunkZQVRSCTWcjs.DataApiError)({
133
+ message: cause instanceof Error ? cause.message : String(cause),
134
+ status: 0,
135
+ code: "network_error",
136
+ details: cause
137
+ });
138
+ if (attempt < retry.maxRetries) {
139
+ await sleep(backoffDelay(attempt, retry), signal);
140
+ continue;
141
+ }
142
+ throw lastError;
143
+ }
144
+ const requestId = _nullishCoalesce(response.headers.get(_chunkZQVRSCTWcjs.REQUEST_ID_HEADER), () => ( void 0));
145
+ const rawBody = await response.text();
146
+ if (response.ok) {
147
+ let json;
148
+ try {
149
+ json = JSON.parse(rawBody);
150
+ } catch (cause) {
151
+ throw new (0, _chunkZQVRSCTWcjs.DataApiError)({
152
+ message: "reopt-data returned a 2xx with an unparseable body",
153
+ status: response.status,
154
+ code: "contract_mismatch",
155
+ requestId,
156
+ details: cause
157
+ });
158
+ }
159
+ try {
160
+ return parse(json);
161
+ } catch (cause) {
162
+ throw new (0, _chunkZQVRSCTWcjs.DataApiError)({
163
+ message: "reopt-data returned a 2xx that does not match the contract",
164
+ status: response.status,
165
+ code: "contract_mismatch",
166
+ requestId,
167
+ details: cause
168
+ });
169
+ }
170
+ }
171
+ const retryAfterMs = retry.respectRetryAfter ? parseRetryAfter(response.headers.get("retry-after")) : void 0;
172
+ lastError = toApiError(response.status, rawBody, retryAfterMs, requestId);
173
+ if (!lastError.retryable || attempt === retry.maxRetries) throw lastError;
174
+ await sleep(_nullishCoalesce(retryAfterMs, () => ( backoffDelay(attempt, retry))), signal);
175
+ }
176
+ throw _nullishCoalesce(lastError, () => ( new (0, _chunkZQVRSCTWcjs.DataApiError)({ message: "reopt-data request failed", status: 0, code: "network_error" })));
177
+ }
178
+ return {
179
+ ingest: {
180
+ track(events, init) {
181
+ return post(TRACK_PATH, events, (value) => _chunkHRKELGGHcjs.zIngestResponse.parse(value), _optionalChain([init, 'optionalAccess', _21 => _21.signal]));
182
+ }
183
+ },
184
+ query: {
185
+ eventsTimeseries(input, init) {
186
+ return post(
187
+ _chunkDHSMXB54cjs.QUERY_API_PATHS.eventsTimeseries,
188
+ input,
189
+ (value) => _chunkDHSMXB54cjs.zEventsTimeseriesResponse.parse(value),
190
+ _optionalChain([init, 'optionalAccess', _22 => _22.signal])
191
+ );
192
+ },
193
+ funnel(input, init) {
194
+ return post(_chunkDHSMXB54cjs.QUERY_API_PATHS.funnel, input, (value) => _chunkDHSMXB54cjs.zFunnelResponse.parse(value), _optionalChain([init, 'optionalAccess', _23 => _23.signal]));
195
+ },
196
+ retention(input, init) {
197
+ return post(_chunkDHSMXB54cjs.QUERY_API_PATHS.retention, input, (value) => _chunkDHSMXB54cjs.zRetentionResponse.parse(value), _optionalChain([init, 'optionalAccess', _24 => _24.signal]));
198
+ }
199
+ }
200
+ };
201
+ }
202
+
203
+
204
+ exports.createDataClient = createDataClient;
205
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/client.cjs","../src/client.ts"],"names":[],"mappings":"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","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/client.cjs","sourcesContent":[null,"/**\n * `createDataClient` — typed fetch over the ingest and query planes.\n *\n * Deliberately stateless: no queue, no buffer, no background flush. Batch\n * assembly and cursor management belong to the caller (a forwarder already has\n * a cursor and byte-batching; duplicating that here would give it two).\n *\n * Every response is parsed with the contract schema, so a server that drifts\n * from the contract surfaces as `contract_mismatch` instead of quietly\n * returning something the caller then mis-reads.\n */\nimport {\n CLIENT_ID_HEADER,\n CLIENT_SECRET_HEADER,\n CONTRACT_VERSION,\n DataApiError,\n REQUEST_ID_HEADER,\n type ClientCredentials,\n} from \"./index.js\";\nimport { zIngestResponse, type IngestResponse, type ITrackHandlerPayload } from \"./ingest.js\";\nimport {\n QUERY_API_PATHS,\n zEventsTimeseriesResponse,\n zFunnelResponse,\n zRetentionResponse,\n type EventsTimeseriesInput,\n type EventsTimeseriesResponse,\n type FunnelInput,\n type FunnelResponse,\n type RetentionInput,\n type RetentionResponse,\n} from \"./query.js\";\n\nconst TRACK_PATH = \"/api/track\";\n\nexport interface DataClientRetryOptions {\n /** Attempts after the first. Only 429 and 5xx are retried. Default 2. */\n maxRetries?: number;\n /** First backoff step; doubles each attempt, with jitter. Default 500ms. */\n baseDelayMs?: number;\n /** Cap on a single backoff step. Default 20s. */\n maxDelayMs?: number;\n /** Prefer the server's `Retry-After` over the computed backoff. Default true. */\n respectRetryAfter?: boolean;\n}\n\nexport interface DataClientOptions extends ClientCredentials {\n /** Origin of the reopt-data deployment, e.g. `https://data.reopt.app`. Trailing slash ignored. */\n baseUrl: string;\n /** Injected so callers can pass a framework-instrumented fetch. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /** Per-attempt timeout. Default 10s. */\n timeoutMs?: number;\n retry?: DataClientRetryOptions;\n /** Appended to the default `reopt-data-contract/<version>`. */\n userAgent?: string;\n}\n\nexport interface DataRequestInit {\n signal?: AbortSignal;\n}\n\nexport interface DataClient {\n ingest: {\n /**\n * `POST /api/track`. Send events as one batch; splitting to stay under\n * the 512KB body cap is the caller's job, since only the caller knows how\n * to resume a partially-sent range.\n */\n track(events: ITrackHandlerPayload[], init?: DataRequestInit): Promise<IngestResponse>;\n };\n query: {\n eventsTimeseries(input: EventsTimeseriesInput, init?: DataRequestInit): Promise<EventsTimeseriesResponse>;\n funnel(input: FunnelInput, init?: DataRequestInit): Promise<FunnelResponse>;\n retention(input: RetentionInput, init?: DataRequestInit): Promise<RetentionResponse>;\n };\n}\n\nconst DEFAULTS = {\n timeoutMs: 10_000,\n maxRetries: 2,\n baseDelayMs: 500,\n maxDelayMs: 20_000,\n} as const;\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.replace(/\\/+$/, \"\");\n}\n\n/** `Retry-After` is either delta-seconds or an HTTP date; both are legal. */\nfunction parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n\n const seconds = Number(header);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n\n const at = Date.parse(header);\n if (Number.isFinite(at)) return Math.max(0, at - Date.now());\n\n return undefined;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error(\"Aborted\"));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal?.reason ?? new Error(\"Aborted\"));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\nfunction backoffDelay(attempt: number, retry: Required<DataClientRetryOptions>): number {\n const exponential = retry.baseDelayMs * 2 ** attempt;\n const capped = Math.min(exponential, retry.maxDelayMs);\n // Full jitter — a fleet of forwarders that all got 429 should not all come\n // back at the same instant.\n return Math.round(capped * (0.5 + Math.random() * 0.5));\n}\n\n/**\n * Body of a non-2xx response. Both planes share `{ status, code, error, ... }`,\n * so one reader covers them; anything else is reported as `internal_error`\n * with the raw text kept in `details`.\n */\nfunction toApiError(\n status: number,\n rawBody: string,\n retryAfterMs: number | undefined,\n requestId?: string\n): DataApiError {\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawBody);\n } catch {\n return new DataApiError({\n message: `reopt-data responded ${status} with a non-JSON body`,\n status,\n code: status >= 500 ? \"internal_error\" : \"unknown_error\",\n requestId,\n retryAfterMs,\n details: rawBody.slice(0, 2000),\n });\n }\n\n const body = (parsed ?? {}) as Record<string, unknown>;\n const code = typeof body.code === \"string\" ? body.code : status >= 500 ? \"internal_error\" : \"unknown_error\";\n const message =\n (typeof body.message === \"string\" && body.message) ||\n (typeof body.error === \"string\" && body.error) ||\n `reopt-data responded ${status}`;\n\n return new DataApiError({\n message,\n status,\n code,\n requestId: (typeof body.requestId === \"string\" ? body.requestId : undefined) ?? requestId,\n retryAfterMs,\n details: body.errors ?? undefined,\n });\n}\n\nexport function createDataClient(options: DataClientOptions): DataClient {\n const baseUrl = normalizeBaseUrl(options.baseUrl);\n const doFetch = options.fetch ?? globalThis.fetch;\n if (typeof doFetch !== \"function\") {\n throw new TypeError(\"createDataClient: no fetch implementation available — pass options.fetch\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;\n const retry: Required<DataClientRetryOptions> = {\n maxRetries: options.retry?.maxRetries ?? DEFAULTS.maxRetries,\n baseDelayMs: options.retry?.baseDelayMs ?? DEFAULTS.baseDelayMs,\n maxDelayMs: options.retry?.maxDelayMs ?? DEFAULTS.maxDelayMs,\n respectRetryAfter: options.retry?.respectRetryAfter ?? true,\n };\n\n const userAgent = options.userAgent\n ? `reopt-data-contract/${CONTRACT_VERSION} ${options.userAgent}`\n : `reopt-data-contract/${CONTRACT_VERSION}`;\n\n async function postOnce(path: string, body: unknown, signal?: AbortSignal): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(new Error(`reopt-data request timed out after ${timeoutMs}ms`)),\n timeoutMs\n );\n const onCallerAbort = () => controller.abort(signal?.reason ?? new Error(\"Aborted\"));\n signal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n try {\n return await doFetch(`${baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": userAgent,\n [CLIENT_ID_HEADER]: options.clientId,\n [CLIENT_SECRET_HEADER]: options.clientSecret,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onCallerAbort);\n }\n }\n\n async function post<T>(path: string, body: unknown, parse: (value: unknown) => T, signal?: AbortSignal): Promise<T> {\n let lastError: DataApiError | undefined;\n\n for (let attempt = 0; attempt <= retry.maxRetries; attempt++) {\n let response: Response;\n try {\n response = await postOnce(path, body, signal);\n } catch (cause) {\n // A caller-initiated abort is not a transport failure — propagate it.\n if (signal?.aborted) throw cause;\n lastError = new DataApiError({\n message: cause instanceof Error ? cause.message : String(cause),\n status: 0,\n code: \"network_error\",\n details: cause,\n });\n if (attempt < retry.maxRetries) {\n await sleep(backoffDelay(attempt, retry), signal);\n continue;\n }\n throw lastError;\n }\n\n const requestId = response.headers.get(REQUEST_ID_HEADER) ?? undefined;\n const rawBody = await response.text();\n\n if (response.ok) {\n let json: unknown;\n try {\n json = JSON.parse(rawBody);\n } catch (cause) {\n throw new DataApiError({\n message: \"reopt-data returned a 2xx with an unparseable body\",\n status: response.status,\n code: \"contract_mismatch\",\n requestId,\n details: cause,\n });\n }\n\n try {\n return parse(json);\n } catch (cause) {\n throw new DataApiError({\n message: \"reopt-data returned a 2xx that does not match the contract\",\n status: response.status,\n code: \"contract_mismatch\",\n requestId,\n details: cause,\n });\n }\n }\n\n const retryAfterMs = retry.respectRetryAfter ? parseRetryAfter(response.headers.get(\"retry-after\")) : undefined;\n lastError = toApiError(response.status, rawBody, retryAfterMs, requestId);\n\n if (!lastError.retryable || attempt === retry.maxRetries) throw lastError;\n await sleep(retryAfterMs ?? backoffDelay(attempt, retry), signal);\n }\n\n /* c8 ignore next -- the loop either returns or throws */\n throw lastError ?? new DataApiError({ message: \"reopt-data request failed\", status: 0, code: \"network_error\" });\n }\n\n return {\n ingest: {\n track(events, init) {\n return post(TRACK_PATH, events, (value) => zIngestResponse.parse(value), init?.signal);\n },\n },\n query: {\n eventsTimeseries(input, init) {\n return post(\n QUERY_API_PATHS.eventsTimeseries,\n input,\n (value) => zEventsTimeseriesResponse.parse(value),\n init?.signal\n );\n },\n funnel(input, init) {\n return post(QUERY_API_PATHS.funnel, input, (value) => zFunnelResponse.parse(value), init?.signal);\n },\n retention(input, init) {\n return post(QUERY_API_PATHS.retention, input, (value) => zRetentionResponse.parse(value), init?.signal);\n },\n },\n };\n}\n"]}
@@ -0,0 +1,59 @@
1
+ import { ClientCredentials } from './index.cjs';
2
+ import { ITrackHandlerPayload, IngestResponse } from './ingest.cjs';
3
+ import { EventsTimeseriesInput, EventsTimeseriesResponse, FunnelInput, FunnelResponse, RetentionInput, RetentionResponse } from './query.cjs';
4
+ import 'zod';
5
+
6
+ /**
7
+ * `createDataClient` — typed fetch over the ingest and query planes.
8
+ *
9
+ * Deliberately stateless: no queue, no buffer, no background flush. Batch
10
+ * assembly and cursor management belong to the caller (a forwarder already has
11
+ * a cursor and byte-batching; duplicating that here would give it two).
12
+ *
13
+ * Every response is parsed with the contract schema, so a server that drifts
14
+ * from the contract surfaces as `contract_mismatch` instead of quietly
15
+ * returning something the caller then mis-reads.
16
+ */
17
+
18
+ interface DataClientRetryOptions {
19
+ /** Attempts after the first. Only 429 and 5xx are retried. Default 2. */
20
+ maxRetries?: number;
21
+ /** First backoff step; doubles each attempt, with jitter. Default 500ms. */
22
+ baseDelayMs?: number;
23
+ /** Cap on a single backoff step. Default 20s. */
24
+ maxDelayMs?: number;
25
+ /** Prefer the server's `Retry-After` over the computed backoff. Default true. */
26
+ respectRetryAfter?: boolean;
27
+ }
28
+ interface DataClientOptions extends ClientCredentials {
29
+ /** Origin of the reopt-data deployment, e.g. `https://data.reopt.app`. Trailing slash ignored. */
30
+ baseUrl: string;
31
+ /** Injected so callers can pass a framework-instrumented fetch. Defaults to `globalThis.fetch`. */
32
+ fetch?: typeof globalThis.fetch;
33
+ /** Per-attempt timeout. Default 10s. */
34
+ timeoutMs?: number;
35
+ retry?: DataClientRetryOptions;
36
+ /** Appended to the default `reopt-data-contract/<version>`. */
37
+ userAgent?: string;
38
+ }
39
+ interface DataRequestInit {
40
+ signal?: AbortSignal;
41
+ }
42
+ interface DataClient {
43
+ ingest: {
44
+ /**
45
+ * `POST /api/track`. Send events as one batch; splitting to stay under
46
+ * the 512KB body cap is the caller's job, since only the caller knows how
47
+ * to resume a partially-sent range.
48
+ */
49
+ track(events: ITrackHandlerPayload[], init?: DataRequestInit): Promise<IngestResponse>;
50
+ };
51
+ query: {
52
+ eventsTimeseries(input: EventsTimeseriesInput, init?: DataRequestInit): Promise<EventsTimeseriesResponse>;
53
+ funnel(input: FunnelInput, init?: DataRequestInit): Promise<FunnelResponse>;
54
+ retention(input: RetentionInput, init?: DataRequestInit): Promise<RetentionResponse>;
55
+ };
56
+ }
57
+ declare function createDataClient(options: DataClientOptions): DataClient;
58
+
59
+ export { type DataClient, type DataClientOptions, type DataClientRetryOptions, type DataRequestInit, createDataClient };
@@ -0,0 +1,59 @@
1
+ import { ClientCredentials } from './index.js';
2
+ import { ITrackHandlerPayload, IngestResponse } from './ingest.js';
3
+ import { EventsTimeseriesInput, EventsTimeseriesResponse, FunnelInput, FunnelResponse, RetentionInput, RetentionResponse } from './query.js';
4
+ import 'zod';
5
+
6
+ /**
7
+ * `createDataClient` — typed fetch over the ingest and query planes.
8
+ *
9
+ * Deliberately stateless: no queue, no buffer, no background flush. Batch
10
+ * assembly and cursor management belong to the caller (a forwarder already has
11
+ * a cursor and byte-batching; duplicating that here would give it two).
12
+ *
13
+ * Every response is parsed with the contract schema, so a server that drifts
14
+ * from the contract surfaces as `contract_mismatch` instead of quietly
15
+ * returning something the caller then mis-reads.
16
+ */
17
+
18
+ interface DataClientRetryOptions {
19
+ /** Attempts after the first. Only 429 and 5xx are retried. Default 2. */
20
+ maxRetries?: number;
21
+ /** First backoff step; doubles each attempt, with jitter. Default 500ms. */
22
+ baseDelayMs?: number;
23
+ /** Cap on a single backoff step. Default 20s. */
24
+ maxDelayMs?: number;
25
+ /** Prefer the server's `Retry-After` over the computed backoff. Default true. */
26
+ respectRetryAfter?: boolean;
27
+ }
28
+ interface DataClientOptions extends ClientCredentials {
29
+ /** Origin of the reopt-data deployment, e.g. `https://data.reopt.app`. Trailing slash ignored. */
30
+ baseUrl: string;
31
+ /** Injected so callers can pass a framework-instrumented fetch. Defaults to `globalThis.fetch`. */
32
+ fetch?: typeof globalThis.fetch;
33
+ /** Per-attempt timeout. Default 10s. */
34
+ timeoutMs?: number;
35
+ retry?: DataClientRetryOptions;
36
+ /** Appended to the default `reopt-data-contract/<version>`. */
37
+ userAgent?: string;
38
+ }
39
+ interface DataRequestInit {
40
+ signal?: AbortSignal;
41
+ }
42
+ interface DataClient {
43
+ ingest: {
44
+ /**
45
+ * `POST /api/track`. Send events as one batch; splitting to stay under
46
+ * the 512KB body cap is the caller's job, since only the caller knows how
47
+ * to resume a partially-sent range.
48
+ */
49
+ track(events: ITrackHandlerPayload[], init?: DataRequestInit): Promise<IngestResponse>;
50
+ };
51
+ query: {
52
+ eventsTimeseries(input: EventsTimeseriesInput, init?: DataRequestInit): Promise<EventsTimeseriesResponse>;
53
+ funnel(input: FunnelInput, init?: DataRequestInit): Promise<FunnelResponse>;
54
+ retention(input: RetentionInput, init?: DataRequestInit): Promise<RetentionResponse>;
55
+ };
56
+ }
57
+ declare function createDataClient(options: DataClientOptions): DataClient;
58
+
59
+ export { type DataClient, type DataClientOptions, type DataClientRetryOptions, type DataRequestInit, createDataClient };
package/dist/client.js ADDED
@@ -0,0 +1,205 @@
1
+ import {
2
+ zIngestResponse
3
+ } from "./chunk-FRJK7OCQ.js";
4
+ import {
5
+ QUERY_API_PATHS,
6
+ zEventsTimeseriesResponse,
7
+ zFunnelResponse,
8
+ zRetentionResponse
9
+ } from "./chunk-SJYY42BP.js";
10
+ import {
11
+ CLIENT_ID_HEADER,
12
+ CLIENT_SECRET_HEADER,
13
+ CONTRACT_VERSION,
14
+ DataApiError,
15
+ REQUEST_ID_HEADER
16
+ } from "./chunk-IAOPUBJU.js";
17
+
18
+ // src/client.ts
19
+ var TRACK_PATH = "/api/track";
20
+ var DEFAULTS = {
21
+ timeoutMs: 1e4,
22
+ maxRetries: 2,
23
+ baseDelayMs: 500,
24
+ maxDelayMs: 2e4
25
+ };
26
+ function normalizeBaseUrl(baseUrl) {
27
+ return baseUrl.replace(/\/+$/, "");
28
+ }
29
+ function parseRetryAfter(header) {
30
+ if (!header) return void 0;
31
+ const seconds = Number(header);
32
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
33
+ const at = Date.parse(header);
34
+ if (Number.isFinite(at)) return Math.max(0, at - Date.now());
35
+ return void 0;
36
+ }
37
+ function sleep(ms, signal) {
38
+ return new Promise((resolve, reject) => {
39
+ if (signal?.aborted) {
40
+ reject(signal.reason ?? new Error("Aborted"));
41
+ return;
42
+ }
43
+ const timer = setTimeout(() => {
44
+ signal?.removeEventListener("abort", onAbort);
45
+ resolve();
46
+ }, ms);
47
+ const onAbort = () => {
48
+ clearTimeout(timer);
49
+ reject(signal?.reason ?? new Error("Aborted"));
50
+ };
51
+ signal?.addEventListener("abort", onAbort, { once: true });
52
+ });
53
+ }
54
+ function backoffDelay(attempt, retry) {
55
+ const exponential = retry.baseDelayMs * 2 ** attempt;
56
+ const capped = Math.min(exponential, retry.maxDelayMs);
57
+ return Math.round(capped * (0.5 + Math.random() * 0.5));
58
+ }
59
+ function toApiError(status, rawBody, retryAfterMs, requestId) {
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(rawBody);
63
+ } catch {
64
+ return new DataApiError({
65
+ message: `reopt-data responded ${status} with a non-JSON body`,
66
+ status,
67
+ code: status >= 500 ? "internal_error" : "unknown_error",
68
+ requestId,
69
+ retryAfterMs,
70
+ details: rawBody.slice(0, 2e3)
71
+ });
72
+ }
73
+ const body = parsed ?? {};
74
+ const code = typeof body.code === "string" ? body.code : status >= 500 ? "internal_error" : "unknown_error";
75
+ const message = typeof body.message === "string" && body.message || typeof body.error === "string" && body.error || `reopt-data responded ${status}`;
76
+ return new DataApiError({
77
+ message,
78
+ status,
79
+ code,
80
+ requestId: (typeof body.requestId === "string" ? body.requestId : void 0) ?? requestId,
81
+ retryAfterMs,
82
+ details: body.errors ?? void 0
83
+ });
84
+ }
85
+ function createDataClient(options) {
86
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
87
+ const doFetch = options.fetch ?? globalThis.fetch;
88
+ if (typeof doFetch !== "function") {
89
+ throw new TypeError("createDataClient: no fetch implementation available \u2014 pass options.fetch");
90
+ }
91
+ const timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;
92
+ const retry = {
93
+ maxRetries: options.retry?.maxRetries ?? DEFAULTS.maxRetries,
94
+ baseDelayMs: options.retry?.baseDelayMs ?? DEFAULTS.baseDelayMs,
95
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULTS.maxDelayMs,
96
+ respectRetryAfter: options.retry?.respectRetryAfter ?? true
97
+ };
98
+ const userAgent = options.userAgent ? `reopt-data-contract/${CONTRACT_VERSION} ${options.userAgent}` : `reopt-data-contract/${CONTRACT_VERSION}`;
99
+ async function postOnce(path, body, signal) {
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(
102
+ () => controller.abort(new Error(`reopt-data request timed out after ${timeoutMs}ms`)),
103
+ timeoutMs
104
+ );
105
+ const onCallerAbort = () => controller.abort(signal?.reason ?? new Error("Aborted"));
106
+ signal?.addEventListener("abort", onCallerAbort, { once: true });
107
+ try {
108
+ return await doFetch(`${baseUrl}${path}`, {
109
+ method: "POST",
110
+ headers: {
111
+ "content-type": "application/json",
112
+ "user-agent": userAgent,
113
+ [CLIENT_ID_HEADER]: options.clientId,
114
+ [CLIENT_SECRET_HEADER]: options.clientSecret
115
+ },
116
+ body: JSON.stringify(body),
117
+ signal: controller.signal
118
+ });
119
+ } finally {
120
+ clearTimeout(timer);
121
+ signal?.removeEventListener("abort", onCallerAbort);
122
+ }
123
+ }
124
+ async function post(path, body, parse, signal) {
125
+ let lastError;
126
+ for (let attempt = 0; attempt <= retry.maxRetries; attempt++) {
127
+ let response;
128
+ try {
129
+ response = await postOnce(path, body, signal);
130
+ } catch (cause) {
131
+ if (signal?.aborted) throw cause;
132
+ lastError = new DataApiError({
133
+ message: cause instanceof Error ? cause.message : String(cause),
134
+ status: 0,
135
+ code: "network_error",
136
+ details: cause
137
+ });
138
+ if (attempt < retry.maxRetries) {
139
+ await sleep(backoffDelay(attempt, retry), signal);
140
+ continue;
141
+ }
142
+ throw lastError;
143
+ }
144
+ const requestId = response.headers.get(REQUEST_ID_HEADER) ?? void 0;
145
+ const rawBody = await response.text();
146
+ if (response.ok) {
147
+ let json;
148
+ try {
149
+ json = JSON.parse(rawBody);
150
+ } catch (cause) {
151
+ throw new DataApiError({
152
+ message: "reopt-data returned a 2xx with an unparseable body",
153
+ status: response.status,
154
+ code: "contract_mismatch",
155
+ requestId,
156
+ details: cause
157
+ });
158
+ }
159
+ try {
160
+ return parse(json);
161
+ } catch (cause) {
162
+ throw new DataApiError({
163
+ message: "reopt-data returned a 2xx that does not match the contract",
164
+ status: response.status,
165
+ code: "contract_mismatch",
166
+ requestId,
167
+ details: cause
168
+ });
169
+ }
170
+ }
171
+ const retryAfterMs = retry.respectRetryAfter ? parseRetryAfter(response.headers.get("retry-after")) : void 0;
172
+ lastError = toApiError(response.status, rawBody, retryAfterMs, requestId);
173
+ if (!lastError.retryable || attempt === retry.maxRetries) throw lastError;
174
+ await sleep(retryAfterMs ?? backoffDelay(attempt, retry), signal);
175
+ }
176
+ throw lastError ?? new DataApiError({ message: "reopt-data request failed", status: 0, code: "network_error" });
177
+ }
178
+ return {
179
+ ingest: {
180
+ track(events, init) {
181
+ return post(TRACK_PATH, events, (value) => zIngestResponse.parse(value), init?.signal);
182
+ }
183
+ },
184
+ query: {
185
+ eventsTimeseries(input, init) {
186
+ return post(
187
+ QUERY_API_PATHS.eventsTimeseries,
188
+ input,
189
+ (value) => zEventsTimeseriesResponse.parse(value),
190
+ init?.signal
191
+ );
192
+ },
193
+ funnel(input, init) {
194
+ return post(QUERY_API_PATHS.funnel, input, (value) => zFunnelResponse.parse(value), init?.signal);
195
+ },
196
+ retention(input, init) {
197
+ return post(QUERY_API_PATHS.retention, input, (value) => zRetentionResponse.parse(value), init?.signal);
198
+ }
199
+ }
200
+ };
201
+ }
202
+ export {
203
+ createDataClient
204
+ };
205
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * `createDataClient` — typed fetch over the ingest and query planes.\n *\n * Deliberately stateless: no queue, no buffer, no background flush. Batch\n * assembly and cursor management belong to the caller (a forwarder already has\n * a cursor and byte-batching; duplicating that here would give it two).\n *\n * Every response is parsed with the contract schema, so a server that drifts\n * from the contract surfaces as `contract_mismatch` instead of quietly\n * returning something the caller then mis-reads.\n */\nimport {\n CLIENT_ID_HEADER,\n CLIENT_SECRET_HEADER,\n CONTRACT_VERSION,\n DataApiError,\n REQUEST_ID_HEADER,\n type ClientCredentials,\n} from \"./index.js\";\nimport { zIngestResponse, type IngestResponse, type ITrackHandlerPayload } from \"./ingest.js\";\nimport {\n QUERY_API_PATHS,\n zEventsTimeseriesResponse,\n zFunnelResponse,\n zRetentionResponse,\n type EventsTimeseriesInput,\n type EventsTimeseriesResponse,\n type FunnelInput,\n type FunnelResponse,\n type RetentionInput,\n type RetentionResponse,\n} from \"./query.js\";\n\nconst TRACK_PATH = \"/api/track\";\n\nexport interface DataClientRetryOptions {\n /** Attempts after the first. Only 429 and 5xx are retried. Default 2. */\n maxRetries?: number;\n /** First backoff step; doubles each attempt, with jitter. Default 500ms. */\n baseDelayMs?: number;\n /** Cap on a single backoff step. Default 20s. */\n maxDelayMs?: number;\n /** Prefer the server's `Retry-After` over the computed backoff. Default true. */\n respectRetryAfter?: boolean;\n}\n\nexport interface DataClientOptions extends ClientCredentials {\n /** Origin of the reopt-data deployment, e.g. `https://data.reopt.app`. Trailing slash ignored. */\n baseUrl: string;\n /** Injected so callers can pass a framework-instrumented fetch. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /** Per-attempt timeout. Default 10s. */\n timeoutMs?: number;\n retry?: DataClientRetryOptions;\n /** Appended to the default `reopt-data-contract/<version>`. */\n userAgent?: string;\n}\n\nexport interface DataRequestInit {\n signal?: AbortSignal;\n}\n\nexport interface DataClient {\n ingest: {\n /**\n * `POST /api/track`. Send events as one batch; splitting to stay under\n * the 512KB body cap is the caller's job, since only the caller knows how\n * to resume a partially-sent range.\n */\n track(events: ITrackHandlerPayload[], init?: DataRequestInit): Promise<IngestResponse>;\n };\n query: {\n eventsTimeseries(input: EventsTimeseriesInput, init?: DataRequestInit): Promise<EventsTimeseriesResponse>;\n funnel(input: FunnelInput, init?: DataRequestInit): Promise<FunnelResponse>;\n retention(input: RetentionInput, init?: DataRequestInit): Promise<RetentionResponse>;\n };\n}\n\nconst DEFAULTS = {\n timeoutMs: 10_000,\n maxRetries: 2,\n baseDelayMs: 500,\n maxDelayMs: 20_000,\n} as const;\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.replace(/\\/+$/, \"\");\n}\n\n/** `Retry-After` is either delta-seconds or an HTTP date; both are legal. */\nfunction parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n\n const seconds = Number(header);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n\n const at = Date.parse(header);\n if (Number.isFinite(at)) return Math.max(0, at - Date.now());\n\n return undefined;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error(\"Aborted\"));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal?.reason ?? new Error(\"Aborted\"));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\nfunction backoffDelay(attempt: number, retry: Required<DataClientRetryOptions>): number {\n const exponential = retry.baseDelayMs * 2 ** attempt;\n const capped = Math.min(exponential, retry.maxDelayMs);\n // Full jitter — a fleet of forwarders that all got 429 should not all come\n // back at the same instant.\n return Math.round(capped * (0.5 + Math.random() * 0.5));\n}\n\n/**\n * Body of a non-2xx response. Both planes share `{ status, code, error, ... }`,\n * so one reader covers them; anything else is reported as `internal_error`\n * with the raw text kept in `details`.\n */\nfunction toApiError(\n status: number,\n rawBody: string,\n retryAfterMs: number | undefined,\n requestId?: string\n): DataApiError {\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawBody);\n } catch {\n return new DataApiError({\n message: `reopt-data responded ${status} with a non-JSON body`,\n status,\n code: status >= 500 ? \"internal_error\" : \"unknown_error\",\n requestId,\n retryAfterMs,\n details: rawBody.slice(0, 2000),\n });\n }\n\n const body = (parsed ?? {}) as Record<string, unknown>;\n const code = typeof body.code === \"string\" ? body.code : status >= 500 ? \"internal_error\" : \"unknown_error\";\n const message =\n (typeof body.message === \"string\" && body.message) ||\n (typeof body.error === \"string\" && body.error) ||\n `reopt-data responded ${status}`;\n\n return new DataApiError({\n message,\n status,\n code,\n requestId: (typeof body.requestId === \"string\" ? body.requestId : undefined) ?? requestId,\n retryAfterMs,\n details: body.errors ?? undefined,\n });\n}\n\nexport function createDataClient(options: DataClientOptions): DataClient {\n const baseUrl = normalizeBaseUrl(options.baseUrl);\n const doFetch = options.fetch ?? globalThis.fetch;\n if (typeof doFetch !== \"function\") {\n throw new TypeError(\"createDataClient: no fetch implementation available — pass options.fetch\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;\n const retry: Required<DataClientRetryOptions> = {\n maxRetries: options.retry?.maxRetries ?? DEFAULTS.maxRetries,\n baseDelayMs: options.retry?.baseDelayMs ?? DEFAULTS.baseDelayMs,\n maxDelayMs: options.retry?.maxDelayMs ?? DEFAULTS.maxDelayMs,\n respectRetryAfter: options.retry?.respectRetryAfter ?? true,\n };\n\n const userAgent = options.userAgent\n ? `reopt-data-contract/${CONTRACT_VERSION} ${options.userAgent}`\n : `reopt-data-contract/${CONTRACT_VERSION}`;\n\n async function postOnce(path: string, body: unknown, signal?: AbortSignal): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(new Error(`reopt-data request timed out after ${timeoutMs}ms`)),\n timeoutMs\n );\n const onCallerAbort = () => controller.abort(signal?.reason ?? new Error(\"Aborted\"));\n signal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n try {\n return await doFetch(`${baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": userAgent,\n [CLIENT_ID_HEADER]: options.clientId,\n [CLIENT_SECRET_HEADER]: options.clientSecret,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onCallerAbort);\n }\n }\n\n async function post<T>(path: string, body: unknown, parse: (value: unknown) => T, signal?: AbortSignal): Promise<T> {\n let lastError: DataApiError | undefined;\n\n for (let attempt = 0; attempt <= retry.maxRetries; attempt++) {\n let response: Response;\n try {\n response = await postOnce(path, body, signal);\n } catch (cause) {\n // A caller-initiated abort is not a transport failure — propagate it.\n if (signal?.aborted) throw cause;\n lastError = new DataApiError({\n message: cause instanceof Error ? cause.message : String(cause),\n status: 0,\n code: \"network_error\",\n details: cause,\n });\n if (attempt < retry.maxRetries) {\n await sleep(backoffDelay(attempt, retry), signal);\n continue;\n }\n throw lastError;\n }\n\n const requestId = response.headers.get(REQUEST_ID_HEADER) ?? undefined;\n const rawBody = await response.text();\n\n if (response.ok) {\n let json: unknown;\n try {\n json = JSON.parse(rawBody);\n } catch (cause) {\n throw new DataApiError({\n message: \"reopt-data returned a 2xx with an unparseable body\",\n status: response.status,\n code: \"contract_mismatch\",\n requestId,\n details: cause,\n });\n }\n\n try {\n return parse(json);\n } catch (cause) {\n throw new DataApiError({\n message: \"reopt-data returned a 2xx that does not match the contract\",\n status: response.status,\n code: \"contract_mismatch\",\n requestId,\n details: cause,\n });\n }\n }\n\n const retryAfterMs = retry.respectRetryAfter ? parseRetryAfter(response.headers.get(\"retry-after\")) : undefined;\n lastError = toApiError(response.status, rawBody, retryAfterMs, requestId);\n\n if (!lastError.retryable || attempt === retry.maxRetries) throw lastError;\n await sleep(retryAfterMs ?? backoffDelay(attempt, retry), signal);\n }\n\n /* c8 ignore next -- the loop either returns or throws */\n throw lastError ?? new DataApiError({ message: \"reopt-data request failed\", status: 0, code: \"network_error\" });\n }\n\n return {\n ingest: {\n track(events, init) {\n return post(TRACK_PATH, events, (value) => zIngestResponse.parse(value), init?.signal);\n },\n },\n query: {\n eventsTimeseries(input, init) {\n return post(\n QUERY_API_PATHS.eventsTimeseries,\n input,\n (value) => zEventsTimeseriesResponse.parse(value),\n init?.signal\n );\n },\n funnel(input, init) {\n return post(QUERY_API_PATHS.funnel, input, (value) => zFunnelResponse.parse(value), init?.signal);\n },\n retention(input, init) {\n return post(QUERY_API_PATHS.retention, input, (value) => zRetentionResponse.parse(value), init?.signal);\n },\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+ var _chunkZQVRSCTWcjs = require('./chunk-ZQVRSCTW.cjs');
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+ exports.CLIENT_ID_HEADER = _chunkZQVRSCTWcjs.CLIENT_ID_HEADER; exports.CLIENT_SECRET_HEADER = _chunkZQVRSCTWcjs.CLIENT_SECRET_HEADER; exports.CONTRACT_VERSION = _chunkZQVRSCTWcjs.CONTRACT_VERSION; exports.CONTRACT_VERSION_HEADER = _chunkZQVRSCTWcjs.CONTRACT_VERSION_HEADER; exports.DEVICE_ID_HEADER = _chunkZQVRSCTWcjs.DEVICE_ID_HEADER; exports.DataApiError = _chunkZQVRSCTWcjs.DataApiError; exports.REQUEST_ID_HEADER = _chunkZQVRSCTWcjs.REQUEST_ID_HEADER; exports.WRITE_KEY_HEADER = _chunkZQVRSCTWcjs.WRITE_KEY_HEADER; exports.zDate = _chunkZQVRSCTWcjs.zDate; exports.zInstant = _chunkZQVRSCTWcjs.zInstant;
25
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/index.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,+lBAAC","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/index.cjs"}