pacificdb-client 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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/index.cjs +377 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +340 -0
- package/dist/index.mjs +340 -0
- package/package.json +47 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PacificDB connection string parsing, building, and redaction.
|
|
3
|
+
*
|
|
4
|
+
* Format:
|
|
5
|
+
* pacificdb://<user>:<password>@<clusterHost>/<databaseName>
|
|
6
|
+
* ?projectId=..&clusterId=..&databaseId=..&tls=true&authSource=admin&appName=..
|
|
7
|
+
*/
|
|
8
|
+
interface PacificConnectionString {
|
|
9
|
+
protocol: 'pacificdb';
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
clusterHost: string;
|
|
13
|
+
databaseName: string;
|
|
14
|
+
orgId: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
clusterId: string;
|
|
17
|
+
databaseId: string;
|
|
18
|
+
tls: boolean;
|
|
19
|
+
authSource: string;
|
|
20
|
+
appName?: string;
|
|
21
|
+
}
|
|
22
|
+
declare function parseConnectionString(uri: string): PacificConnectionString;
|
|
23
|
+
declare function buildConnectionString(input: Omit<PacificConnectionString, 'protocol'>): string;
|
|
24
|
+
/** Replace the password portion of a URI with REDACTED. Never throws. */
|
|
25
|
+
declare function redactConnectionString(uri: string): string;
|
|
26
|
+
/** Deep-copy a value with secret-looking keys replaced by REDACTED. */
|
|
27
|
+
declare function redactSecrets(value: unknown): unknown;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* PacificDBClient - database client over the PacificDB gateway data plane.
|
|
31
|
+
*
|
|
32
|
+
* Auth: exchanges the connection string credentials for a short-lived database
|
|
33
|
+
* session (POST /api/v1/db/sessions), then sends Bearer session tokens.
|
|
34
|
+
* Sessions are transparently refreshed once when the server reports 401.
|
|
35
|
+
*
|
|
36
|
+
* Retries: exponential backoff with jitter for server_busy / not_leader /
|
|
37
|
+
* 429 / 503 / timeouts, bounded by retryLimit.
|
|
38
|
+
*
|
|
39
|
+
* Logging: optional logger receives structured events; connection strings and
|
|
40
|
+
* passwords are always redacted before they reach the logger.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
interface PacificDBLogger {
|
|
44
|
+
debug?(message: string, fields?: Record<string, unknown>): void;
|
|
45
|
+
warn?(message: string, fields?: Record<string, unknown>): void;
|
|
46
|
+
}
|
|
47
|
+
interface PacificDBClientOptions {
|
|
48
|
+
/** Override the gateway base URL (defaults to https://<clusterHost>). */
|
|
49
|
+
apiUrl?: string;
|
|
50
|
+
/** Allow http:// gateways (local development only). */
|
|
51
|
+
allowInsecure?: boolean;
|
|
52
|
+
fetch?: typeof fetch;
|
|
53
|
+
/** Timeout for read operations (find). Default 30s. */
|
|
54
|
+
readTimeoutMs?: number;
|
|
55
|
+
/** Timeout for write operations (insert/update/delete). Default 30s. */
|
|
56
|
+
writeTimeoutMs?: number;
|
|
57
|
+
retryLimit?: number;
|
|
58
|
+
retryBackoffMs?: number;
|
|
59
|
+
logger?: PacificDBLogger;
|
|
60
|
+
appName?: string;
|
|
61
|
+
}
|
|
62
|
+
interface FindOptions {
|
|
63
|
+
limit?: number;
|
|
64
|
+
skip?: number;
|
|
65
|
+
sort?: Record<string, 1 | -1>;
|
|
66
|
+
}
|
|
67
|
+
interface CollectionHandle<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
68
|
+
insertOne(document: T): Promise<{
|
|
69
|
+
inserted: boolean;
|
|
70
|
+
id: string | null;
|
|
71
|
+
}>;
|
|
72
|
+
findOne(filter: Partial<T>): Promise<T | null>;
|
|
73
|
+
find(filter: Partial<T>, options?: FindOptions): Promise<T[]>;
|
|
74
|
+
updateOne(filter: Partial<T>, update: Record<string, unknown>): Promise<unknown>;
|
|
75
|
+
deleteOne(filter: Partial<T>): Promise<unknown>;
|
|
76
|
+
}
|
|
77
|
+
interface ModelHandle<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
78
|
+
create(document: T): Promise<{
|
|
79
|
+
inserted: boolean;
|
|
80
|
+
id: string | null;
|
|
81
|
+
}>;
|
|
82
|
+
findOne(filter: Partial<T>): Promise<T | null>;
|
|
83
|
+
find(filter: Partial<T>, options?: FindOptions): Promise<T[]>;
|
|
84
|
+
updateOne(filter: Partial<T>, update: Record<string, unknown>): Promise<unknown>;
|
|
85
|
+
deleteOne(filter: Partial<T>): Promise<unknown>;
|
|
86
|
+
}
|
|
87
|
+
type ModelSchema = Record<string, unknown>;
|
|
88
|
+
declare class PacificDBClient {
|
|
89
|
+
readonly connection: PacificConnectionString;
|
|
90
|
+
private readonly fetchImpl;
|
|
91
|
+
private readonly apiUrl;
|
|
92
|
+
private readonly readTimeoutMs;
|
|
93
|
+
private readonly writeTimeoutMs;
|
|
94
|
+
private readonly retryLimit;
|
|
95
|
+
private readonly retryBackoffMs;
|
|
96
|
+
private readonly logger?;
|
|
97
|
+
private sessionToken;
|
|
98
|
+
private connected;
|
|
99
|
+
constructor(uri: string, options?: PacificDBClientOptions);
|
|
100
|
+
/** The redacted URI (safe for logs/errors). */
|
|
101
|
+
get redactedUri(): string;
|
|
102
|
+
connect(): Promise<void>;
|
|
103
|
+
private authenticate;
|
|
104
|
+
close(): Promise<void>;
|
|
105
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string): CollectionHandle<T>;
|
|
106
|
+
model<T extends Record<string, unknown> = Record<string, unknown>>(name: string, schema: ModelSchema): ModelHandle<T>;
|
|
107
|
+
private request;
|
|
108
|
+
private backoff;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Typed error hierarchy for the PacificDB client.
|
|
113
|
+
*/
|
|
114
|
+
declare class PacificDBError extends Error {
|
|
115
|
+
readonly code: string;
|
|
116
|
+
readonly requestId?: string;
|
|
117
|
+
constructor(message: string, code?: string, requestId?: string);
|
|
118
|
+
}
|
|
119
|
+
/** Invalid connection string or client configuration. */
|
|
120
|
+
declare class PacificDBConfigError extends PacificDBError {
|
|
121
|
+
constructor(message: string);
|
|
122
|
+
}
|
|
123
|
+
/** Authentication or session failure (401-class). */
|
|
124
|
+
declare class PacificDBAuthError extends PacificDBError {
|
|
125
|
+
constructor(message: string, requestId?: string);
|
|
126
|
+
}
|
|
127
|
+
/** Non-retriable client-side failure (4xx-class). */
|
|
128
|
+
declare class PacificDBClientError extends PacificDBError {
|
|
129
|
+
readonly status: number;
|
|
130
|
+
constructor(message: string, code: string, status: number, requestId?: string);
|
|
131
|
+
}
|
|
132
|
+
/** Retriable server-side failure (5xx-class, server_busy, not_leader). */
|
|
133
|
+
declare class PacificDBServerError extends PacificDBError {
|
|
134
|
+
readonly status: number;
|
|
135
|
+
constructor(message: string, code: string, status: number, requestId?: string);
|
|
136
|
+
}
|
|
137
|
+
/** Request exceeded the configured timeout. */
|
|
138
|
+
declare class PacificDBTimeoutError extends PacificDBError {
|
|
139
|
+
constructor(timeoutMs: number, requestId?: string);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { type CollectionHandle, type FindOptions, type ModelHandle, type PacificConnectionString, PacificDBAuthError, PacificDBClient, PacificDBClientError, type PacificDBClientOptions, PacificDBConfigError, PacificDBError, type PacificDBLogger, PacificDBServerError, PacificDBTimeoutError, buildConnectionString, parseConnectionString, redactConnectionString, redactSecrets };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var PacificDBError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
requestId;
|
|
5
|
+
constructor(message, code = "PACIFICDB_ERROR", requestId) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.requestId = requestId;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var PacificDBConfigError = class extends PacificDBError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message, "CONFIG_ERROR");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var PacificDBAuthError = class extends PacificDBError {
|
|
18
|
+
constructor(message, requestId) {
|
|
19
|
+
super(message, "AUTH_ERROR", requestId);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var PacificDBClientError = class extends PacificDBError {
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, code, status, requestId) {
|
|
25
|
+
super(message, code, requestId);
|
|
26
|
+
this.status = status;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var PacificDBServerError = class extends PacificDBError {
|
|
30
|
+
status;
|
|
31
|
+
constructor(message, code, status, requestId) {
|
|
32
|
+
super(message, code, requestId);
|
|
33
|
+
this.status = status;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var PacificDBTimeoutError = class extends PacificDBError {
|
|
37
|
+
constructor(timeoutMs, requestId) {
|
|
38
|
+
super(`PacificDB request timed out after ${timeoutMs}ms`, "TIMEOUT", requestId);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/connectionString.ts
|
|
43
|
+
var SECRET_KEYS = /* @__PURE__ */ new Set(["password", "token", "secret", "key", "sessiontoken", "connectionstring"]);
|
|
44
|
+
function parseConnectionString(uri) {
|
|
45
|
+
let url;
|
|
46
|
+
try {
|
|
47
|
+
url = new URL(uri);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new PacificDBConfigError(`Invalid PacificDB connection string: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
const protocol = url.protocol.replace(":", "");
|
|
52
|
+
if (protocol !== "pacificdb") {
|
|
53
|
+
throw new PacificDBConfigError("PacificDB connection strings must use the pacificdb:// protocol.");
|
|
54
|
+
}
|
|
55
|
+
if (!url.username || !url.password) {
|
|
56
|
+
throw new PacificDBConfigError("PacificDB connection string requires a database username and password.");
|
|
57
|
+
}
|
|
58
|
+
const databaseName = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
|
59
|
+
if (!databaseName) {
|
|
60
|
+
throw new PacificDBConfigError("PacificDB connection string requires a database name path.");
|
|
61
|
+
}
|
|
62
|
+
const orgId = url.searchParams.get("orgId") || url.searchParams.get("organizationId") || "";
|
|
63
|
+
const projectId = url.searchParams.get("projectId") || "";
|
|
64
|
+
const clusterId = url.searchParams.get("clusterId") || "";
|
|
65
|
+
const databaseId = url.searchParams.get("databaseId") || "";
|
|
66
|
+
if (!orgId || !projectId || !clusterId || !databaseId) {
|
|
67
|
+
throw new PacificDBConfigError("PacificDB connection string requires orgId, projectId, clusterId, and databaseId.");
|
|
68
|
+
}
|
|
69
|
+
const tlsRaw = url.searchParams.get("tls");
|
|
70
|
+
return {
|
|
71
|
+
protocol: "pacificdb",
|
|
72
|
+
username: decodeURIComponent(url.username),
|
|
73
|
+
password: decodeURIComponent(url.password),
|
|
74
|
+
clusterHost: url.host,
|
|
75
|
+
databaseName,
|
|
76
|
+
orgId,
|
|
77
|
+
projectId,
|
|
78
|
+
clusterId,
|
|
79
|
+
databaseId,
|
|
80
|
+
tls: tlsRaw === null ? true : tlsRaw !== "false",
|
|
81
|
+
authSource: url.searchParams.get("authSource") || "admin",
|
|
82
|
+
appName: url.searchParams.get("appName") || void 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function buildConnectionString(input) {
|
|
86
|
+
const url = new URL(`pacificdb://${input.clusterHost}/${encodeURIComponent(input.databaseName)}`);
|
|
87
|
+
url.username = encodeURIComponent(input.username);
|
|
88
|
+
url.password = encodeURIComponent(input.password);
|
|
89
|
+
if (input.orgId) url.searchParams.set("orgId", input.orgId);
|
|
90
|
+
url.searchParams.set("projectId", input.projectId);
|
|
91
|
+
url.searchParams.set("clusterId", input.clusterId);
|
|
92
|
+
url.searchParams.set("databaseId", input.databaseId);
|
|
93
|
+
url.searchParams.set("tls", String(input.tls !== false));
|
|
94
|
+
url.searchParams.set("authSource", input.authSource || "admin");
|
|
95
|
+
if (input.appName) url.searchParams.set("appName", input.appName);
|
|
96
|
+
return url.toString();
|
|
97
|
+
}
|
|
98
|
+
function redactConnectionString(uri) {
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(uri);
|
|
101
|
+
if (url.password) url.password = "REDACTED";
|
|
102
|
+
return url.toString();
|
|
103
|
+
} catch {
|
|
104
|
+
return uri.replace(/(pacificdb:\/\/[^:]+:)[^@]+@/i, "$1REDACTED@");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function redactSecrets(value) {
|
|
108
|
+
if (!value || typeof value !== "object") return value;
|
|
109
|
+
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const [key, item] of Object.entries(value)) {
|
|
112
|
+
out[key] = SECRET_KEYS.has(key.toLowerCase()) ? "REDACTED" : redactSecrets(item);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/client.ts
|
|
118
|
+
var RETRIABLE_CODES = /* @__PURE__ */ new Set(["server_busy", "not_leader", "TIMEOUT", "RATE_LIMITED", "HTTP_503", "HTTP_429", "ENGINE_UNAVAILABLE"]);
|
|
119
|
+
function randomRequestId() {
|
|
120
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
121
|
+
}
|
|
122
|
+
var PacificDBClient = class {
|
|
123
|
+
connection;
|
|
124
|
+
fetchImpl;
|
|
125
|
+
apiUrl;
|
|
126
|
+
readTimeoutMs;
|
|
127
|
+
writeTimeoutMs;
|
|
128
|
+
retryLimit;
|
|
129
|
+
retryBackoffMs;
|
|
130
|
+
logger;
|
|
131
|
+
sessionToken = "";
|
|
132
|
+
connected = false;
|
|
133
|
+
constructor(uri, options = {}) {
|
|
134
|
+
this.connection = parseConnectionString(uri);
|
|
135
|
+
this.apiUrl = (options.apiUrl || `https://${this.connection.clusterHost}`).replace(/\/+$/, "");
|
|
136
|
+
if (this.apiUrl.startsWith("http://") && !options.allowInsecure) {
|
|
137
|
+
throw new PacificDBConfigError(
|
|
138
|
+
"Refusing to send database credentials over http://. Use https, or pass { allowInsecure: true } for local development."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const fetchImpl = options.fetch || (typeof fetch !== "undefined" ? fetch : void 0);
|
|
142
|
+
if (!fetchImpl) {
|
|
143
|
+
throw new PacificDBConfigError("No fetch implementation available. Use Node.js >= 18 or pass options.fetch.");
|
|
144
|
+
}
|
|
145
|
+
this.fetchImpl = fetchImpl;
|
|
146
|
+
this.readTimeoutMs = options.readTimeoutMs ?? 3e4;
|
|
147
|
+
this.writeTimeoutMs = options.writeTimeoutMs ?? 3e4;
|
|
148
|
+
this.retryLimit = options.retryLimit ?? 3;
|
|
149
|
+
this.retryBackoffMs = options.retryBackoffMs ?? 250;
|
|
150
|
+
this.logger = options.logger;
|
|
151
|
+
}
|
|
152
|
+
/** The redacted URI (safe for logs/errors). */
|
|
153
|
+
get redactedUri() {
|
|
154
|
+
return redactConnectionString(
|
|
155
|
+
`pacificdb://${encodeURIComponent(this.connection.username)}:x@${this.connection.clusterHost}/${this.connection.databaseName}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
async connect() {
|
|
159
|
+
await this.authenticate();
|
|
160
|
+
this.connected = true;
|
|
161
|
+
}
|
|
162
|
+
async authenticate() {
|
|
163
|
+
const result = await this.request("POST", "/api/v1/db/sessions", {
|
|
164
|
+
username: this.connection.username,
|
|
165
|
+
password: this.connection.password,
|
|
166
|
+
orgId: this.connection.orgId,
|
|
167
|
+
projectId: this.connection.projectId,
|
|
168
|
+
clusterId: this.connection.clusterId,
|
|
169
|
+
databaseId: this.connection.databaseId,
|
|
170
|
+
databaseName: this.connection.databaseName,
|
|
171
|
+
authSource: this.connection.authSource
|
|
172
|
+
}, { requireSession: false, isWrite: true });
|
|
173
|
+
if (!result?.sessionToken) {
|
|
174
|
+
throw new PacificDBAuthError("Database authentication did not return a session token.");
|
|
175
|
+
}
|
|
176
|
+
this.sessionToken = result.sessionToken;
|
|
177
|
+
this.logger?.debug?.("pacificdb.session.created", { database: this.connection.databaseName });
|
|
178
|
+
}
|
|
179
|
+
async close() {
|
|
180
|
+
if (!this.sessionToken) return;
|
|
181
|
+
try {
|
|
182
|
+
await this.request("DELETE", "/api/v1/db/sessions/current", void 0, { isWrite: true });
|
|
183
|
+
} catch {
|
|
184
|
+
} finally {
|
|
185
|
+
this.sessionToken = "";
|
|
186
|
+
this.connected = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
collection(name) {
|
|
190
|
+
const encoded = encodeURIComponent(name);
|
|
191
|
+
return {
|
|
192
|
+
insertOne: async (document) => {
|
|
193
|
+
const r = await this.request(
|
|
194
|
+
"POST",
|
|
195
|
+
`/api/v1/db/collections/${encoded}/documents`,
|
|
196
|
+
{ document },
|
|
197
|
+
{ isWrite: true }
|
|
198
|
+
);
|
|
199
|
+
return { inserted: r?.inserted !== false, id: r?.id ?? null };
|
|
200
|
+
},
|
|
201
|
+
findOne: async (filter) => {
|
|
202
|
+
const r = await this.request(
|
|
203
|
+
"POST",
|
|
204
|
+
`/api/v1/db/collections/${encoded}/find`,
|
|
205
|
+
{ query: filter, limit: 1 },
|
|
206
|
+
{ isWrite: false }
|
|
207
|
+
);
|
|
208
|
+
return r?.documents?.[0] ?? null;
|
|
209
|
+
},
|
|
210
|
+
find: async (filter, options = {}) => {
|
|
211
|
+
const r = await this.request(
|
|
212
|
+
"POST",
|
|
213
|
+
`/api/v1/db/collections/${encoded}/find`,
|
|
214
|
+
{ query: filter, ...options },
|
|
215
|
+
{ isWrite: false }
|
|
216
|
+
);
|
|
217
|
+
return r?.documents ?? [];
|
|
218
|
+
},
|
|
219
|
+
updateOne: (filter, update) => this.request("PATCH", `/api/v1/db/collections/${encoded}/documents`, { filter, update }, { isWrite: true }),
|
|
220
|
+
deleteOne: (filter) => this.request("DELETE", `/api/v1/db/collections/${encoded}/documents`, { filter }, { isWrite: true })
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
model(name, schema) {
|
|
224
|
+
const collection = this.collection(name);
|
|
225
|
+
const validate = (document) => {
|
|
226
|
+
for (const [field, type] of Object.entries(schema)) {
|
|
227
|
+
const value = document[field];
|
|
228
|
+
if (value === void 0 || value === null) continue;
|
|
229
|
+
const expected = typeof type === "function" ? type.name?.toLowerCase() : void 0;
|
|
230
|
+
if (expected && expected !== "object" && typeof value !== expected) {
|
|
231
|
+
throw new PacificDBConfigError(`Field "${field}" expected ${expected}, got ${typeof value}.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
create: async (document) => {
|
|
237
|
+
validate(document);
|
|
238
|
+
return collection.insertOne(document);
|
|
239
|
+
},
|
|
240
|
+
findOne: (filter) => collection.findOne(filter),
|
|
241
|
+
find: (filter, options) => collection.find(filter, options),
|
|
242
|
+
updateOne: (filter, update) => collection.updateOne(filter, update),
|
|
243
|
+
deleteOne: (filter) => collection.deleteOne(filter)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async request(method, path, body, opts = {}) {
|
|
247
|
+
const { requireSession = true, isWrite = false } = opts;
|
|
248
|
+
if (requireSession && !this.sessionToken) {
|
|
249
|
+
throw new PacificDBAuthError("Call connect() before issuing database operations.");
|
|
250
|
+
}
|
|
251
|
+
const timeoutMs = isWrite ? this.writeTimeoutMs : this.readTimeoutMs;
|
|
252
|
+
const url = new URL(path, `${this.apiUrl}/`);
|
|
253
|
+
url.searchParams.set("projectId", this.connection.projectId);
|
|
254
|
+
url.searchParams.set("clusterId", this.connection.clusterId);
|
|
255
|
+
url.searchParams.set("databaseId", this.connection.databaseId);
|
|
256
|
+
let refreshed = false;
|
|
257
|
+
let lastError = null;
|
|
258
|
+
for (let attempt = 0; attempt <= this.retryLimit; attempt++) {
|
|
259
|
+
const requestId = randomRequestId();
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
262
|
+
try {
|
|
263
|
+
const response = await this.fetchImpl(url, {
|
|
264
|
+
method,
|
|
265
|
+
signal: controller.signal,
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
"x-request-id": requestId,
|
|
269
|
+
...this.sessionToken ? { authorization: `Bearer ${this.sessionToken}` } : {}
|
|
270
|
+
},
|
|
271
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
272
|
+
});
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
const json = await response.json().catch(() => ({}));
|
|
275
|
+
if (response.ok && json.success !== false) {
|
|
276
|
+
return json.data ?? json;
|
|
277
|
+
}
|
|
278
|
+
const errObj = typeof json.error === "object" ? json.error : void 0;
|
|
279
|
+
const code = errObj?.code || (typeof json.error === "string" ? json.error : "") || `HTTP_${response.status}`;
|
|
280
|
+
const message = errObj?.message || `PacificDB request failed (${code})`;
|
|
281
|
+
if (response.status === 401 && this.connected && !refreshed && requireSession) {
|
|
282
|
+
refreshed = true;
|
|
283
|
+
this.logger?.debug?.("pacificdb.session.refresh", { requestId });
|
|
284
|
+
await this.authenticate();
|
|
285
|
+
attempt--;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (response.status === 401) {
|
|
289
|
+
throw new PacificDBAuthError(message, requestId);
|
|
290
|
+
}
|
|
291
|
+
const retriable = RETRIABLE_CODES.has(code) || response.status === 429 || response.status === 503;
|
|
292
|
+
if (!retriable || attempt === this.retryLimit) {
|
|
293
|
+
if (response.status >= 500) throw new PacificDBServerError(message, code, response.status, requestId);
|
|
294
|
+
throw new PacificDBClientError(message, code, response.status, requestId);
|
|
295
|
+
}
|
|
296
|
+
const retryAfter = Number(response.headers.get("retry-after-ms") || json.retry_after_ms || 0);
|
|
297
|
+
lastError = new PacificDBServerError(message, code, response.status, requestId);
|
|
298
|
+
await this.backoff(attempt, retryAfter);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
if (error instanceof PacificDBClientError || error instanceof PacificDBAuthError || error instanceof PacificDBConfigError || error instanceof PacificDBServerError) {
|
|
302
|
+
if (error instanceof PacificDBServerError && attempt < this.retryLimit) {
|
|
303
|
+
lastError = error;
|
|
304
|
+
await this.backoff(attempt, 0);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
const isAbort = error.name === "AbortError";
|
|
310
|
+
lastError = isAbort ? new PacificDBTimeoutError(timeoutMs) : error;
|
|
311
|
+
this.logger?.warn?.("pacificdb.request.retry", {
|
|
312
|
+
attempt,
|
|
313
|
+
code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
|
|
314
|
+
target: this.redactedUri
|
|
315
|
+
});
|
|
316
|
+
if (attempt === this.retryLimit) break;
|
|
317
|
+
await this.backoff(attempt, 0);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw lastError || new PacificDBServerError("PacificDB request failed.", "UNKNOWN", 0);
|
|
321
|
+
}
|
|
322
|
+
backoff(attempt, retryAfterMs) {
|
|
323
|
+
const base = retryAfterMs || this.retryBackoffMs * Math.pow(2, attempt);
|
|
324
|
+
const jitter = Math.floor(Math.random() * this.retryBackoffMs);
|
|
325
|
+
return new Promise((resolve) => setTimeout(resolve, base + jitter));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
export {
|
|
329
|
+
PacificDBAuthError,
|
|
330
|
+
PacificDBClient,
|
|
331
|
+
PacificDBClientError,
|
|
332
|
+
PacificDBConfigError,
|
|
333
|
+
PacificDBError,
|
|
334
|
+
PacificDBServerError,
|
|
335
|
+
PacificDBTimeoutError,
|
|
336
|
+
buildConnectionString,
|
|
337
|
+
parseConnectionString,
|
|
338
|
+
redactConnectionString,
|
|
339
|
+
redactSecrets
|
|
340
|
+
};
|