@harperfast/harper 5.2.10 → 5.2.13

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 (44) hide show
  1. package/dist/resources/Table.d.ts +2 -2
  2. package/dist/resources/Table.js +182 -61
  3. package/dist/resources/Table.js.map +1 -1
  4. package/dist/resources/databases.d.ts +1 -0
  5. package/dist/resources/databases.js +73 -40
  6. package/dist/resources/databases.js.map +1 -1
  7. package/dist/utility/errors/hdbError.d.ts +8 -0
  8. package/dist/utility/errors/hdbError.js +29 -0
  9. package/dist/utility/errors/hdbError.js.map +1 -1
  10. package/npm-shrinkwrap.json +38 -38
  11. package/package.json +2 -2
  12. package/resources/Table.ts +177 -62
  13. package/resources/databases.ts +74 -42
  14. package/studio/web/assets/{Chat-Br06zdMA.js → Chat-4RrB5134.js} +1 -1
  15. package/studio/web/assets/{FloatingChat-BWImX5fA.js → FloatingChat-omlNMDcJ.js} +3 -3
  16. package/studio/web/assets/{apiToken-DN0nmDsq.js → apiToken-Bke3wvfZ.js} +1 -1
  17. package/studio/web/assets/{applications-kSxVoyeU.js → applications-DvFDYJgK.js} +1 -1
  18. package/studio/web/assets/{cssMode-C1JeufH5.js → cssMode-C1vRa7zh.js} +1 -1
  19. package/studio/web/assets/{editor-qoo9CrGO.js → editor-3XRWEDWX.js} +1 -1
  20. package/studio/web/assets/{html-Dt4IIy04.js → html-CTY5tdMr.js} +1 -1
  21. package/studio/web/assets/{htmlMode-DXgKKr4C.js → htmlMode-CG1vSD9t.js} +1 -1
  22. package/studio/web/assets/{index-6onkYFOG.js → index-Dfpeofdu.js} +5 -5
  23. package/studio/web/assets/{index.lazy-BrCFnpNJ.js → index.lazy-B1VOIv-t.js} +1 -1
  24. package/studio/web/assets/{javascript-DNCQGUBc.js → javascript-CJeJzGnI.js} +1 -1
  25. package/studio/web/assets/{jsonMode-CR6HWruP.js → jsonMode-CYPBwM82.js} +1 -1
  26. package/studio/web/assets/{languageServices-BM4fI4rS.js → languageServices-IKH4GUHl.js} +1 -1
  27. package/studio/web/assets/{lspLanguageFeatures-DSa1ttcD.js → lspLanguageFeatures-BC8_gxKG.js} +1 -1
  28. package/studio/web/assets/{notifications-BHXLnh6x.js → notifications-BfKBpYcq.js} +1 -1
  29. package/studio/web/assets/{notifications-CUmtIA6z.js → notifications-DFbArTfC.js} +1 -1
  30. package/studio/web/assets/{profile-8BeFSF3j.js → profile-Q4-T6c-S.js} +1 -1
  31. package/studio/web/assets/{regions-C8qR0HhD.js → regions-CtkV0xje.js} +1 -1
  32. package/studio/web/assets/{register-B4n5i0SD.js → register-1ZZuMsiA.js} +2 -2
  33. package/studio/web/assets/{setComponentFile-g0_B0lgX.js → setComponentFile-ZWLcWv5X.js} +1 -1
  34. package/studio/web/assets/{setup-B0CTj_Q5.js → setup-DWprJyJy.js} +2 -2
  35. package/studio/web/assets/{status-C6Yib7-K.js → status--aNm8isn.js} +1 -1
  36. package/studio/web/assets/{toggleHighContrast-Dgta7bVi.js → toggleHighContrast-Cq_lt3XD.js} +1 -1
  37. package/studio/web/assets/{tsMode-CH_jHvU-.js → tsMode-pjgytARx.js} +1 -1
  38. package/studio/web/assets/{typescript-Co9LCXd5.js → typescript-Cdg0mqUh.js} +1 -1
  39. package/studio/web/assets/{useEntityRestURL-wKC8NsC_.js → useEntityRestURL-RZhaY8Rn.js} +1 -1
  40. package/studio/web/assets/{workers-CWeLxCXA.js → workers-BgoXIqQe.js} +1 -1
  41. package/studio/web/assets/{xml-BadC-0Rk.js → xml-BwSeDMiP.js} +1 -1
  42. package/studio/web/assets/{yaml-BiUfxPbC.js → yaml-DotCUG5l.js} +1 -1
  43. package/studio/web/index.html +1 -1
  44. package/utility/errors/hdbError.ts +26 -0
@@ -527,10 +527,10 @@ export declare function makeTable(options: any): {
527
527
  getStorageStats(): Promise<import("../server/storageReclamation.ts").StorageSpaceStats>;
528
528
  getRecordCount(options?: any): Promise<{
529
529
  recordCount: number;
530
- estimatedRange: number[];
530
+ estimatedRange?: undefined;
531
531
  } | {
532
532
  recordCount: number;
533
- estimatedRange?: undefined;
533
+ estimatedRange: number[];
534
534
  }>;
535
535
  /**
536
536
  * When attributes have been changed, we update the accessors that are assigned to this table
@@ -102,6 +102,20 @@ const EVICTION_BATCH_SIZE = 100;
102
102
  // letting an unbounded number of open transactions (and their snapshots) accumulate.
103
103
  const MAX_INFLIGHT_EVICTION_BATCHES = 4;
104
104
  const CACHEABLE_STATUS_CODES = new Set([200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501]);
105
+ // Smallest forward sample `getRecordCount` will extrapolate a record rate from; below it the scan runs
106
+ // to completion and reports an exact count.
107
+ const MIN_ESTIMATOR_SAMPLE = 1_000;
108
+ // Budget intervals the forward scan may spend before it must estimate rather than keep scanning.
109
+ const MAX_ESTIMATE_CHECKPOINTS = 20;
110
+ // A store estimate's `count`, or 0 when the store answered with a shape that cannot be trusted --
111
+ // DESIGN.md's invariant for this API family is that such an answer degrades rather than poisons.
112
+ function usableCount(estimate) {
113
+ const { count, confidence } = estimate ?? {};
114
+ // `confidence` needs its own finiteness check, not just the range: `null >= 0 && null <= 1` is true
115
+ if (!Number.isFinite(count) || count < 0 || !Number.isFinite(confidence) || confidence < 0 || confidence > 1)
116
+ return 0;
117
+ return count;
118
+ }
105
119
  envMngr.initSync();
106
120
  const LMDB_PREFETCH_WRITES = envMngr.get(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PREFETCHWRITES);
107
121
  const LOCK_TIMEOUT = 10000;
@@ -1534,7 +1548,7 @@ function makeTable(options) {
1534
1548
  records: './', // an href to the records themselves
1535
1549
  name: tableName,
1536
1550
  database: databaseName,
1537
- auditSize: auditStore instanceof rocksdb_js_1.RocksDatabase ? auditStore.getKeysCount() : auditStore?.getStats().entryCount,
1551
+ auditSize: auditStore?.getStats().entryCount,
1538
1552
  attributes,
1539
1553
  recordCount: undefined,
1540
1554
  estimatedRecordRange: undefined,
@@ -4674,85 +4688,182 @@ function makeTable(options) {
4674
4688
  const exactCount = options?.exactCount;
4675
4689
  const TIME_LIMIT = options?.timeLimit ?? 1000 / 2; // one second time limit, enforced by seeing if we are halfway through at 500ms
4676
4690
  const start = performance.now();
4677
- // `entryCount` (the exact key count) is only needed once the scan blows the time budget --
4678
- // to decide whether to estimate and as the extrapolation base. On RocksDB it is a full
4679
- // key-only scan, so we defer it: tables that finish within budget (the common case) and
4680
- // `exact_count` requests never pay for it. `halfway`/`entryCount` stay 0 until first computed.
4681
4691
  let entryCount = 0;
4682
- let halfway = 0;
4683
- let counted = false;
4684
- let completeForExact = false;
4692
+ let remainderPhysical = 0;
4693
+ let estimator;
4694
+ // feature-detected per DESIGN.md's invariant for this API family; LMDB stores do not implement it
4695
+ const canEstimate = typeof primaryStore.createCountEstimator === 'function' && typeof primaryStore.estimateCount === 'function';
4696
+ let estimatorFailed = false;
4697
+ let warnedNoBase = false;
4698
+ let checkpoints = 0;
4699
+ let checkpointedEntries = 0;
4685
4700
  let recordCount = 0;
4686
4701
  let entriesScanned = 0;
4702
+ let lastKey;
4687
4703
  let limit;
4688
- for (const { value } of primaryStore.getRange({ start: true, lazy: true, snapshot: false })) {
4704
+ let nextCheckAt = start + TIME_LIMIT;
4705
+ for (const { key, value } of primaryStore.getRange({ start: true, lazy: true, snapshot: false })) {
4689
4706
  if (value != null)
4690
4707
  recordCount++;
4691
4708
  entriesScanned++;
4709
+ lastKey = key;
4692
4710
  await rest();
4693
- if (!exactCount && !completeForExact && performance.now() - start > TIME_LIMIT) {
4694
- if (!counted) {
4695
- counted = true;
4696
- entryCount = isRocksDB
4697
- ? primaryStore.getKeysCount({ start: undefined })
4698
- : primaryStore.getStats().entryCount;
4699
- halfway = Math.floor(entryCount / 2);
4700
- }
4701
- if (entriesScanned < halfway) {
4702
- // it is taking too long, so we will just take this sample and a sample from the end to estimate
4703
- limit = entriesScanned;
4704
- break;
4711
+ // a table too small to reach the floor is small enough to finish exactly
4712
+ if (exactCount || entriesScanned < MIN_ESTIMATOR_SAMPLE)
4713
+ continue;
4714
+ const now = performance.now();
4715
+ if (now <= nextCheckAt)
4716
+ continue;
4717
+ nextCheckAt = now + TIME_LIMIT;
4718
+ checkpoints++;
4719
+ if (canEstimate && !estimatorFailed) {
4720
+ try {
4721
+ estimator ??= primaryStore.createCountEstimator({ start: true });
4722
+ estimator.advance(lastKey, entriesScanned - checkpointedEntries);
4723
+ checkpointedEntries = entriesScanned;
4724
+ entryCount = usableCount(estimator.estimate());
4725
+ }
4726
+ catch (error) {
4727
+ // a store closing concurrently -- drop_table can, while this scan is parked in a yield
4728
+ logger_ts_1.logger.debug?.('Count estimator unavailable, falling back to an exact scan', error);
4729
+ estimatorFailed = true;
4730
+ estimator = undefined;
4731
+ entryCount = 0;
4705
4732
  }
4706
- // Past the halfway point already: finishing the scan for an exact count is cheaper
4707
- // than estimating. Set the flag so we stop re-evaluating the budget on each remaining iteration.
4708
- completeForExact = true;
4733
+ }
4734
+ else if (!canEstimate) {
4735
+ // `canEstimate` false is not the same as "LMDB": a RocksDB store whose native module predates
4736
+ // the estimator API lands here too, and `RocksDatabase.getStats()` carries no `entryCount`.
4737
+ try {
4738
+ const stats = primaryStore.getStats?.();
4739
+ entryCount = Number.isFinite(stats?.entryCount) && stats.entryCount > 0 ? stats.entryCount : 0;
4740
+ }
4741
+ catch {
4742
+ entryCount = 0;
4743
+ }
4744
+ }
4745
+ if (!entryCount && canEstimate && !estimatorFailed) {
4746
+ // Range estimates are block-granular and can report 0 for a store whose entries are still
4747
+ // in the memtable. Without a base the escape cannot fire at all, so fall back to the
4748
+ // whole-store property rather than silently walking the table.
4749
+ try {
4750
+ const wholeStore = primaryStore.getEstimatedKeyCount();
4751
+ entryCount = Number.isFinite(wholeStore) && wholeStore > 0 ? wholeStore : 0;
4752
+ }
4753
+ catch {
4754
+ entryCount = 0;
4755
+ }
4756
+ if (!entryCount && !warnedNoBase) {
4757
+ warnedNoBase = true;
4758
+ logger_ts_1.logger.debug?.(`No usable key-count estimate for ${tableName}; counting records by full scan`);
4759
+ }
4760
+ }
4761
+ // Zero is "no usable base": degrade to the exact scan. The checkpoint ceiling is what stops a
4762
+ // base that keeps undershooting from holding the halfway test false forever and walking the
4763
+ // whole table; the reverse sample is bounded by `limit` in turn.
4764
+ if (entryCount > 0 &&
4765
+ (checkpoints >= MAX_ESTIMATE_CHECKPOINTS || entriesScanned < Math.floor(entryCount / 2))) {
4766
+ if (canEstimate) {
4767
+ try {
4768
+ const remaining = primaryStore.estimateCount({ start: lastKey, exclusiveStart: true });
4769
+ // widened by its own reported untrustworthiness: block-granular, so it can land below
4770
+ // the live count it is meant to bound
4771
+ const remainingCount = usableCount(remaining);
4772
+ remainderPhysical = remainingCount > 0 ? remainingCount * (2 - remaining.confidence) : 0;
4773
+ }
4774
+ catch {
4775
+ remainderPhysical = 0;
4776
+ }
4777
+ // A zero or unusable remainder is valid -- entries still in the memtable read as none
4778
+ // through range statistics -- but it would leave `baseMax` resting on the sampled ends
4779
+ // alone. The whole-store property is a separate, non-range source, so fall back to it.
4780
+ if (!remainderPhysical) {
4781
+ try {
4782
+ const wholeStore = primaryStore.getEstimatedKeyCount();
4783
+ if (Number.isFinite(wholeStore))
4784
+ remainderPhysical = Math.max(wholeStore - entriesScanned, 0);
4785
+ }
4786
+ catch {
4787
+ remainderPhysical = 0;
4788
+ }
4789
+ }
4790
+ }
4791
+ limit = entriesScanned;
4792
+ break;
4709
4793
  }
4710
4794
  }
4711
4795
  if (limit) {
4712
4796
  // in this case we are going to make an estimate of the table count using the first thousand
4713
4797
  // entries and last thousand entries
4714
4798
  const firstRecordCount = recordCount;
4799
+ const firstKey = lastKey;
4715
4800
  recordCount = 0;
4716
4801
  // Bound the reverse scan explicitly. The getRange `limit` option is honored by lmdb-js but
4717
4802
  // ignored by rocksdb-js; without this break the scan reads the whole table, so `recordRate`
4718
4803
  // blows up to ~entryCount/(2*limit) and the estimate scales with entryCount^2 -- the source
4719
4804
  // of the wildly inflated `record_count` (e.g. 20,000,000 for ~105k rows) on large RocksDB
4720
- // tables. The early-exit above guarantees limit < entryCount/2, so the two samples stay disjoint.
4805
+ // tables.
4721
4806
  let reverseScanned = 0;
4722
- for (const { value } of primaryStore.getRange({
4807
+ // Sized independently of the forward scan. `entriesScanned` is whatever the forward pass
4808
+ // covered before it escaped, and the checkpoint ceiling lets that run twenty budget
4809
+ // intervals when the base keeps undershooting; matching it here would read that same count
4810
+ // again and double the wall clock of the call this path exists to bound.
4811
+ const reverseLimit = Math.min(limit, MIN_ESTIMATOR_SAMPLE);
4812
+ // Disjointness is enforced against the forward scan's own last key rather than inferred from
4813
+ // the base, which is an estimate that can overshoot by more than 2x.
4814
+ let sampledWholeTable = false;
4815
+ for (const { key, value } of primaryStore.getRange({
4723
4816
  start: '\uffff',
4724
4817
  reverse: true,
4725
4818
  lazy: true,
4726
- limit,
4819
+ limit: reverseLimit,
4727
4820
  snapshot: false,
4728
4821
  })) {
4822
+ if ((0, ordered_binary_1.compareKeys)(key, firstKey) <= 0) {
4823
+ sampledWholeTable = true;
4824
+ break;
4825
+ }
4729
4826
  if (value != null)
4730
4827
  recordCount++;
4731
4828
  reverseScanned++;
4732
4829
  await rest();
4733
- if (reverseScanned >= limit)
4830
+ if (reverseScanned >= reverseLimit)
4734
4831
  break;
4735
4832
  }
4833
+ // the samples met, so between them they covered every entry
4834
+ if (sampledWholeTable)
4835
+ return { recordCount: recordCount + firstRecordCount };
4736
4836
  // Use the actual entries sampled, not limit*2: the reverse scan can yield fewer than `limit`
4737
4837
  // (concurrent deletions under snapshot:false, or an overestimated entryCount), and counting
4738
4838
  // those un-scanned slots would inflate the denominator and underestimate the rate.
4739
- const sampleSize = limit + reverseScanned;
4740
- const recordRate = (recordCount + firstRecordCount) / sampleSize;
4741
- const variance = Math.pow((recordCount - firstRecordCount + 1) / limit / 2, 2) + // variance between samples
4742
- (recordRate * (1 - recordRate)) / sampleSize;
4743
- const sd = Math.max(Math.sqrt(variance) * entryCount, 1);
4744
- const estimatedRecordCount = Math.round(recordRate * entryCount);
4745
- // TODO: This uses a normal/Wald interval, but a binomial confidence interval is probably better calculated using
4746
- // Wilson score interval or Agresti-Coull interval (I think the latter is a little easier to calculate/implement).
4747
- const lowerCiLimit = Math.max(estimatedRecordCount - 1.96 * sd, recordCount + firstRecordCount);
4748
- const upperCiLimit = Math.min(estimatedRecordCount + 1.96 * sd, entryCount);
4749
- let significantUnit = Math.pow(10, Math.round(Math.log10(sd)));
4750
- if (significantUnit > estimatedRecordCount)
4751
- significantUnit = significantUnit / 10;
4752
- recordCount = Math.round(estimatedRecordCount / significantUnit) * significantUnit;
4839
+ const sampledRecords = recordCount + firstRecordCount;
4840
+ const recordRate = sampledRecords / (limit + reverseScanned);
4841
+ // Endpoints for the extrapolation base, spanning both ways an estimated base can be wrong:
4842
+ // every remaining entry superseded (only what was sampled is live) through every remaining
4843
+ // entry live (the uncalibrated physical count). Churn concentrated outside the sampled ends
4844
+ // calibrates to nothing, so an interval derived from the estimator's confidence would sit
4845
+ // narrowly around the wrong number. Both endpoints are themselves estimates on RocksDB, so
4846
+ // this is a widened heuristic interval, not a guaranteed bound on the live count.
4847
+ const baseMin = entriesScanned + reverseScanned;
4848
+ const baseMax = Math.max(entriesScanned + remainderPhysical, entryCount, baseMin);
4849
+ const estimatedRecordCount = Math.round(recordRate * Math.max(entryCount, baseMin));
4850
+ // The samples counted these directly, and the entries between them can only add; everything
4851
+ // outside the samples could be live. A statistical interval inside those endpoints would be
4852
+ // narrowest exactly where the ends are least representative of the middle -- sampled ends
4853
+ // that are all deletion entries give a rate of 0, collapsing an upper end to ~0 with live
4854
+ // rows in between -- so the endpoints are the evidence itself.
4855
+ const lower = sampledRecords;
4856
+ const upper = Math.round(baseMax);
4857
+ // Report only the precision the interval supports, but never so coarse a unit that the
4858
+ // estimate rounds away: `baseMax` is physical and can exceed a calibrated estimate by
4859
+ // orders of magnitude, which a single division cannot walk back.
4860
+ let significantUnit = Math.pow(10, Math.round(Math.log10(Math.max((upper - lower) / 2, 1))));
4861
+ while (significantUnit > estimatedRecordCount && significantUnit > 1)
4862
+ significantUnit /= 10;
4863
+ recordCount = Math.min(Math.max(Math.round(estimatedRecordCount / significantUnit) * significantUnit, lower), upper);
4753
4864
  return {
4754
4865
  recordCount,
4755
- estimatedRange: [Math.round(lowerCiLimit), Math.round(upperCiLimit)],
4866
+ estimatedRange: [lower, upper],
4756
4867
  };
4757
4868
  }
4758
4869
  return {
@@ -5903,25 +6014,35 @@ function makeTable(options) {
5903
6014
  resolve(resolvedEntry);
5904
6015
  }
5905
6016
  catch (error) {
5906
- error.message += ` while resolving record ${id} for ${tableName}`;
5907
- if (existingRecord &&
5908
- (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
5909
- !context?.mustRevalidate) ||
5910
- (context?.staleIfError &&
5911
- (error.statusCode === 500 ||
5912
- error.statusCode === 502 ||
5913
- error.statusCode === 503 ||
5914
- error.statusCode === 504)))) {
5915
- // these are conditions under which we can use stale data after an error
5916
- resolve({
5917
- key: id,
5918
- version: existingVersion,
5919
- value: existingRecord,
5920
- });
5921
- logger_ts_1.logger.trace?.(error.message, '(returned stale record)');
6017
+ // A source may reject with anything at all, so deciding how to settle is itself
6018
+ // fallible: `message` is not assignable on every error (a DOMException from
6019
+ // AbortSignal.timeout), and a nullish rejection makes the reads below throw.
6020
+ // Leaving this promise unsettled hangs the caller forever, so every path here
6021
+ // has to end in resolve() or reject().
6022
+ try {
6023
+ (0, hdbError_ts_1.appendErrorContext)(error, ` while resolving record ${id} for ${tableName}`);
6024
+ if (existingRecord &&
6025
+ (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
6026
+ !context?.mustRevalidate) ||
6027
+ (context?.staleIfError &&
6028
+ (error.statusCode === 500 ||
6029
+ error.statusCode === 502 ||
6030
+ error.statusCode === 503 ||
6031
+ error.statusCode === 504)))) {
6032
+ // these are conditions under which we can use stale data after an error
6033
+ resolve({
6034
+ key: id,
6035
+ version: existingVersion,
6036
+ value: existingRecord,
6037
+ });
6038
+ logger_ts_1.logger.trace?.(error?.message, '(returned stale record)');
6039
+ }
6040
+ else
6041
+ reject(error);
6042
+ }
6043
+ catch (settlingError) {
6044
+ reject(error ?? settlingError);
5922
6045
  }
5923
- else
5924
- reject(error);
5925
6046
  const resolveDuration = performance.now() - start;
5926
6047
  (0, write_ts_1.recordAction)(resolveDuration, 'cache-resolution', tableName, null, 'fail');
5927
6048
  if (responseHeaders)