@thadeu/ndb 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +306 -2
- package/dist/index.cjs +639 -0
- package/dist/index.d.cts +247 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +597 -0
- package/package.json +66 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var NdbError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(code, message, status = 0) {
|
|
6
|
+
super(message ? `ndb: ${code}: ${message}` : `ndb: ${code}`);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var UnauthorizedError = class extends NdbError {
|
|
13
|
+
};
|
|
14
|
+
var NotFoundError = class extends NdbError {
|
|
15
|
+
};
|
|
16
|
+
var VersionConflictError = class extends NdbError {
|
|
17
|
+
};
|
|
18
|
+
var DuplicateIdError = class extends NdbError {
|
|
19
|
+
};
|
|
20
|
+
var UniqueViolationError = class extends NdbError {
|
|
21
|
+
};
|
|
22
|
+
var InvalidQueryError = class extends NdbError {
|
|
23
|
+
};
|
|
24
|
+
var CursorError = class extends NdbError {
|
|
25
|
+
};
|
|
26
|
+
var WatchLaggedError = class extends NdbError {
|
|
27
|
+
};
|
|
28
|
+
var HistoryTruncatedError = class extends NdbError {
|
|
29
|
+
};
|
|
30
|
+
var TtlError = class extends NdbError {
|
|
31
|
+
};
|
|
32
|
+
var subclasses = {
|
|
33
|
+
unauthorized: UnauthorizedError,
|
|
34
|
+
not_found: NotFoundError,
|
|
35
|
+
unknown_collection: NotFoundError,
|
|
36
|
+
version_conflict: VersionConflictError,
|
|
37
|
+
duplicate_id: DuplicateIdError,
|
|
38
|
+
unique_violation: UniqueViolationError,
|
|
39
|
+
invalid_query: InvalidQueryError,
|
|
40
|
+
invalid_pipeline: InvalidQueryError,
|
|
41
|
+
cursor_invalid: CursorError,
|
|
42
|
+
cursor_mismatch: CursorError,
|
|
43
|
+
cursor_expired: CursorError,
|
|
44
|
+
watch_lagged: WatchLaggedError,
|
|
45
|
+
history_truncated: HistoryTruncatedError,
|
|
46
|
+
ttl_not_enabled: TtlError,
|
|
47
|
+
ttl_derived: TtlError
|
|
48
|
+
};
|
|
49
|
+
function makeError(code, message, status = 0) {
|
|
50
|
+
const Cls = subclasses[code] ?? NdbError;
|
|
51
|
+
return new Cls(code, message, status);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/where.ts
|
|
55
|
+
var supportedFlags = /* @__PURE__ */ new Set(["i", "m", "s"]);
|
|
56
|
+
function isPlain(v) {
|
|
57
|
+
if (typeof v !== "object" || v === null) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const proto = Object.getPrototypeOf(v);
|
|
61
|
+
return proto === Object.prototype || proto === null;
|
|
62
|
+
}
|
|
63
|
+
function regexParts(re) {
|
|
64
|
+
const bad = [...re.flags].filter((f) => !supportedFlags.has(f));
|
|
65
|
+
if (bad.length > 0) {
|
|
66
|
+
throw new InvalidQueryError(
|
|
67
|
+
"invalid_query",
|
|
68
|
+
`RegExp flag "${bad.join("")}" is not supported by $regex (use i, m or s): ${String(re)}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return re.flags === "" ? { $regex: re.source } : { $regex: re.source, $options: re.flags };
|
|
72
|
+
}
|
|
73
|
+
function operators(o) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [key, value] of Object.entries(o)) {
|
|
76
|
+
if (key === "$regex" && value instanceof RegExp) {
|
|
77
|
+
const parts = regexParts(value);
|
|
78
|
+
if (parts.$options !== void 0 && typeof o.$options === "string" && o.$options !== "") {
|
|
79
|
+
throw new InvalidQueryError("invalid_query", "use the RegExp flags or $options, not both");
|
|
80
|
+
}
|
|
81
|
+
out.$regex = parts.$regex;
|
|
82
|
+
if (parts.$options !== void 0) {
|
|
83
|
+
out.$options = parts.$options;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
out[key] = toWhere(value);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function toWhere(v) {
|
|
92
|
+
if (v instanceof RegExp) {
|
|
93
|
+
return regexParts(v);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(v)) {
|
|
96
|
+
return v.map(toWhere);
|
|
97
|
+
}
|
|
98
|
+
if (isPlain(v)) {
|
|
99
|
+
return operators(v);
|
|
100
|
+
}
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/query.ts
|
|
105
|
+
var Query = class _Query {
|
|
106
|
+
constructor(model, state = { clauses: [] }) {
|
|
107
|
+
this.model = model;
|
|
108
|
+
this.state = state;
|
|
109
|
+
}
|
|
110
|
+
model;
|
|
111
|
+
state;
|
|
112
|
+
nextCursor = "";
|
|
113
|
+
where(w) {
|
|
114
|
+
return this.with({ clauses: [...this.state.clauses, toWhere(w)] });
|
|
115
|
+
}
|
|
116
|
+
not(w) {
|
|
117
|
+
return this.with({ clauses: [...this.state.clauses, { $not: toWhere(w) }] });
|
|
118
|
+
}
|
|
119
|
+
or(...ws) {
|
|
120
|
+
return this.with({ clauses: [...this.state.clauses, { $or: ws.map(toWhere) }] });
|
|
121
|
+
}
|
|
122
|
+
sort(spec) {
|
|
123
|
+
return this.with({ sort: { ...this.state.sort, ...spec } });
|
|
124
|
+
}
|
|
125
|
+
limit(n) {
|
|
126
|
+
return this.with({ limit: n });
|
|
127
|
+
}
|
|
128
|
+
skip(n) {
|
|
129
|
+
return this.with({ skip: n });
|
|
130
|
+
}
|
|
131
|
+
page(n, size) {
|
|
132
|
+
return this.with({ skip: (n - 1) * size, limit: size });
|
|
133
|
+
}
|
|
134
|
+
after(token) {
|
|
135
|
+
return this.with({ after: token });
|
|
136
|
+
}
|
|
137
|
+
fields(...names) {
|
|
138
|
+
return this.with({ fields: names });
|
|
139
|
+
}
|
|
140
|
+
then(onfulfilled, onrejected) {
|
|
141
|
+
return this.all().then(onfulfilled, onrejected);
|
|
142
|
+
}
|
|
143
|
+
async all() {
|
|
144
|
+
const out = [];
|
|
145
|
+
for await (const row of this.rows(this.body())) {
|
|
146
|
+
out.push(row);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
async first() {
|
|
151
|
+
for await (const row of this.rows({ ...this.body(), limit: 1 })) {
|
|
152
|
+
return row;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
async count() {
|
|
157
|
+
await this.model.ready();
|
|
158
|
+
const body = { where: this.whereDoc(), count: true, limit: 1 };
|
|
159
|
+
const res = await this.model.db.transport.stream("POST", this.model.path("find"), body);
|
|
160
|
+
await res.body.dump();
|
|
161
|
+
return Number(res.header("x-ndb-total") ?? 0);
|
|
162
|
+
}
|
|
163
|
+
async *iterate() {
|
|
164
|
+
let body = this.body();
|
|
165
|
+
for (; ; ) {
|
|
166
|
+
yield* this.rows(body);
|
|
167
|
+
if (this.nextCursor === "") {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
body = { ...body, after: this.nextCursor, skip: void 0 };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async *rows(body) {
|
|
174
|
+
await this.model.ready();
|
|
175
|
+
const res = await this.model.db.transport.stream("POST", this.model.path("find"), body);
|
|
176
|
+
this.nextCursor = res.header("x-ndb-next") ?? "";
|
|
177
|
+
for await (const raw of res.rows()) {
|
|
178
|
+
yield this.model.decode(raw);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
body() {
|
|
182
|
+
const { fields, sort, limit, skip, after } = this.state;
|
|
183
|
+
return { where: this.whereDoc(), fields, sort, limit, skip, after, versions: true };
|
|
184
|
+
}
|
|
185
|
+
whereDoc() {
|
|
186
|
+
const merged = {};
|
|
187
|
+
for (const clause of this.state.clauses) {
|
|
188
|
+
for (const key of Object.keys(clause)) {
|
|
189
|
+
if (key in merged) {
|
|
190
|
+
return { $and: this.state.clauses };
|
|
191
|
+
}
|
|
192
|
+
merged[key] = clause[key];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return merged;
|
|
196
|
+
}
|
|
197
|
+
with(patch) {
|
|
198
|
+
return new _Query(this.model, { ...this.state, ...patch });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/watch.ts
|
|
203
|
+
async function* watchEvents(lines2, consumer) {
|
|
204
|
+
for await (const raw of lines2) {
|
|
205
|
+
const line = raw;
|
|
206
|
+
if (line.heartbeat) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (line.error) {
|
|
210
|
+
throw makeError(line.error.code, line.error.message);
|
|
211
|
+
}
|
|
212
|
+
yield { consumer, pos: line.pos, op: line.op, id: line.id, v: line.v, doc: line.doc };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/model.ts
|
|
217
|
+
var Model = class {
|
|
218
|
+
constructor(db, name, schema) {
|
|
219
|
+
this.db = db;
|
|
220
|
+
this.name = name;
|
|
221
|
+
this.schema = schema;
|
|
222
|
+
}
|
|
223
|
+
db;
|
|
224
|
+
name;
|
|
225
|
+
schema;
|
|
226
|
+
ensured;
|
|
227
|
+
get idField() {
|
|
228
|
+
return this.schema.id ?? "_id";
|
|
229
|
+
}
|
|
230
|
+
path(op) {
|
|
231
|
+
const base = `/v1/c/${encodeURIComponent(this.name)}`;
|
|
232
|
+
return op ? `${base}/${op}` : base;
|
|
233
|
+
}
|
|
234
|
+
ready() {
|
|
235
|
+
this.ensured ??= this.db.transport.request("PUT", this.path(), this.catalog()).catch((err) => {
|
|
236
|
+
this.ensured = void 0;
|
|
237
|
+
throw err;
|
|
238
|
+
});
|
|
239
|
+
return this.ensured;
|
|
240
|
+
}
|
|
241
|
+
async insert(docs, opts = {}) {
|
|
242
|
+
await this.ready();
|
|
243
|
+
const list = Array.isArray(docs) ? docs : [docs];
|
|
244
|
+
const out = await this.db.transport.request("POST", this.path("insert"), {
|
|
245
|
+
insert: list,
|
|
246
|
+
expire_in: opts.expireIn
|
|
247
|
+
});
|
|
248
|
+
return out.results;
|
|
249
|
+
}
|
|
250
|
+
create(docs, opts = {}) {
|
|
251
|
+
return this.insert(docs, opts);
|
|
252
|
+
}
|
|
253
|
+
async update(id, set, opts = {}) {
|
|
254
|
+
await this.ready();
|
|
255
|
+
const update = { id, set, unset: opts.unset, if_version: opts.ifVersion, expire_in: opts.expireIn };
|
|
256
|
+
return this.db.transport.request("POST", this.path("update"), { update });
|
|
257
|
+
}
|
|
258
|
+
async delete(id, opts = {}) {
|
|
259
|
+
await this.ready();
|
|
260
|
+
await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
|
|
261
|
+
}
|
|
262
|
+
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
263
|
+
// NotFoundError for a missing or expired document, and TtlError when the
|
|
264
|
+
// schema has no expires field.
|
|
265
|
+
expire(id, duration, opts = {}) {
|
|
266
|
+
return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
|
|
267
|
+
}
|
|
268
|
+
expireAt(id, at, opts = {}) {
|
|
269
|
+
return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
|
|
270
|
+
}
|
|
271
|
+
// Removes the expiry of a document (PERSIST in Redis).
|
|
272
|
+
async persist(id) {
|
|
273
|
+
await this.ready();
|
|
274
|
+
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
275
|
+
}
|
|
276
|
+
// The seconds left (rounded up), -1 when the document never expires, null
|
|
277
|
+
// when it is missing or expired.
|
|
278
|
+
async ttl(id) {
|
|
279
|
+
await this.ready();
|
|
280
|
+
try {
|
|
281
|
+
const out = await this.db.transport.request("GET", `${this.path("ttl")}/${encodeURIComponent(id)}`);
|
|
282
|
+
return out.ttl;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
if (err instanceof NotFoundError) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async setExpiry(body) {
|
|
291
|
+
await this.ready();
|
|
292
|
+
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
293
|
+
return { id: out.id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
294
|
+
}
|
|
295
|
+
find(id) {
|
|
296
|
+
return this.query().where({ [this.idField]: id }).first();
|
|
297
|
+
}
|
|
298
|
+
query() {
|
|
299
|
+
return new Query(this);
|
|
300
|
+
}
|
|
301
|
+
where(w) {
|
|
302
|
+
return this.query().where(w);
|
|
303
|
+
}
|
|
304
|
+
not(w) {
|
|
305
|
+
return this.query().not(w);
|
|
306
|
+
}
|
|
307
|
+
or(...ws) {
|
|
308
|
+
return this.query().or(...ws);
|
|
309
|
+
}
|
|
310
|
+
sort(spec) {
|
|
311
|
+
return this.query().sort(spec);
|
|
312
|
+
}
|
|
313
|
+
limit(n) {
|
|
314
|
+
return this.query().limit(n);
|
|
315
|
+
}
|
|
316
|
+
skip(n) {
|
|
317
|
+
return this.query().skip(n);
|
|
318
|
+
}
|
|
319
|
+
page(n, size) {
|
|
320
|
+
return this.query().page(n, size);
|
|
321
|
+
}
|
|
322
|
+
after(token) {
|
|
323
|
+
return this.query().after(token);
|
|
324
|
+
}
|
|
325
|
+
fields(...names) {
|
|
326
|
+
return this.query().fields(...names);
|
|
327
|
+
}
|
|
328
|
+
all() {
|
|
329
|
+
return this.query().all();
|
|
330
|
+
}
|
|
331
|
+
count() {
|
|
332
|
+
return this.query().count();
|
|
333
|
+
}
|
|
334
|
+
first() {
|
|
335
|
+
return this.query().first();
|
|
336
|
+
}
|
|
337
|
+
iterate() {
|
|
338
|
+
return this.query().iterate();
|
|
339
|
+
}
|
|
340
|
+
async aggregate(pipeline, opts = {}) {
|
|
341
|
+
await this.ready();
|
|
342
|
+
const res = await this.db.transport.stream("POST", this.path("aggregate"), { pipeline, ...opts });
|
|
343
|
+
const rows = [];
|
|
344
|
+
for await (const row of res.rows()) {
|
|
345
|
+
rows.push(row);
|
|
346
|
+
}
|
|
347
|
+
return { rows, next: res.header("x-ndb-next") ?? "", total: Number(res.header("x-ndb-total") ?? 0) };
|
|
348
|
+
}
|
|
349
|
+
async *watch(consumer, opts = {}) {
|
|
350
|
+
await this.ready();
|
|
351
|
+
const body = { consumer, from: opts.from, limit: opts.limit, where: opts.where === void 0 ? void 0 : toWhere(opts.where) };
|
|
352
|
+
const res = await this.db.transport.stream("POST", this.path("watch"), body, opts.signal);
|
|
353
|
+
for await (const ev of watchEvents(res.rows(), consumer)) {
|
|
354
|
+
yield { ...ev, doc: ev.doc && this.decode(ev.doc) };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async ack(event) {
|
|
358
|
+
await this.db.transport.request("POST", this.path("ack"), { consumer: event.consumer, pos: event.pos });
|
|
359
|
+
}
|
|
360
|
+
decode(raw) {
|
|
361
|
+
const out = { ...raw };
|
|
362
|
+
for (const [name, def] of Object.entries(this.schema.fields)) {
|
|
363
|
+
const v = out[name];
|
|
364
|
+
if (def.type === "date" && typeof v === "string") {
|
|
365
|
+
out[name] = new Date(v);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
catalog() {
|
|
371
|
+
const entries = Object.entries(this.schema.fields);
|
|
372
|
+
return {
|
|
373
|
+
id: this.schema.id,
|
|
374
|
+
time: this.schema.time,
|
|
375
|
+
expires_field: this.schema.expires,
|
|
376
|
+
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
377
|
+
unique: entries.filter(([, d]) => d.unique).map(([n]) => n)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/transport.ts
|
|
383
|
+
import { Pool } from "undici";
|
|
384
|
+
|
|
385
|
+
// src/ndjson.ts
|
|
386
|
+
async function* lines(body) {
|
|
387
|
+
let rest = "";
|
|
388
|
+
body.setEncoding("utf8");
|
|
389
|
+
for await (const chunk of body) {
|
|
390
|
+
rest += chunk;
|
|
391
|
+
let at = rest.indexOf("\n");
|
|
392
|
+
while (at >= 0) {
|
|
393
|
+
const line = rest.slice(0, at);
|
|
394
|
+
rest = rest.slice(at + 1);
|
|
395
|
+
if (line.trim() !== "") {
|
|
396
|
+
yield line;
|
|
397
|
+
}
|
|
398
|
+
at = rest.indexOf("\n");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (rest.trim() !== "") {
|
|
402
|
+
yield rest;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function* jsonRows(body) {
|
|
406
|
+
for await (const line of lines(body)) {
|
|
407
|
+
yield JSON.parse(line);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/transport.ts
|
|
412
|
+
var staleSocket = /* @__PURE__ */ new Set(["UND_ERR_SOCKET", "ECONNRESET", "EPIPE"]);
|
|
413
|
+
var notConnected = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENOENT"]);
|
|
414
|
+
var retryBaseMs = 50;
|
|
415
|
+
var retryCapMs = 1e3;
|
|
416
|
+
var defaultReadyTimeoutMs = 1e4;
|
|
417
|
+
var readOps = /\/(find|count|aggregate|get|stats)$/;
|
|
418
|
+
var Transport = class {
|
|
419
|
+
client;
|
|
420
|
+
token;
|
|
421
|
+
readyTimeout;
|
|
422
|
+
// addr: unix:///path or tcp://host:port. Default: env NDB_URL.
|
|
423
|
+
constructor(addr, opts = {}) {
|
|
424
|
+
const target = addr || process.env.NDB_URL || "";
|
|
425
|
+
const url = new URL(target);
|
|
426
|
+
this.token = opts.token || process.env.NDB_TOKEN || void 0;
|
|
427
|
+
this.readyTimeout = opts.readyTimeout ?? defaultReadyTimeoutMs;
|
|
428
|
+
if (url.protocol === "unix:" && url.pathname !== "") {
|
|
429
|
+
this.client = new Pool("http://ndb", { socketPath: decodeURIComponent(url.pathname), connections: 16 });
|
|
430
|
+
} else if (url.protocol === "tcp:" && url.host !== "") {
|
|
431
|
+
this.client = new Pool(`http://${url.host}`, { connections: 16 });
|
|
432
|
+
} else {
|
|
433
|
+
throw new TypeError(`ndb: address "${target}" must be unix:///path or tcp://host:port`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async stream(method, path, body, signal) {
|
|
437
|
+
const res = await this.withReadyRetry(method, path, signal, async () => {
|
|
438
|
+
const r = await this.send(method, path, body, signal);
|
|
439
|
+
if (r.statusCode >= 300) {
|
|
440
|
+
throw await apiError(r);
|
|
441
|
+
}
|
|
442
|
+
return r;
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
status: res.statusCode,
|
|
446
|
+
headers: res.headers,
|
|
447
|
+
body: res.body,
|
|
448
|
+
rows: () => jsonRows(res.body),
|
|
449
|
+
header: (name) => headerOf(res.headers, name)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async request(method, path, body) {
|
|
453
|
+
const res = await this.stream(method, path, body);
|
|
454
|
+
const text = await res.body.setEncoding("utf8").toArray();
|
|
455
|
+
return text.length > 0 ? JSON.parse(text.join("")) : void 0;
|
|
456
|
+
}
|
|
457
|
+
close() {
|
|
458
|
+
return this.client.close();
|
|
459
|
+
}
|
|
460
|
+
// Runs fn again while the daemon is not ready, with exponential backoff and
|
|
461
|
+
// jitter. It repeats only when the request certainly was not processed
|
|
462
|
+
// (not_ready, a failed connect), or for a read whose connection dropped.
|
|
463
|
+
// When the deadline passes, the last error is thrown as it was.
|
|
464
|
+
async withReadyRetry(method, path, signal, fn) {
|
|
465
|
+
const deadline = Date.now() + this.readyTimeout;
|
|
466
|
+
const idempotent = isIdempotent(method, path);
|
|
467
|
+
let delay = retryBaseMs;
|
|
468
|
+
for (; ; ) {
|
|
469
|
+
try {
|
|
470
|
+
return await fn();
|
|
471
|
+
} catch (err) {
|
|
472
|
+
const wait = delay / 2 + Math.random() * (delay / 2);
|
|
473
|
+
if (this.readyTimeout <= 0 || signal?.aborted || !retryable(err, idempotent) || Date.now() + wait > deadline) {
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
await sleep(wait, signal);
|
|
477
|
+
delay = Math.min(delay * 2, retryCapMs);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
headers() {
|
|
482
|
+
const headers = { "content-type": "application/json" };
|
|
483
|
+
if (this.token) {
|
|
484
|
+
headers.authorization = `Bearer ${this.token}`;
|
|
485
|
+
}
|
|
486
|
+
return headers;
|
|
487
|
+
}
|
|
488
|
+
async send(method, path, body, signal) {
|
|
489
|
+
const opts = {
|
|
490
|
+
method,
|
|
491
|
+
path,
|
|
492
|
+
signal,
|
|
493
|
+
headers: this.headers(),
|
|
494
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
return await this.client.request(opts);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
if (!isIdempotent(method, path) || !staleSocket.has(err.code ?? "") || signal?.aborted) {
|
|
500
|
+
throw err;
|
|
501
|
+
}
|
|
502
|
+
return await this.client.request(opts);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function isIdempotent(method, path) {
|
|
507
|
+
return method === "GET" || readOps.test(path.split("?")[0] ?? path);
|
|
508
|
+
}
|
|
509
|
+
function errorCodes(err) {
|
|
510
|
+
const e = err;
|
|
511
|
+
const nested = [...e.errors ?? [], ...e.cause ? [e.cause] : []];
|
|
512
|
+
return [e.code ?? "", ...nested.flatMap(errorCodes)];
|
|
513
|
+
}
|
|
514
|
+
function retryable(err, idempotent) {
|
|
515
|
+
if (err instanceof NdbError) {
|
|
516
|
+
return err.code === "not_ready";
|
|
517
|
+
}
|
|
518
|
+
const codes = errorCodes(err);
|
|
519
|
+
if (codes.some((c) => notConnected.has(c))) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
return idempotent && codes.some((c) => staleSocket.has(c));
|
|
523
|
+
}
|
|
524
|
+
function sleep(ms, signal) {
|
|
525
|
+
return new Promise((resolve) => {
|
|
526
|
+
const done = () => {
|
|
527
|
+
clearTimeout(timer);
|
|
528
|
+
signal?.removeEventListener("abort", done);
|
|
529
|
+
resolve();
|
|
530
|
+
};
|
|
531
|
+
const timer = setTimeout(done, ms);
|
|
532
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function headerOf(headers, name) {
|
|
536
|
+
const v = headers[name.toLowerCase()];
|
|
537
|
+
return Array.isArray(v) ? v[0] : v;
|
|
538
|
+
}
|
|
539
|
+
async function apiError(res) {
|
|
540
|
+
const text = (await res.body.setEncoding("utf8").toArray()).join("");
|
|
541
|
+
try {
|
|
542
|
+
const { error, status } = JSON.parse(text);
|
|
543
|
+
if (res.statusCode === 503 && status === "recovering") {
|
|
544
|
+
return makeError("not_ready", "ndb is recovering", 503);
|
|
545
|
+
}
|
|
546
|
+
if (error?.code) {
|
|
547
|
+
return makeError(error.code, error.message ?? "", res.statusCode);
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
return makeError("http_status", text.trim(), res.statusCode);
|
|
551
|
+
}
|
|
552
|
+
return makeError("http_status", text.trim(), res.statusCode);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/ndb.ts
|
|
556
|
+
var Ndb = class {
|
|
557
|
+
transport;
|
|
558
|
+
constructor(addr, opts) {
|
|
559
|
+
this.transport = new Transport(addr, opts);
|
|
560
|
+
}
|
|
561
|
+
model(name, schema) {
|
|
562
|
+
return new Model(this, name, schema);
|
|
563
|
+
}
|
|
564
|
+
async health() {
|
|
565
|
+
await this.transport.request("GET", "/health");
|
|
566
|
+
}
|
|
567
|
+
request(method, path, body) {
|
|
568
|
+
return this.transport.request(method, path, body);
|
|
569
|
+
}
|
|
570
|
+
stream(...args) {
|
|
571
|
+
return this.transport.stream(...args);
|
|
572
|
+
}
|
|
573
|
+
close() {
|
|
574
|
+
return this.transport.close();
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
function connect(addr, opts) {
|
|
578
|
+
return new Ndb(addr, opts);
|
|
579
|
+
}
|
|
580
|
+
export {
|
|
581
|
+
CursorError,
|
|
582
|
+
DuplicateIdError,
|
|
583
|
+
HistoryTruncatedError,
|
|
584
|
+
InvalidQueryError,
|
|
585
|
+
Model,
|
|
586
|
+
Ndb,
|
|
587
|
+
NdbError,
|
|
588
|
+
NotFoundError,
|
|
589
|
+
Query,
|
|
590
|
+
TtlError,
|
|
591
|
+
UnauthorizedError,
|
|
592
|
+
UniqueViolationError,
|
|
593
|
+
VersionConflictError,
|
|
594
|
+
WatchLaggedError,
|
|
595
|
+
connect,
|
|
596
|
+
makeError
|
|
597
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thadeu/ndb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "TypeScript client for the ndb daemon",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ndb",
|
|
7
|
+
"ndjson",
|
|
8
|
+
"database",
|
|
9
|
+
"client"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/thadeu/ndb#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/thadeu/ndb/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/thadeu/ndb.git",
|
|
18
|
+
"directory": "sdk/ts"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/index.d.cts",
|
|
35
|
+
"default": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"lint": "eslint .",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"undici": "^8.12.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
55
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
56
|
+
"@types/node": "^26.6.6",
|
|
57
|
+
"eslint": "^10.14.0",
|
|
58
|
+
"tsup": "^9.0.0",
|
|
59
|
+
"typescript": "^6.0.3",
|
|
60
|
+
"typescript-eslint": "^8.72.1",
|
|
61
|
+
"vitest": "^5.1.0"
|
|
62
|
+
},
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public",
|
|
66
|
+
"provenance": true
|
|
67
|
+
}
|
|
68
|
+
}
|