@valon-technologies/gestalt 0.0.1-alpha.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/README.md +160 -0
- package/gen/v1/auth_pb.ts +212 -0
- package/gen/v1/cache_pb.ts +357 -0
- package/gen/v1/datastore_pb.ts +922 -0
- package/gen/v1/plugin_pb.ts +772 -0
- package/gen/v1/runtime_pb.ts +216 -0
- package/gen/v1/s3_pb.ts +640 -0
- package/gen/v1/secrets_pb.ts +63 -0
- package/package.json +55 -0
- package/src/api.ts +98 -0
- package/src/auth.ts +103 -0
- package/src/build.ts +181 -0
- package/src/cache.ts +304 -0
- package/src/catalog.ts +188 -0
- package/src/index.ts +182 -0
- package/src/indexeddb.ts +740 -0
- package/src/plugin.ts +402 -0
- package/src/provider.ts +133 -0
- package/src/runtime.ts +871 -0
- package/src/s3.ts +1128 -0
- package/src/schema.ts +219 -0
- package/src/secrets.ts +36 -0
- package/src/target.ts +192 -0
- package/tsconfig.json +22 -0
package/src/indexeddb.ts
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { createClient, type Client } from "@connectrpc/connect";
|
|
2
|
+
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
3
|
+
import {
|
|
4
|
+
IndexedDB as IndexedDBService,
|
|
5
|
+
CursorDirection as ProtoCursorDirection,
|
|
6
|
+
} from "../gen/v1/datastore_pb";
|
|
7
|
+
|
|
8
|
+
const ENV_INDEXEDDB_SOCKET = "GESTALT_INDEXEDDB_SOCKET";
|
|
9
|
+
|
|
10
|
+
export function indexedDBSocketEnv(name?: string): string {
|
|
11
|
+
const trimmed = name?.trim() ?? "";
|
|
12
|
+
if (!trimmed) return ENV_INDEXEDDB_SOCKET;
|
|
13
|
+
return `${ENV_INDEXEDDB_SOCKET}_${trimmed.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class AsyncQueue<T> implements AsyncIterable<T> {
|
|
17
|
+
private queue: T[] = [];
|
|
18
|
+
private waiting: ((result: IteratorResult<T>) => void) | null = null;
|
|
19
|
+
private closed = false;
|
|
20
|
+
|
|
21
|
+
push(value: T) {
|
|
22
|
+
if (this.waiting) {
|
|
23
|
+
const resolve = this.waiting;
|
|
24
|
+
this.waiting = null;
|
|
25
|
+
resolve({ value, done: false });
|
|
26
|
+
} else {
|
|
27
|
+
this.queue.push(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
end() {
|
|
32
|
+
this.closed = true;
|
|
33
|
+
if (this.waiting) {
|
|
34
|
+
const resolve = this.waiting;
|
|
35
|
+
this.waiting = null;
|
|
36
|
+
resolve({ value: undefined as any, done: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[Symbol.asyncIterator]() {
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async next(): Promise<IteratorResult<T>> {
|
|
45
|
+
if (this.queue.length > 0) {
|
|
46
|
+
return { value: this.queue.shift()!, done: false };
|
|
47
|
+
}
|
|
48
|
+
if (this.closed) {
|
|
49
|
+
return { value: undefined as any, done: true };
|
|
50
|
+
}
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
this.waiting = resolve;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async throw(err: unknown): Promise<IteratorResult<T>> {
|
|
57
|
+
this.closed = true;
|
|
58
|
+
if (this.waiting) {
|
|
59
|
+
const resolve = this.waiting;
|
|
60
|
+
this.waiting = null;
|
|
61
|
+
resolve({ value: undefined as any, done: true });
|
|
62
|
+
}
|
|
63
|
+
return { value: undefined as any, done: true };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export enum CursorDirection {
|
|
68
|
+
Next = 0,
|
|
69
|
+
NextUnique = 1,
|
|
70
|
+
Prev = 2,
|
|
71
|
+
PrevUnique = 3,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const CURSOR_DIRECTION_TO_PROTO: { [K in CursorDirection]: ProtoCursorDirection } = {
|
|
75
|
+
[CursorDirection.Next]: ProtoCursorDirection.CURSOR_NEXT,
|
|
76
|
+
[CursorDirection.NextUnique]: ProtoCursorDirection.CURSOR_NEXT_UNIQUE,
|
|
77
|
+
[CursorDirection.Prev]: ProtoCursorDirection.CURSOR_PREV,
|
|
78
|
+
[CursorDirection.PrevUnique]: ProtoCursorDirection.CURSOR_PREV_UNIQUE,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface OpenCursorOptions {
|
|
82
|
+
range?: KeyRange;
|
|
83
|
+
direction?: CursorDirection;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class Cursor {
|
|
87
|
+
private sendQueue: AsyncQueue<any>;
|
|
88
|
+
private responseIterator: AsyncIterator<any>;
|
|
89
|
+
private _key: unknown = undefined;
|
|
90
|
+
private _primaryKey: string = "";
|
|
91
|
+
private _value: Record | undefined = undefined;
|
|
92
|
+
private _done = false;
|
|
93
|
+
|
|
94
|
+
private _indexCursor = false;
|
|
95
|
+
|
|
96
|
+
private constructor(
|
|
97
|
+
sendQueue: AsyncQueue<any>,
|
|
98
|
+
responseIterator: AsyncIterator<any>,
|
|
99
|
+
indexCursor: boolean = false,
|
|
100
|
+
) {
|
|
101
|
+
this.sendQueue = sendQueue;
|
|
102
|
+
this.responseIterator = responseIterator;
|
|
103
|
+
this._indexCursor = indexCursor;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static async open(
|
|
107
|
+
client: Client<typeof IndexedDBService>,
|
|
108
|
+
store: string,
|
|
109
|
+
options?: OpenCursorOptions & { keysOnly?: boolean; index?: string; indexValues?: unknown[] },
|
|
110
|
+
): Promise<Cursor | null> {
|
|
111
|
+
const sendQueue = new AsyncQueue<any>();
|
|
112
|
+
const direction = options?.direction ?? CursorDirection.Next;
|
|
113
|
+
|
|
114
|
+
sendQueue.push({
|
|
115
|
+
msg: {
|
|
116
|
+
case: "open" as const,
|
|
117
|
+
value: {
|
|
118
|
+
store,
|
|
119
|
+
range: options?.range ? toProtoKeyRange(options.range) : undefined,
|
|
120
|
+
direction: CURSOR_DIRECTION_TO_PROTO[direction],
|
|
121
|
+
keysOnly: options?.keysOnly ?? false,
|
|
122
|
+
index: options?.index ?? "",
|
|
123
|
+
values: (options?.indexValues ?? []).map(toProtoTypedValue),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const responses = client.openCursor(sendQueue);
|
|
129
|
+
const responseIterator = responses[Symbol.asyncIterator]();
|
|
130
|
+
|
|
131
|
+
const isIndex = !!(options?.index);
|
|
132
|
+
const cursor = new Cursor(sendQueue, responseIterator, isIndex);
|
|
133
|
+
// Read the open ack to surface creation errors synchronously.
|
|
134
|
+
await cursor.recvOpenAck();
|
|
135
|
+
return cursor;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get key(): unknown {
|
|
139
|
+
return this._key;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get primaryKey(): string {
|
|
143
|
+
return this._primaryKey;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get value(): Record | undefined {
|
|
147
|
+
return this._value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get done(): boolean {
|
|
151
|
+
return this._done;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async continue(): Promise<boolean> {
|
|
155
|
+
this.sendQueue.push({
|
|
156
|
+
msg: { case: "command" as const, value: { command: { case: "next" as const, value: true } } },
|
|
157
|
+
});
|
|
158
|
+
return this.pull();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async continueToKey(key: unknown): Promise<boolean> {
|
|
162
|
+
this.sendQueue.push({
|
|
163
|
+
msg: {
|
|
164
|
+
case: "command" as const,
|
|
165
|
+
value: {
|
|
166
|
+
command: {
|
|
167
|
+
case: "continueToKey" as const,
|
|
168
|
+
value: { key: toProtoCursorKey(key, this._indexCursor) },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
return this.pull();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async advance(count: number): Promise<boolean> {
|
|
177
|
+
this.sendQueue.push({
|
|
178
|
+
msg: {
|
|
179
|
+
case: "command" as const,
|
|
180
|
+
value: { command: { case: "advance" as const, value: count } },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return this.pull();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async delete(): Promise<void> {
|
|
187
|
+
if (this._done) throw new NotFoundError("cursor is exhausted");
|
|
188
|
+
this.sendQueue.push({
|
|
189
|
+
msg: {
|
|
190
|
+
case: "command" as const,
|
|
191
|
+
value: { command: { case: "delete" as const, value: true } },
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
await this.recvMutationAck();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async update(record: Record): Promise<void> {
|
|
198
|
+
if (this._done) throw new NotFoundError("cursor is exhausted");
|
|
199
|
+
this.sendQueue.push({
|
|
200
|
+
msg: {
|
|
201
|
+
case: "command" as const,
|
|
202
|
+
value: { command: { case: "update" as const, value: toProtoRecord(record) } },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
await this.recvMutationAck();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
close(): void {
|
|
209
|
+
this.sendQueue.push({
|
|
210
|
+
msg: {
|
|
211
|
+
case: "command" as const,
|
|
212
|
+
value: { command: { case: "close" as const, value: true } },
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
this.sendQueue.end();
|
|
216
|
+
this._done = true;
|
|
217
|
+
this._key = undefined;
|
|
218
|
+
this._primaryKey = "";
|
|
219
|
+
this._value = undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private resetState(): void {
|
|
223
|
+
this._done = true;
|
|
224
|
+
this._key = undefined;
|
|
225
|
+
this._primaryKey = "";
|
|
226
|
+
this._value = undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private mapCursorError(err: any): never {
|
|
230
|
+
if (err?.code === 5) throw new NotFoundError(err.message);
|
|
231
|
+
if (err?.code === 6) throw new AlreadyExistsError(err.message);
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async recvOpenAck(): Promise<void> {
|
|
236
|
+
try {
|
|
237
|
+
const { value: resp, done } = await this.responseIterator.next();
|
|
238
|
+
if (done || !resp) {
|
|
239
|
+
this.sendQueue.end();
|
|
240
|
+
this.resetState();
|
|
241
|
+
throw new Error("cursor stream ended during open");
|
|
242
|
+
}
|
|
243
|
+
if (resp.result?.case !== "done" || resp.result.value !== false) {
|
|
244
|
+
this.sendQueue.end();
|
|
245
|
+
this.resetState();
|
|
246
|
+
throw new Error("unexpected cursor open ack");
|
|
247
|
+
}
|
|
248
|
+
} catch (err: any) {
|
|
249
|
+
this.mapCursorError(err);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async recvMutationAck(): Promise<void> {
|
|
254
|
+
try {
|
|
255
|
+
const { value: resp, done } = await this.responseIterator.next();
|
|
256
|
+
if (done || !resp) {
|
|
257
|
+
this.sendQueue.end();
|
|
258
|
+
this.resetState();
|
|
259
|
+
throw new Error("cursor stream ended during mutation");
|
|
260
|
+
}
|
|
261
|
+
if (resp.result?.case === "entry") {
|
|
262
|
+
this.refreshFromEntry(resp.result.value);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (resp.result?.case === "done") return;
|
|
266
|
+
throw new Error("unexpected cursor mutation ack");
|
|
267
|
+
} catch (err: any) {
|
|
268
|
+
this.mapCursorError(err);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async pull(): Promise<boolean> {
|
|
273
|
+
let resp: any;
|
|
274
|
+
let done: boolean | undefined;
|
|
275
|
+
try {
|
|
276
|
+
({ value: resp, done } = await this.responseIterator.next());
|
|
277
|
+
} catch (err: any) {
|
|
278
|
+
this.mapCursorError(err);
|
|
279
|
+
}
|
|
280
|
+
if (done || !resp) {
|
|
281
|
+
this.resetState();
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (resp.result?.case === "done" && resp.result.value === true) {
|
|
285
|
+
this.resetState();
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
if (resp.result?.case === "done") {
|
|
289
|
+
// done=false is an ack (e.g. open ack), not exhaustion.
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
if (resp.result?.case === "entry") {
|
|
293
|
+
this.refreshFromEntry(resp.result.value);
|
|
294
|
+
this._done = false;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private refreshFromEntry(entry: any): void {
|
|
301
|
+
if (!this._indexCursor && entry.key.length === 1) {
|
|
302
|
+
this._key = fromProtoKeyValue(entry.key[0]);
|
|
303
|
+
} else if (entry.key.length > 0) {
|
|
304
|
+
this._key = entry.key.map(fromProtoKeyValue);
|
|
305
|
+
} else {
|
|
306
|
+
this._key = undefined;
|
|
307
|
+
}
|
|
308
|
+
this._primaryKey = entry.primaryKey;
|
|
309
|
+
this._value = entry.record ? fromProtoRecord(entry.record) : undefined;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export class NotFoundError extends Error {
|
|
314
|
+
constructor(message?: string) {
|
|
315
|
+
super(message ?? "not found");
|
|
316
|
+
this.name = "NotFoundError";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export class AlreadyExistsError extends Error {
|
|
321
|
+
constructor(message?: string) {
|
|
322
|
+
super(message ?? "already exists");
|
|
323
|
+
this.name = "AlreadyExistsError";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export type Record = { [key: string]: unknown };
|
|
328
|
+
|
|
329
|
+
export interface KeyRange {
|
|
330
|
+
lower?: unknown;
|
|
331
|
+
upper?: unknown;
|
|
332
|
+
lowerOpen?: boolean;
|
|
333
|
+
upperOpen?: boolean;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface IndexSchema {
|
|
337
|
+
name: string;
|
|
338
|
+
keyPath: string[];
|
|
339
|
+
unique?: boolean;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export enum ColumnType {
|
|
343
|
+
String = 0,
|
|
344
|
+
Int = 1,
|
|
345
|
+
Float = 2,
|
|
346
|
+
Bool = 3,
|
|
347
|
+
Time = 4,
|
|
348
|
+
Bytes = 5,
|
|
349
|
+
JSON = 6,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface ColumnSchema {
|
|
353
|
+
name: string;
|
|
354
|
+
type?: ColumnType;
|
|
355
|
+
primaryKey?: boolean;
|
|
356
|
+
notNull?: boolean;
|
|
357
|
+
unique?: boolean;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface ObjectStoreSchema {
|
|
361
|
+
indexes?: IndexSchema[];
|
|
362
|
+
columns?: ColumnSchema[];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export class IndexedDB {
|
|
366
|
+
private client: Client<typeof IndexedDBService>;
|
|
367
|
+
|
|
368
|
+
constructor(name?: string) {
|
|
369
|
+
const envName = indexedDBSocketEnv(name);
|
|
370
|
+
const socketPath = process.env[envName];
|
|
371
|
+
if (!socketPath) {
|
|
372
|
+
throw new Error(`${envName} is not set`);
|
|
373
|
+
}
|
|
374
|
+
const transport = createGrpcTransport({
|
|
375
|
+
baseUrl: `http://localhost`,
|
|
376
|
+
nodeOptions: { path: socketPath },
|
|
377
|
+
});
|
|
378
|
+
this.client = createClient(IndexedDBService, transport);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async createObjectStore(name: string, schema?: ObjectStoreSchema): Promise<void> {
|
|
382
|
+
await this.client.createObjectStore({
|
|
383
|
+
name,
|
|
384
|
+
schema: {
|
|
385
|
+
indexes: (schema?.indexes ?? []).map((idx) => ({
|
|
386
|
+
name: idx.name,
|
|
387
|
+
keyPath: idx.keyPath,
|
|
388
|
+
unique: idx.unique ?? false,
|
|
389
|
+
})),
|
|
390
|
+
columns: (schema?.columns ?? []).map((col) => ({
|
|
391
|
+
name: col.name,
|
|
392
|
+
type: col.type ?? ColumnType.String,
|
|
393
|
+
primaryKey: col.primaryKey ?? false,
|
|
394
|
+
notNull: col.notNull ?? false,
|
|
395
|
+
unique: col.unique ?? false,
|
|
396
|
+
})),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async deleteObjectStore(name: string): Promise<void> {
|
|
402
|
+
await this.client.deleteObjectStore({ name });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
objectStore(name: string): ObjectStore {
|
|
406
|
+
return new ObjectStore(this.client, name);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export class ObjectStore {
|
|
411
|
+
constructor(
|
|
412
|
+
private client: Client<typeof IndexedDBService>,
|
|
413
|
+
private store: string,
|
|
414
|
+
) {}
|
|
415
|
+
|
|
416
|
+
async get(id: string): Promise<Record> {
|
|
417
|
+
const resp = await rpc(() => this.client.get({ store: this.store, id }));
|
|
418
|
+
return fromProtoRecord(resp.record);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async getKey(id: string): Promise<string> {
|
|
422
|
+
const resp = await rpc(() => this.client.getKey({ store: this.store, id }));
|
|
423
|
+
return resp.key;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async add(record: Record): Promise<void> {
|
|
427
|
+
await rpc(() => this.client.add({ store: this.store, record: toProtoRecord(record) }));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async put(record: Record): Promise<void> {
|
|
431
|
+
await rpc(() => this.client.put({ store: this.store, record: toProtoRecord(record) }));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async delete(id: string): Promise<void> {
|
|
435
|
+
await rpc(() => this.client.delete({ store: this.store, id }));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async clear(): Promise<void> {
|
|
439
|
+
await this.client.clear({ store: this.store });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async getAll(keyRange?: KeyRange): Promise<Record[]> {
|
|
443
|
+
const resp = await this.client.getAll({
|
|
444
|
+
store: this.store,
|
|
445
|
+
range: keyRange ? toProtoKeyRange(keyRange) : undefined,
|
|
446
|
+
});
|
|
447
|
+
return resp.records.map((r) => fromProtoRecord(r));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async getAllKeys(keyRange?: KeyRange): Promise<string[]> {
|
|
451
|
+
const resp = await this.client.getAllKeys({
|
|
452
|
+
store: this.store,
|
|
453
|
+
range: keyRange ? toProtoKeyRange(keyRange) : undefined,
|
|
454
|
+
});
|
|
455
|
+
return resp.keys;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async count(keyRange?: KeyRange): Promise<number> {
|
|
459
|
+
const resp = await this.client.count({
|
|
460
|
+
store: this.store,
|
|
461
|
+
range: keyRange ? toProtoKeyRange(keyRange) : undefined,
|
|
462
|
+
});
|
|
463
|
+
return Number(resp.count);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async deleteRange(keyRange: KeyRange): Promise<number> {
|
|
467
|
+
const resp = await this.client.deleteRange({
|
|
468
|
+
store: this.store,
|
|
469
|
+
range: toProtoKeyRange(keyRange),
|
|
470
|
+
});
|
|
471
|
+
return Number(resp.deleted);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async openCursor(options?: OpenCursorOptions): Promise<Cursor | null> {
|
|
475
|
+
return Cursor.open(this.client, this.store, options);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async openKeyCursor(options?: OpenCursorOptions): Promise<Cursor | null> {
|
|
479
|
+
return Cursor.open(this.client, this.store, { ...options, keysOnly: true });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
index(name: string): Index {
|
|
483
|
+
return new Index(this.client, this.store, name);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export class Index {
|
|
488
|
+
constructor(
|
|
489
|
+
private client: Client<typeof IndexedDBService>,
|
|
490
|
+
private store: string,
|
|
491
|
+
private indexName: string,
|
|
492
|
+
) {}
|
|
493
|
+
|
|
494
|
+
async get(...values: unknown[]): Promise<Record> {
|
|
495
|
+
const resp = await rpc(() =>
|
|
496
|
+
this.client.indexGet({
|
|
497
|
+
store: this.store,
|
|
498
|
+
index: this.indexName,
|
|
499
|
+
values: values.map(toProtoTypedValue),
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
return fromProtoRecord(resp.record);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async getKey(...values: unknown[]): Promise<string> {
|
|
506
|
+
const resp = await rpc(() =>
|
|
507
|
+
this.client.indexGetKey({
|
|
508
|
+
store: this.store,
|
|
509
|
+
index: this.indexName,
|
|
510
|
+
values: values.map(toProtoTypedValue),
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
return resp.key;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async getAll(keyRange?: KeyRange, ...values: unknown[]): Promise<Record[]> {
|
|
517
|
+
const resp = await this.client.indexGetAll({
|
|
518
|
+
store: this.store,
|
|
519
|
+
index: this.indexName,
|
|
520
|
+
values: values.map(toProtoTypedValue),
|
|
521
|
+
range: keyRange ? toProtoKeyRange(keyRange) : undefined,
|
|
522
|
+
});
|
|
523
|
+
return resp.records.map((r) => fromProtoRecord(r));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async getAllKeys(keyRange?: KeyRange, ...values: unknown[]): Promise<string[]> {
|
|
527
|
+
const resp = await this.client.indexGetAllKeys({
|
|
528
|
+
store: this.store,
|
|
529
|
+
index: this.indexName,
|
|
530
|
+
values: values.map(toProtoTypedValue),
|
|
531
|
+
range: keyRange ? toProtoKeyRange(keyRange) : undefined,
|
|
532
|
+
});
|
|
533
|
+
return resp.keys;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async count(keyRange?: KeyRange, ...values: unknown[]): Promise<number> {
|
|
537
|
+
const resp = await this.client.indexCount({
|
|
538
|
+
store: this.store,
|
|
539
|
+
index: this.indexName,
|
|
540
|
+
values: values.map(toProtoTypedValue),
|
|
541
|
+
range: keyRange ? toProtoKeyRange(keyRange) : undefined,
|
|
542
|
+
});
|
|
543
|
+
return Number(resp.count);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async delete(...values: unknown[]): Promise<number> {
|
|
547
|
+
const resp = await this.client.indexDelete({
|
|
548
|
+
store: this.store,
|
|
549
|
+
index: this.indexName,
|
|
550
|
+
values: values.map(toProtoTypedValue),
|
|
551
|
+
});
|
|
552
|
+
return Number(resp.deleted);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async openCursor(options?: OpenCursorOptions, ...values: unknown[]): Promise<Cursor | null> {
|
|
556
|
+
return Cursor.open(this.client, this.store, {
|
|
557
|
+
...options,
|
|
558
|
+
index: this.indexName,
|
|
559
|
+
indexValues: values,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async openKeyCursor(options?: OpenCursorOptions, ...values: unknown[]): Promise<Cursor | null> {
|
|
564
|
+
return Cursor.open(this.client, this.store, {
|
|
565
|
+
...options,
|
|
566
|
+
keysOnly: true,
|
|
567
|
+
index: this.indexName,
|
|
568
|
+
indexValues: values,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function fromProtoKeyValue(kv: any): unknown {
|
|
574
|
+
if (kv.kind?.case === "scalar") return fromProtoTypedValue(kv.kind.value);
|
|
575
|
+
if (kv.kind?.case === "array") return kv.kind.value.elements.map(fromProtoKeyValue);
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function toProtoKeyValue(v: unknown): any {
|
|
580
|
+
if (Array.isArray(v)) {
|
|
581
|
+
return { kind: { case: "array" as const, value: { elements: v.map(toProtoKeyValue) } } };
|
|
582
|
+
}
|
|
583
|
+
return { kind: { case: "scalar" as const, value: toProtoTypedValue(v) } };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function toProtoCursorKey(key: unknown, indexCursor: boolean): any[] {
|
|
587
|
+
if (indexCursor && Array.isArray(key)) {
|
|
588
|
+
return key.map(toProtoKeyValue);
|
|
589
|
+
}
|
|
590
|
+
return [toProtoKeyValue(key)];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function toProtoRecord(record: Record): any {
|
|
594
|
+
const fields: { [key: string]: unknown } = {};
|
|
595
|
+
for (const [key, value] of Object.entries(record)) {
|
|
596
|
+
fields[key] = toProtoTypedValue(value);
|
|
597
|
+
}
|
|
598
|
+
return { fields };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function fromProtoRecord(record: any): Record {
|
|
602
|
+
const fields = record?.fields ?? {};
|
|
603
|
+
const out: Record = {};
|
|
604
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
605
|
+
out[key] = fromProtoTypedValue(value);
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function toProtoTypedValue(v: unknown): any {
|
|
611
|
+
if (v === null || v === undefined) return { kind: { case: "nullValue", value: 0 } };
|
|
612
|
+
if (typeof v === "boolean") return { kind: { case: "boolValue", value: v } };
|
|
613
|
+
if (typeof v === "bigint") return { kind: { case: "intValue", value: v } };
|
|
614
|
+
if (typeof v === "number") {
|
|
615
|
+
if (Number.isInteger(v) && Number.isSafeInteger(v)) {
|
|
616
|
+
return { kind: { case: "intValue", value: BigInt(v) } };
|
|
617
|
+
}
|
|
618
|
+
return { kind: { case: "floatValue", value: v } };
|
|
619
|
+
}
|
|
620
|
+
if (typeof v === "string") return { kind: { case: "stringValue", value: v } };
|
|
621
|
+
if (v instanceof Date) return { kind: { case: "timeValue", value: toProtoTimestamp(v) } };
|
|
622
|
+
if (v instanceof Uint8Array) return { kind: { case: "bytesValue", value: v } };
|
|
623
|
+
if (v instanceof ArrayBuffer) return { kind: { case: "bytesValue", value: new Uint8Array(v) } };
|
|
624
|
+
return { kind: { case: "jsonValue", value: toProtoJsonValue(v) } };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function fromProtoTypedValue(v: any): unknown {
|
|
628
|
+
switch (v?.kind?.case) {
|
|
629
|
+
case undefined:
|
|
630
|
+
case "nullValue":
|
|
631
|
+
return null;
|
|
632
|
+
case "stringValue":
|
|
633
|
+
return v.kind.value;
|
|
634
|
+
case "intValue":
|
|
635
|
+
return toJsInt(v.kind.value);
|
|
636
|
+
case "floatValue":
|
|
637
|
+
return v.kind.value;
|
|
638
|
+
case "boolValue":
|
|
639
|
+
return v.kind.value;
|
|
640
|
+
case "timeValue":
|
|
641
|
+
return fromProtoTimestamp(v.kind.value);
|
|
642
|
+
case "bytesValue":
|
|
643
|
+
return new Uint8Array(v.kind.value);
|
|
644
|
+
case "jsonValue":
|
|
645
|
+
return fromProtoJsonValue(v.kind.value);
|
|
646
|
+
default:
|
|
647
|
+
throw new Error(`unsupported typed value kind: ${String(v?.kind?.case)}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function toProtoKeyRange(kr: KeyRange): any {
|
|
652
|
+
return {
|
|
653
|
+
lower: kr.lower !== undefined ? toProtoTypedValue(kr.lower) : undefined,
|
|
654
|
+
upper: kr.upper !== undefined ? toProtoTypedValue(kr.upper) : undefined,
|
|
655
|
+
lowerOpen: kr.lowerOpen ?? false,
|
|
656
|
+
upperOpen: kr.upperOpen ?? false,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function toProtoTimestamp(value: Date): any {
|
|
661
|
+
const millis = value.getTime();
|
|
662
|
+
const seconds = Math.trunc(millis / 1000);
|
|
663
|
+
const nanos = Math.trunc((millis % 1000) * 1_000_000);
|
|
664
|
+
return { seconds: BigInt(seconds), nanos };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function fromProtoTimestamp(value: any): Date {
|
|
668
|
+
const seconds = Number(value?.seconds ?? 0n);
|
|
669
|
+
const nanos = Number(value?.nanos ?? 0);
|
|
670
|
+
return new Date((seconds * 1000) + Math.trunc(nanos / 1_000_000));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function toJsInt(value: bigint): number | bigint {
|
|
674
|
+
const asNumber = Number(value);
|
|
675
|
+
return Number.isSafeInteger(asNumber) ? asNumber : value;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function toProtoJsonValue(value: unknown): any {
|
|
679
|
+
if (value === null || value === undefined) return { kind: { case: "nullValue", value: 0 } };
|
|
680
|
+
if (typeof value === "boolean") return { kind: { case: "boolValue", value } };
|
|
681
|
+
if (typeof value === "number") return { kind: { case: "numberValue", value } };
|
|
682
|
+
if (typeof value === "string") return { kind: { case: "stringValue", value } };
|
|
683
|
+
if (value instanceof Date || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
684
|
+
throw new Error(`unsupported JSON value type: ${value.constructor.name}`);
|
|
685
|
+
}
|
|
686
|
+
if (Array.isArray(value)) {
|
|
687
|
+
return {
|
|
688
|
+
kind: {
|
|
689
|
+
case: "listValue",
|
|
690
|
+
value: { values: value.map((item) => toProtoJsonValue(item)) },
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (typeof value === "object") {
|
|
695
|
+
const fields: { [key: string]: unknown } = {};
|
|
696
|
+
for (const [key, inner] of Object.entries(value as { [key: string]: unknown })) {
|
|
697
|
+
fields[key] = toProtoJsonValue(inner);
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
kind: {
|
|
701
|
+
case: "structValue",
|
|
702
|
+
value: { fields },
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
throw new Error(`unsupported JSON value type: ${typeof value}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function fromProtoJsonValue(value: any): unknown {
|
|
710
|
+
switch (value?.kind?.case) {
|
|
711
|
+
case undefined:
|
|
712
|
+
case "nullValue":
|
|
713
|
+
return null;
|
|
714
|
+
case "numberValue":
|
|
715
|
+
case "stringValue":
|
|
716
|
+
case "boolValue":
|
|
717
|
+
return value.kind.value;
|
|
718
|
+
case "listValue":
|
|
719
|
+
return (value.kind.value?.values ?? []).map((item: unknown) => fromProtoJsonValue(item));
|
|
720
|
+
case "structValue": {
|
|
721
|
+
const out: Record = {};
|
|
722
|
+
for (const [key, inner] of Object.entries(value.kind.value?.fields ?? {})) {
|
|
723
|
+
out[key] = fromProtoJsonValue(inner);
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
default:
|
|
728
|
+
throw new Error(`unsupported JSON value kind: ${String(value?.kind?.case)}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function rpc<T>(fn: () => Promise<T>): Promise<T> {
|
|
733
|
+
try {
|
|
734
|
+
return await fn();
|
|
735
|
+
} catch (err: any) {
|
|
736
|
+
if (err?.code === 5) throw new NotFoundError(err.message);
|
|
737
|
+
if (err?.code === 6) throw new AlreadyExistsError(err.message);
|
|
738
|
+
throw err;
|
|
739
|
+
}
|
|
740
|
+
}
|