@lossless.org/client 1.17.0 → 1.18.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 (29) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.collection.d.ts +17 -0
  3. package/dist_ts/nosqldb/classes.collection.js +56 -4
  4. package/dist_ts/nosqldb/classes.collectiontopology.d.ts +9 -0
  5. package/dist_ts/nosqldb/classes.collectiontopology.js +33 -5
  6. package/dist_ts/nosqldb/classes.distributedcoordinator.js +7 -7
  7. package/dist_ts/nosqldb/classes.easystore.js +2 -2
  8. package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +5 -0
  9. package/dist_ts/nosqldb/classes.ordinarypersistence.js +7 -1
  10. package/dist_ts/nosqldb/classes.watcher.js +2 -2
  11. package/dist_ts/nosqldb/plugins.d.ts +1 -2
  12. package/dist_ts/nosqldb/plugins.js +2 -3
  13. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +2 -2
  14. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +2 -2
  15. package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +1 -2
  16. package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +2 -3
  17. package/package.json +5 -6
  18. package/readme.md +1 -0
  19. package/ts/00_commitinfo_data.ts +1 -1
  20. package/ts/nosqldb/classes.collection.ts +71 -0
  21. package/ts/nosqldb/classes.collectiontopology.ts +36 -9
  22. package/ts/nosqldb/classes.distributedcoordinator.ts +7 -7
  23. package/ts/nosqldb/classes.easystore.ts +1 -1
  24. package/ts/nosqldb/classes.ordinarypersistence.ts +8 -0
  25. package/ts/nosqldb/classes.watcher.ts +1 -1
  26. package/ts/nosqldb/plugins.ts +0 -2
  27. package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +1 -1
  28. package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +1 -1
  29. package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +1 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lossless.org/client",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "private": false,
5
5
  "description": "One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.",
6
6
  "exports": {
@@ -46,12 +46,11 @@
46
46
  "homepage": "https://code.foss.global/lossless.org/client#readme",
47
47
  "dependencies": {
48
48
  "@push.rocks/lik": "^6.4.0",
49
- "@push.rocks/smartdelay": "^3.0.5",
50
49
  "@push.rocks/smartlog": "^3.2.1 || ^4.0.0",
51
50
  "@push.rocks/smartmime": "^2.0.4",
52
51
  "@push.rocks/smartobject": "^1.0.12",
53
52
  "@push.rocks/smartpath": "^6.0.0",
54
- "@push.rocks/smartpromise": "^4.2.5",
53
+ "@push.rocks/smartpromise": "^4.3.0",
55
54
  "@push.rocks/smartrx": "^3.0.10",
56
55
  "@push.rocks/smartstream": "^3.4.0",
57
56
  "@push.rocks/smartstring": "^4.1.0",
@@ -81,10 +80,10 @@
81
80
  "devDependencies": {
82
81
  "@aws-sdk/client-s3": "^3.1039.0",
83
82
  "@clickhouse/client": "1.23.1",
84
- "@git.zone/tsbuild": "5.0.0",
85
- "@git.zone/tsbundle": "3.1.0",
83
+ "@git.zone/tsbuild": "5.1.2",
84
+ "@git.zone/tsbundle": "3.1.2",
86
85
  "@git.zone/tsrun": "3.0.2",
87
- "@git.zone/tstest": "6.3.2",
86
+ "@git.zone/tstest": "6.3.4",
88
87
  "@lossless.org/nosqldb": "8.0.2",
89
88
  "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.7.2",
90
89
  "@lossless.org/objectstorage": "10.3.1",
package/readme.md CHANGED
@@ -114,6 +114,7 @@ Family imports preserve the established SmartData and SmartBucket names and cons
114
114
  | Grouped totals element filter | `elementFilter: { 'lines.accountNumber': '1200' }` beside `unwind: 'lines'` counts and sums only the matching elements — one account's balance per side — with the atomic-filter operators on the unwound array and its declared element paths; any other path or operator is refused with `invalid_argument` |
115
115
  | Grouped totals paging | rows come ordered by group key; `getGroupedTotalsPage()` returns `nextCursor` while `hasMore`, `cursor` continues after it, and `iterateGroupedTotals()` reads every group page by page — exact across pages for null, number, string and boolean group values (proven on NoSQLDB 8.0.2, 10.7.2 and MongoDB; ObjectId and date values are ordered by BSON type order but not yet covered by the paging tests), refused with `unsupported_operation` for other values, and a cursor from another query is refused with `invalid_argument` |
116
116
  | Query-plan inspection | `Model.inspectQueryPlan(db, { filter, sort?, projection?, limit?, timeoutMS?, signal? })` — the winning plan's stages (`stage`, `depth`, `indexName`, `direction`) and `nReturned`/`totalKeysExamined`/`totalDocsExamined`, read-only and bounded, so a consumer proves a model query uses its intended index without raw driver access |
117
+ | Per-instance model binding | `bindDocToManager(manager, Model)` returns a subclass of a model declared with `@managed()` and no manager argument, named like the model and bound to that manager, so several databases use the same models at once; any other binding is refused with `invalid_configuration` |
117
118
  | Disposable test databases on a unix socket | `SmartdataTestDatabase.create({ connection: { socketPath } })` from `@lossless.org/client/testsupport` reaches an engine that listens only on a unix-domain socket, fault helpers included; the loopback `host`/`port` form is unchanged |
118
119
 
119
120
  `docs/source/smartdata/readme.md` documents each one with its refusals.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/client',
6
- version: '1.17.0',
6
+ version: '1.18.0',
7
7
  description: 'One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.'
8
8
  }
@@ -37,6 +37,7 @@ import {
37
37
  import {
38
38
  collectionModelSchemaResolverSymbol,
39
39
  collectionTopologyDbInspectionSymbol,
40
+ findCollectionModelSchemaResolver,
40
41
  getExpectedCollectionTopologyForSchema,
41
42
  getIdentityIndexName,
42
43
  isIdentityIndexFor,
@@ -1219,12 +1220,23 @@ export const getIdentityDocumentIdField = (modelArg: any): string | undefined =>
1219
1220
  : undefined;
1220
1221
  };
1221
1222
 
1223
+ /**
1224
+ * Records on every collection binding whether its database follows the
1225
+ * receiver's manager. Only such a binding can be bound per instance through
1226
+ * `bindDocToManager()`; a binding with a fixed database or manager would
1227
+ * silently keep using it.
1228
+ */
1229
+ const managerFollowsReceiverSymbol = Symbol(
1230
+ '@lossless.org/client.nosqldb.managerFollowsReceiver',
1231
+ );
1232
+
1222
1233
  const installCollectionBinding = <
1223
1234
  TModel extends SmartDataDbDoc<any, any>,
1224
1235
  >(
1225
1236
  constructorArg: TCollectionModelConstructor<TModel>,
1226
1237
  dbResolverArg: (receiverArg?: unknown) => SmartdataDb,
1227
1238
  schemaResolverArg: () => INormalizedCollectionModelSchema,
1239
+ managerFollowsReceiverArg = false,
1228
1240
  ): TCollectionModelConstructor<TModel> => {
1229
1241
  const getCollection = (receiverArg?: unknown) => {
1230
1242
  const db = dbResolverArg(receiverArg);
@@ -1251,6 +1263,12 @@ const installCollectionBinding = <
1251
1263
  configurable: true,
1252
1264
  writable: false,
1253
1265
  });
1266
+ Object.defineProperty(constructorArg, managerFollowsReceiverSymbol, {
1267
+ value: managerFollowsReceiverArg,
1268
+ enumerable: false,
1269
+ configurable: true,
1270
+ writable: false,
1271
+ });
1254
1272
  (constructorArg as any).className = constructorArg.name;
1255
1273
  Object.defineProperty(constructorArg, 'collection', {
1256
1274
  get(this: unknown) {
@@ -1620,6 +1638,58 @@ export const setDefaultManagerForDoc = <T,>(managerArg: IManager, dbDocArg: T):
1620
1638
  return dbDocArg;
1621
1639
  };
1622
1640
 
1641
+ /**
1642
+ * Returns a subclass of a `@managed()` model that is bound to `managerArg`.
1643
+ *
1644
+ * `setDefaultManagerForDoc()` sets the manager on the model itself, so every
1645
+ * binding of one model shares the manager set last. The bound subclass carries
1646
+ * its own manager instead: an application instance binds every model it uses,
1647
+ * and several instances run on several databases at once. The subclass keeps
1648
+ * the model's name, collection and declarations, and its statics and instances
1649
+ * use the manager's `db` whenever they reach the database, so the manager may
1650
+ * connect after binding. The binding is fixed for the subclass's lifetime.
1651
+ *
1652
+ * The model must be declared with `@managed()` without a manager argument;
1653
+ * any other binding would ignore the bound manager, and a subclass that
1654
+ * declares fields or a policy without a declaration of its own has no schema.
1655
+ * Both are refused with `invalid_configuration`.
1656
+ */
1657
+ export const bindDocToManager = <
1658
+ TDoc extends abstract new (...args: never[]) => object,
1659
+ >(
1660
+ managerArg: IManager,
1661
+ docArg: TDoc,
1662
+ ): TDoc => {
1663
+ if (
1664
+ typeof docArg !== 'function' ||
1665
+ !(docArg.prototype instanceof SmartDataDbDoc) ||
1666
+ (docArg as unknown as Record<symbol, unknown>)[managerFollowsReceiverSymbol] !== true ||
1667
+ findCollectionModelSchemaResolver(docArg) === undefined
1668
+ ) {
1669
+ throw new SmartdataPersistenceError(
1670
+ 'invalid_configuration',
1671
+ `Model "${typeof docArg === 'function' ? docArg.name : 'unknown'}" cannot be bound to a manager; ` +
1672
+ 'declare it with @managed() without a manager argument.',
1673
+ );
1674
+ }
1675
+ if (typeof managerArg !== 'object' || managerArg === null) {
1676
+ throw new SmartdataPersistenceError(
1677
+ 'invalid_configuration',
1678
+ `Model "${docArg.name}" can only be bound to a manager object.`,
1679
+ );
1680
+ }
1681
+ const modelConstructor = docArg as unknown as new () => SmartDataDbDoc<object, object>;
1682
+ const boundDoc = class extends modelConstructor {};
1683
+ Object.defineProperty(boundDoc, 'name', { value: docArg.name, configurable: true });
1684
+ Object.defineProperty(boundDoc.prototype, 'defaultManager', {
1685
+ value: managerArg,
1686
+ enumerable: false,
1687
+ configurable: false,
1688
+ writable: false,
1689
+ });
1690
+ return boundDoc as unknown as TDoc;
1691
+ };
1692
+
1623
1693
  /**
1624
1694
  * This is a decorator that will tell the decorated class what dbTable to use
1625
1695
  * @param dbArg
@@ -1682,6 +1752,7 @@ export function managed<TManager extends IManager>(
1682
1752
  bindingOptions?.toleratedIndexNames,
1683
1753
  );
1684
1754
  },
1755
+ !managerArg,
1685
1756
  );
1686
1757
  Object.defineProperty(constructor, 'manager', {
1687
1758
  get(this: unknown) { return getManager(this); },
@@ -2,6 +2,7 @@ import type * as plugins from './plugins.js';
2
2
  import type { INormalizedCollectionModelSchema } from './classes.collection.js';
3
3
  import type { SmartdataDb } from './classes.db.js';
4
4
  import { SmartdataPersistenceError } from './classes.persistence.js';
5
+ import { resolvesSamePersistencePolicies } from './classes.ordinarypersistence.js';
5
6
 
6
7
  export type TSmartdataCollectionTopologyStatus =
7
8
  | 'absent'
@@ -228,18 +229,44 @@ const indexValuesEqual = (
228
229
  rightArg: ISmartdataCollectionTopologyIndex,
229
230
  ): boolean => JSON.stringify(leftArg) === JSON.stringify(rightArg);
230
231
 
232
+ const decoratorMetadataSymbol = (Symbol as unknown as { readonly metadata: symbol }).metadata;
233
+
234
+ /**
235
+ * @internal Finds the collection schema binding a model resolves: its own, or
236
+ * the one of the model it extends when it declares nothing of its own. Such an
237
+ * undecorated subclass — `bindDocToManager()` creates one per manager — is
238
+ * described exactly by the inherited schema. A subclass that declares decorator
239
+ * metadata or a persistence policy without a binding of its own is not, so it
240
+ * resolves none.
241
+ */
242
+ export const findCollectionModelSchemaResolver = (
243
+ constructorArg: unknown,
244
+ ): TCollectionModelSchemaResolver | undefined => {
245
+ if (typeof constructorArg !== 'function') {
246
+ return undefined;
247
+ }
248
+ let owner: unknown = constructorArg;
249
+ while (typeof owner === 'function') {
250
+ if (Object.hasOwn(owner, collectionModelSchemaResolverSymbol)) {
251
+ const resolver = (owner as ICollectionTopologyModelConstructor)[collectionModelSchemaResolverSymbol];
252
+ return typeof resolver === 'function' &&
253
+ resolvesSamePersistencePolicies(constructorArg, owner)
254
+ ? resolver
255
+ : undefined;
256
+ }
257
+ if (Object.hasOwn(owner, decoratorMetadataSymbol)) {
258
+ return undefined;
259
+ }
260
+ owner = Object.getPrototypeOf(owner);
261
+ }
262
+ return undefined;
263
+ };
264
+
231
265
  const getSchemaResolver = (
232
266
  constructorArg: ICollectionTopologyModelConstructor,
233
267
  ): TCollectionModelSchemaResolver => {
234
- const resolver = constructorArg?.[collectionModelSchemaResolverSymbol];
235
- if (
236
- typeof constructorArg !== 'function' ||
237
- !Object.prototype.hasOwnProperty.call(
238
- constructorArg,
239
- collectionModelSchemaResolverSymbol,
240
- ) ||
241
- typeof resolver !== 'function'
242
- ) {
268
+ const resolver = findCollectionModelSchemaResolver(constructorArg);
269
+ if (resolver === undefined) {
243
270
  throw new SmartdataPersistenceError(
244
271
  'invalid_configuration',
245
272
  `Model "${constructorArg?.name || 'unknown'}" has no collection schema binding.`,
@@ -77,7 +77,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
77
77
  private startHeartbeat = async () => {
78
78
  while (this.ownInstance.data.status !== 'stopped') {
79
79
  await this.sendHeartbeat();
80
- await plugins.smartdelay.delayForRandom(5000, 10000);
80
+ await plugins.smartpromise.delayForRandom(5000, 10000);
81
81
  }
82
82
  };
83
83
 
@@ -179,7 +179,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
179
179
  logger.log('info', 'bidding code stored.');
180
180
  });
181
181
  logger.log('info', `bidding for leadership...`);
182
- await plugins.smartdelay.delayFor(plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 }));
182
+ await plugins.smartpromise.delayFor(plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 }));
183
183
  await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
184
184
  let biddingInstances = await DistributedClass.getInstances({});
185
185
  biddingInstances = biddingInstances.filter(
@@ -195,7 +195,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
195
195
  this.ownInstance.data.elected = false;
196
196
  }
197
197
  }
198
- await plugins.smartdelay.delayFor(5000);
198
+ await plugins.smartpromise.delayFor(5000);
199
199
  logger.log('info', `settling with status elected = ${this.ownInstance.data.elected}`);
200
200
  this.ownInstance.data.status = 'settled';
201
201
  await this.ownInstance.save();
@@ -221,7 +221,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
221
221
  * all instances that requested this task
222
222
  */
223
223
  requestingDistibutedInstanceIds: string[];
224
- responseTimeout: plugins.smartdelay.Timeout<any>;
224
+ responseTimeout: plugins.smartpromise.Timeout<any>;
225
225
  }> = [];
226
226
 
227
227
  this.distributedWatcher.changeSubject.subscribe({
@@ -243,7 +243,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
243
243
  await instance.delete();
244
244
  }
245
245
  }
246
- await plugins.smartdelay.delayFor(10000);
246
+ await plugins.smartpromise.delayFor(10000);
247
247
  }
248
248
  }
249
249
 
@@ -260,7 +260,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
260
260
  this.ownInstance.data.taskRequests.push(taskRequestArg);
261
261
  await this.ownInstance.save();
262
262
  });
263
- await plugins.smartdelay.delayFor(10000);
263
+ await plugins.smartpromise.delayFor(10000);
264
264
  const result = await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
265
265
  await this.ownInstance.updateFromDb();
266
266
  const taskRequestResult = this.ownInstance.data.taskRequestResults.find((resultItem) => {
@@ -291,7 +291,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
291
291
  }
292
292
  Object.assign(existingInfoBasis, infoBasisArg);
293
293
  await this.ownInstance.save();
294
- plugins.smartdelay.delayFor(60000).then(() => {
294
+ plugins.smartpromise.delayFor(60000).then(() => {
295
295
  this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
296
296
  const indexToRemove = this.ownInstance.data.taskRequests.indexOf(existingInfoBasis);
297
297
  if (indexToRemove >= 0) {
@@ -246,7 +246,7 @@ export class EasyStore<T> {
246
246
  public async cleanUpEphemeral() {
247
247
  // Clean up ephemeral data periodically while connected
248
248
  while (this.smartdataDbRef.status === 'connected') {
249
- await plugins.smartdelay.delayFor(60000); // Check every minute
249
+ await plugins.smartpromise.delayFor(60000); // Check every minute
250
250
  // TODO: Implement actual cleanup logic for ephemeral data
251
251
  // For now, this prevents the infinite CPU loop
252
252
  }
@@ -28,6 +28,14 @@ const exactSymbol = Symbol.for('@push.rocks/smartdata.exactPersistencePolicy');
28
28
  export const hasExactPersistencePolicy = (modelArg: any): boolean =>
29
29
  Boolean(modelArg?.[exactSymbol]);
30
30
 
31
+ /**
32
+ * @internal Whether two models resolve the same ordinary and exact persistence
33
+ * policies, so one model's collection schema describes the other.
34
+ */
35
+ export const resolvesSamePersistencePolicies = (leftArg: object, rightArg: object): boolean =>
36
+ Reflect.get(leftArg, policySymbol) === Reflect.get(rightArg, policySymbol) &&
37
+ Reflect.get(leftArg, exactSymbol) === Reflect.get(rightArg, exactSymbol);
38
+
31
39
  export const getOrdinaryPersistencePolicy = (modelArg: any): IOrdinaryPersistencePolicy | undefined => {
32
40
  const policy = modelArg?.[policySymbol] as IOrdinaryPersistencePolicy | undefined;
33
41
  if (policy && modelArg[exactSymbol]) {
@@ -67,7 +67,7 @@ export class SmartdataDbWatcher<T = any> extends EventEmitter {
67
67
  this.changeStream.on('change', this.onChange);
68
68
  this.changeStream.on('error', this.onStreamError);
69
69
  // Signal readiness after one tick
70
- plugins.smartdelay.delayFor(0).then(() => {
70
+ plugins.smartpromise.delayFor(0).then(() => {
71
71
  this.readyDeferred.resolve();
72
72
  });
73
73
  }
@@ -5,7 +5,6 @@ export { tsclass };
5
5
 
6
6
  // @pushrocks scope
7
7
  import * as lik from '@push.rocks/lik';
8
- import * as smartdelay from '@push.rocks/smartdelay';
9
8
  import * as smartlog from '@push.rocks/smartlog';
10
9
  import * as smartpromise from '@push.rocks/smartpromise';
11
10
  import * as smartrx from '@push.rocks/smartrx';
@@ -21,7 +20,6 @@ import * as nodeNet from 'node:net';
21
20
 
22
21
  export {
23
22
  lik,
24
- smartdelay,
25
23
  smartpromise,
26
24
  smartlog,
27
25
  smartrx,
@@ -301,7 +301,7 @@ export class ClickhouseTable<T extends Record<string, any>> {
301
301
  'SELECT count() AS cnt FROM system.mutations WHERE is_done = 0 AND database = {database:String} AND table = {table:String}',
302
302
  { database: this.options.database, table: this.options.tableName }, { signal: operation.signal });
303
303
  if (BigInt(rows[0]?.cnt ?? 0) === 0n) return;
304
- await plugins.smartdelay.delayFor(100);
304
+ await plugins.smartpromise.delayFor(100);
305
305
  }
306
306
  } finally { operation.dispose(); }
307
307
  }
@@ -83,7 +83,7 @@ export class SmartClickHouseDb {
83
83
  while (true) {
84
84
  operation.check();
85
85
  if (await this.clickhouseHttpClient.ping({ signal: operation.signal })) return;
86
- await plugins.smartdelay.delayFor(100, null, this.options.unref);
86
+ await plugins.smartpromise.delayFor(100, null, this.options.unref);
87
87
  }
88
88
  } finally { operation.dispose(); }
89
89
  }
@@ -1,5 +1,4 @@
1
- import * as smartdelay from '@push.rocks/smartdelay';
2
1
  import * as smartobject from '@push.rocks/smartobject';
3
2
  import * as smartpromise from '@push.rocks/smartpromise';
4
3
  import * as smartrx from '@push.rocks/smartrx';
5
- export { smartdelay, smartobject, smartpromise, smartrx };
4
+ export { smartobject, smartpromise, smartrx };