@lossless.org/client 1.18.3 → 1.20.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.
Files changed (41) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.atomicdelete.js +12 -4
  3. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +11 -2
  4. package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
  5. package/dist_ts/nosqldb/classes.atomicupdate.js +32 -5
  6. package/dist_ts/nosqldb/classes.bootstraprecord.js +7 -2
  7. package/dist_ts/nosqldb/classes.collection.js +52 -7
  8. package/dist_ts/nosqldb/classes.db.d.ts +19 -0
  9. package/dist_ts/nosqldb/classes.db.js +21 -1
  10. package/dist_ts/nosqldb/classes.doc.js +2 -2
  11. package/dist_ts/nosqldb/classes.exactpersistence.js +17 -2
  12. package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
  13. package/dist_ts/nosqldb/classes.persistence.js +16 -1
  14. package/dist_ts/nosqldb/classes.session.d.ts +11 -0
  15. package/dist_ts/nosqldb/classes.session.js +51 -1
  16. package/dist_ts/nosqldb/classes.writeobserver.d.ts +57 -0
  17. package/dist_ts/nosqldb/classes.writeobserver.js +139 -0
  18. package/dist_ts/objectstorage/classes.bucket.d.ts +9 -0
  19. package/dist_ts/objectstorage/classes.bucket.js +35 -11
  20. package/dist_ts/objectstorage/classes.metadata.js +4 -5
  21. package/dist_ts/objectstorage/interfaces.d.ts +4 -0
  22. package/dist_ts/objectstorage/internal.bucketrequests.d.ts +7 -0
  23. package/dist_ts/objectstorage/internal.bucketrequests.js +14 -1
  24. package/package.json +3 -3
  25. package/readme.md +5 -1
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/nosqldb/classes.atomicdelete.ts +11 -2
  28. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +22 -0
  29. package/ts/nosqldb/classes.atomicupdate.ts +53 -3
  30. package/ts/nosqldb/classes.bootstraprecord.ts +7 -1
  31. package/ts/nosqldb/classes.collection.ts +63 -4
  32. package/ts/nosqldb/classes.db.ts +31 -0
  33. package/ts/nosqldb/classes.doc.ts +1 -1
  34. package/ts/nosqldb/classes.exactpersistence.ts +24 -0
  35. package/ts/nosqldb/classes.persistence.ts +16 -0
  36. package/ts/nosqldb/classes.session.ts +73 -0
  37. package/ts/nosqldb/classes.writeobserver.ts +224 -0
  38. package/ts/objectstorage/classes.bucket.ts +42 -10
  39. package/ts/objectstorage/classes.metadata.ts +4 -5
  40. package/ts/objectstorage/interfaces.ts +5 -0
  41. package/ts/objectstorage/internal.bucketrequests.ts +14 -0
@@ -0,0 +1,224 @@
1
+ import type { SmartdataDb } from './classes.db.js';
2
+ import { assertDataOptions } from './classes.operationbudget.js';
3
+ import { SmartdataPersistenceError } from './classes.persistence.js';
4
+ import { logger } from './logging.js';
5
+
6
+ /** What a write did to its documents. An upsert that inserted is an `insert`. */
7
+ export type TSmartdataWriteKind = 'insert' | 'update' | 'replace' | 'delete';
8
+
9
+ /** The values a report carries; any other stored value is left out. */
10
+ export type TSmartdataObservedValue = string | number | boolean | null;
11
+
12
+ /** The observed fields of one document or selector, by field name. */
13
+ export type TSmartdataObservedFields = Readonly<Record<string, TSmartdataObservedValue>>;
14
+
15
+ /** One acknowledged write, reported to every write observer of its database. */
16
+ export interface ISmartdataWriteReport {
17
+ /** The collection the write went to. */
18
+ readonly collection: string;
19
+ readonly kind: TSmartdataWriteKind;
20
+ /**
21
+ * The observed fields of each document the write named or returned: the
22
+ * inserted documents, a model instance's save or delete, an exact write, the
23
+ * document a find-one-and-modify returned. `null` when the write addressed
24
+ * its documents by selector only; `anchors` then says what it pinned.
25
+ */
26
+ readonly documents: readonly TSmartdataObservedFields[] | null;
27
+ /**
28
+ * The observed fields the write's selector pinned by top-level equality
29
+ * (`{ field: value }`, `{ field: { $eq: value } }`, or inside `$and`).
30
+ * Empty for an insert, which has no selector.
31
+ */
32
+ readonly anchors: TSmartdataObservedFields;
33
+ }
34
+
35
+ export interface ISmartdataWriteObserverOptions {
36
+ /** Top-level field names the reports carry: 1 to 64 distinct names, no `.` and no leading `$`. */
37
+ fields: readonly string[];
38
+ /**
39
+ * Called synchronously once per write. A listener that throws is logged and
40
+ * never fails the write; one that needs to do async work schedules it.
41
+ */
42
+ listener: (reportArg: ISmartdataWriteReport) => void;
43
+ }
44
+
45
+ /** @internal What a write path states about one acknowledged write. */
46
+ export interface ISmartdataWrite {
47
+ collection: string;
48
+ kind: TSmartdataWriteKind;
49
+ /** The documents as written or returned; `null` for a write by selector only. */
50
+ documents: readonly object[] | null;
51
+ selector?: unknown;
52
+ /** The model's `identityAsDocumentId` field, whose value a selector carries as `_id`. */
53
+ identityDocumentIdField?: string;
54
+ }
55
+
56
+ interface IWriteObserver {
57
+ fields: readonly string[];
58
+ listener: (reportArg: ISmartdataWriteReport) => void;
59
+ }
60
+
61
+ const maximumObservedFields = 64;
62
+ const observersByDb = new WeakMap<SmartdataDb, Set<IWriteObserver>>();
63
+
64
+ const normalizeObserverOptions = (optionsArg: unknown): IWriteObserver => {
65
+ const { fields, listener } = assertDataOptions(optionsArg, ['fields', 'listener']);
66
+ if (typeof listener !== 'function') {
67
+ throw new SmartdataPersistenceError('invalid_argument',
68
+ 'A SmartData write observer requires a listener function.');
69
+ }
70
+ if (
71
+ !Array.isArray(fields)
72
+ || fields.length === 0
73
+ || fields.length > maximumObservedFields
74
+ || fields.some((fieldArg) =>
75
+ typeof fieldArg !== 'string'
76
+ || fieldArg.length === 0
77
+ || fieldArg.includes('.')
78
+ || fieldArg.startsWith('$'))
79
+ || new Set(fields).size !== fields.length
80
+ ) {
81
+ throw new SmartdataPersistenceError('invalid_argument',
82
+ `A SmartData write observer requires 1 to ${maximumObservedFields} distinct top-level field names.`);
83
+ }
84
+ return {
85
+ fields: Object.freeze([...fields] as string[]),
86
+ listener: listener as IWriteObserver['listener'],
87
+ };
88
+ };
89
+
90
+ /** @internal Registers a write observer; the returned function removes it. */
91
+ export const addSmartdataWriteObserver = (
92
+ dbArg: SmartdataDb,
93
+ optionsArg: ISmartdataWriteObserverOptions,
94
+ ): (() => void) => {
95
+ const observer = normalizeObserverOptions(optionsArg);
96
+ let observers = observersByDb.get(dbArg);
97
+ if (!observers) {
98
+ observers = new Set();
99
+ observersByDb.set(dbArg, observers);
100
+ }
101
+ observers.add(observer);
102
+ return () => {
103
+ observersByDb.get(dbArg)?.delete(observer);
104
+ };
105
+ };
106
+
107
+ /** @internal */
108
+ export const hasSmartdataWriteObservers = (dbArg: SmartdataDb): boolean =>
109
+ (observersByDb.get(dbArg)?.size ?? 0) > 0;
110
+
111
+ const observedValueOf = (valueArg: unknown): TSmartdataObservedValue | undefined =>
112
+ valueArg === null
113
+ || typeof valueArg === 'string'
114
+ || typeof valueArg === 'boolean'
115
+ || (typeof valueArg === 'number' && Number.isFinite(valueArg))
116
+ ? valueArg as TSmartdataObservedValue
117
+ : undefined;
118
+
119
+ const isPlainRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
120
+ if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) {
121
+ return false;
122
+ }
123
+ const prototype = Object.getPrototypeOf(valueArg);
124
+ return prototype === Object.prototype || prototype === null;
125
+ };
126
+
127
+ const pickDocumentFields = (
128
+ documentArg: object,
129
+ fieldsArg: readonly string[],
130
+ ): Record<string, TSmartdataObservedValue> => {
131
+ const picked: Record<string, TSmartdataObservedValue> = {};
132
+ for (const field of fieldsArg) {
133
+ if (!Object.hasOwn(documentArg, field)) continue;
134
+ const value = observedValueOf((documentArg as Record<string, unknown>)[field]);
135
+ if (value !== undefined) picked[field] = value;
136
+ }
137
+ return picked;
138
+ };
139
+
140
+ /**
141
+ * A `null` condition also matches a missing field, so only concrete values
142
+ * anchor a selector.
143
+ */
144
+ const collectSelectorAnchors = (
145
+ selectorArg: unknown,
146
+ fieldsArg: ReadonlySet<string>,
147
+ identityDocumentIdFieldArg: string | undefined,
148
+ anchorsArg: Record<string, TSmartdataObservedValue>,
149
+ ): void => {
150
+ if (!isPlainRecord(selectorArg)) return;
151
+ for (const key of Object.keys(selectorArg)) {
152
+ const condition = selectorArg[key];
153
+ if (key === '$and') {
154
+ if (Array.isArray(condition)) {
155
+ for (const conjunct of condition) {
156
+ collectSelectorAnchors(conjunct, fieldsArg, identityDocumentIdFieldArg, anchorsArg);
157
+ }
158
+ }
159
+ continue;
160
+ }
161
+ let value = observedValueOf(condition);
162
+ if (
163
+ value === undefined
164
+ && isPlainRecord(condition)
165
+ && Object.keys(condition).length === 1
166
+ && Object.hasOwn(condition, '$eq')
167
+ ) {
168
+ value = observedValueOf(condition.$eq);
169
+ }
170
+ if (value === undefined || value === null) continue;
171
+ const fields = key === '_id' && identityDocumentIdFieldArg
172
+ ? [key, identityDocumentIdFieldArg]
173
+ : [key];
174
+ for (const field of fields) {
175
+ if (fieldsArg.has(field) && !Object.hasOwn(anchorsArg, field)) {
176
+ anchorsArg[field] = value;
177
+ }
178
+ }
179
+ }
180
+ };
181
+
182
+ const describeError = (errorArg: unknown): string =>
183
+ errorArg instanceof Error ? errorArg.message : String(errorArg);
184
+
185
+ /**
186
+ * @internal Captures one acknowledged write for the observers registered now,
187
+ * and returns the delivery, or `undefined` when nobody observes the database.
188
+ * The observed values are copied at capture, so a delivery deferred to a
189
+ * transaction's commit reports the write as it was made. An observer removed
190
+ * before the delivery receives nothing.
191
+ */
192
+ export const captureSmartdataWrite = (
193
+ dbArg: SmartdataDb,
194
+ writeArg: ISmartdataWrite,
195
+ ): (() => void) | undefined => {
196
+ const registered = observersByDb.get(dbArg);
197
+ if (!registered || registered.size === 0) return undefined;
198
+ const observers = [...registered];
199
+ const fields = [...new Set(observers.flatMap((observerArg) => observerArg.fields))];
200
+ const documents = writeArg.documents?.map((documentArg) =>
201
+ pickDocumentFields(documentArg, fields)) ?? null;
202
+ const anchors: Record<string, TSmartdataObservedValue> = {};
203
+ collectSelectorAnchors(writeArg.selector, new Set(fields), writeArg.identityDocumentIdField, anchors);
204
+ return () => {
205
+ for (const observer of observers) {
206
+ if (!observersByDb.get(dbArg)?.has(observer)) continue;
207
+ const report: ISmartdataWriteReport = Object.freeze({
208
+ collection: writeArg.collection,
209
+ kind: writeArg.kind,
210
+ documents: documents === null
211
+ ? null
212
+ : Object.freeze(documents.map((documentArg) =>
213
+ Object.freeze(pickDocumentFields(documentArg, observer.fields)))),
214
+ anchors: Object.freeze(pickDocumentFields(anchors, observer.fields)),
215
+ });
216
+ try {
217
+ observer.listener(report);
218
+ } catch (errorArg) {
219
+ logger.log('error',
220
+ `A SmartData write observer threw on a write to "${writeArg.collection}": ${describeError(errorArg)}`);
221
+ }
222
+ }
223
+ };
224
+ };
@@ -18,7 +18,12 @@ import {
18
18
  purgeExactPath as purgeExactPathOperation,
19
19
  } from './internal.exactpathpurge.operations.js';
20
20
  import { beginBasicBucketOperation } from './internal.exactupload.capability.js';
21
- import { getProviderStatus, headBucketExists, runAccountRequest } from './internal.bucketrequests.js';
21
+ import {
22
+ getProviderStatus,
23
+ headBucketExists,
24
+ isMissingObjectError,
25
+ runAccountRequest,
26
+ } from './internal.bucketrequests.js';
22
27
  import { ObjectRangeError } from './classes.objectrangeerror.js';
23
28
 
24
29
  export interface IListObjectKeysPageOptions {
@@ -288,6 +293,15 @@ const destroyReadableAndWaitForSettlement = async (
288
293
  }
289
294
  };
290
295
 
296
+ /** Reads a downloaded object body to its end and destroys the stream. */
297
+ const readObjectBody = async (sourceArg: plugins.stream.Readable): Promise<Buffer> => {
298
+ const chunks: Buffer[] = [];
299
+ try {
300
+ for await (const chunk of sourceArg) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
301
+ return Buffer.concat(chunks);
302
+ } finally { sourceArg.destroy(); }
303
+ };
304
+
291
305
  /**
292
306
  * The bucket class exposes the basic functionality of a bucket.
293
307
  * The functions of the bucket alone are enough to
@@ -456,12 +470,30 @@ export class Bucket {
456
470
  * get file
457
471
  */
458
472
  public async fastGet(optionsArg: { path: string }): Promise<Buffer> {
459
- const source = await this.fastGetStream(optionsArg);
460
- const chunks: Buffer[] = [];
473
+ return await readObjectBody(await this.fastGetStream(optionsArg));
474
+ }
475
+
476
+ /**
477
+ * Reads an object that may be missing with one GetObject request: resolves
478
+ * its bytes, or `null` when the provider answers that the key does not
479
+ * exist (`NoSuchKey`, `NotFound` or HTTP 404). Every other failure rejects
480
+ * as `fastGet()` does: a missing bucket, a denied request, a failure while
481
+ * the body streams, and an abort through `signal` or `SmartBucket.close()`,
482
+ * which rejects with the abort reason.
483
+ */
484
+ public async fastGetIfPresent(
485
+ optionsArg: interfaces.IFastGetIfPresentOptions,
486
+ ): Promise<Buffer | null> {
487
+ let source: plugins.stream.Readable;
461
488
  try {
462
- for await (const chunk of source) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
463
- return Buffer.concat(chunks);
464
- } finally { source.destroy(); }
489
+ source = await this.fastGetStream(optionsArg);
490
+ } catch (error) {
491
+ if (isMissingObjectError(error)) {
492
+ return null;
493
+ }
494
+ throw error;
495
+ }
496
+ return await readObjectBody(source);
465
497
  }
466
498
 
467
499
  /**
@@ -822,13 +854,13 @@ export class Bucket {
822
854
  { abortSignal: operation.signal },
823
855
  ));
824
856
  exists = true;
825
- } catch (error: any) {
857
+ } catch (error) {
826
858
  try {
827
859
  operation.assertCurrent();
828
860
  } catch (abortError) {
829
861
  throw abortError;
830
862
  }
831
- if (error?.name === 'NotFound') {
863
+ if (isMissingObjectError(error)) {
832
864
  exists = false;
833
865
  } else {
834
866
  throw error;
@@ -989,13 +1021,13 @@ export class Bucket {
989
1021
  () => operation.client.send(command, { abortSignal: operation.signal }),
990
1022
  );
991
1023
  return true;
992
- } catch (error: any) {
1024
+ } catch (error) {
993
1025
  try {
994
1026
  operation.assertCurrent();
995
1027
  } catch (abortError) {
996
1028
  throw abortError;
997
1029
  }
998
- if (error?.name === 'NotFound') {
1030
+ if (isMissingObjectError(error)) {
999
1031
  return false;
1000
1032
  } else {
1001
1033
  throw error; // Rethrow if it's not a NotFound error to handle unexpected issues
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
2
 
3
3
  import { File } from './classes.file.js';
4
+ import { isMissingObjectError } from './internal.bucketrequests.js';
4
5
 
5
6
  export class MetaData {
6
7
  public static async hasMetaData(optionsArg: { file: File }) {
@@ -10,12 +11,10 @@ export class MetaData {
10
11
  path: optionsArg.file.name + '.metadata',
11
12
  });
12
13
  return !!existingFile;
13
- } catch (error: any) {
14
- const message = error?.message || '';
14
+ } catch (error) {
15
15
  const isNotFound =
16
- message.includes('File not found') ||
17
- error?.name === 'NotFound' ||
18
- error?.$metadata?.httpStatusCode === 404;
16
+ (error instanceof Error && error.message.includes('File not found')) ||
17
+ isMissingObjectError(error);
19
18
  if (isNotFound) {
20
19
  return false;
21
20
  }
@@ -78,6 +78,11 @@ export interface IFastExistsOptions {
78
78
  signal?: AbortSignal;
79
79
  }
80
80
 
81
+ export interface IFastGetIfPresentOptions {
82
+ path: string;
83
+ signal?: AbortSignal;
84
+ }
85
+
81
86
  export interface IFastStatOptions extends IPathDecriptor {
82
87
  signal?: AbortSignal;
83
88
  }
@@ -35,6 +35,20 @@ export const getProviderStatus = (errorArg: unknown): { name?: string; httpStatu
35
35
  };
36
36
  };
37
37
 
38
+ /**
39
+ * Answers whether a provider error says the addressed object does not exist:
40
+ * GetObject's `NoSuchKey`, HeadObject's body-less `NotFound`, or any other
41
+ * HTTP 404. A `NoSuchBucket` answer names a missing bucket, not a missing
42
+ * object, and is not one.
43
+ */
44
+ export const isMissingObjectError = (errorArg: unknown): boolean => {
45
+ const { name, httpStatusCode } = getProviderStatus(errorArg);
46
+ if (name === 'NoSuchBucket') {
47
+ return false;
48
+ }
49
+ return name === 'NoSuchKey' || name === 'NotFound' || httpStatusCode === 404;
50
+ };
51
+
38
52
  /**
39
53
  * Answers whether the bucket exists with one HeadBucket request, which a
40
54
  * bucket-scoped credential may send for its own bucket. A missing bucket