@proteinjs/db 1.57.0 → 1.58.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/generated/source/index.d.ts +9 -0
  5. package/dist/generated/source/index.d.ts.map +1 -0
  6. package/dist/generated/source/index.js +63 -0
  7. package/dist/generated/source/index.js.map +1 -0
  8. package/dist/generated/test/index.js +1 -1
  9. package/dist/generated/test/index.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src/source → source}/SourceRecordSyncRunner.d.ts +17 -28
  15. package/dist/source/SourceRecordSyncRunner.d.ts.map +1 -0
  16. package/dist/{src/source → source}/SourceRecordSyncRunner.js +62 -36
  17. package/dist/source/SourceRecordSyncRunner.js.map +1 -0
  18. package/dist/source/index.d.ts +10 -0
  19. package/dist/source/index.d.ts.map +1 -0
  20. package/dist/source/index.js +26 -0
  21. package/dist/source/index.js.map +1 -0
  22. package/dist/src/CursorWindowPager.d.ts +9 -0
  23. package/dist/src/CursorWindowPager.d.ts.map +1 -1
  24. package/dist/src/CursorWindowPager.js +26 -0
  25. package/dist/src/CursorWindowPager.js.map +1 -1
  26. package/dist/src/Db.d.ts +3 -1
  27. package/dist/src/Db.d.ts.map +1 -1
  28. package/dist/src/Db.js +5 -3
  29. package/dist/src/Db.js.map +1 -1
  30. package/dist/src/MigrationRunner.js +2 -2
  31. package/dist/src/MigrationRunner.js.map +1 -1
  32. package/dist/src/source/SourceRecordSync.d.ts +44 -0
  33. package/dist/src/source/SourceRecordSync.d.ts.map +1 -0
  34. package/dist/src/source/SourceRecordSync.js +18 -0
  35. package/dist/src/source/SourceRecordSync.js.map +1 -0
  36. package/dist/test/BrowserIndexNoNodeBuiltins.test.d.ts +2 -0
  37. package/dist/test/BrowserIndexNoNodeBuiltins.test.d.ts.map +1 -0
  38. package/dist/test/BrowserIndexNoNodeBuiltins.test.js +185 -0
  39. package/dist/test/BrowserIndexNoNodeBuiltins.test.js.map +1 -0
  40. package/dist/test/CursorWindowPager.test.js +74 -1
  41. package/dist/test/CursorWindowPager.test.js.map +1 -1
  42. package/dist/test/SourceRecordLoaderQualifiedName.test.js +4 -3
  43. package/dist/test/SourceRecordLoaderQualifiedName.test.js.map +1 -1
  44. package/dist/test/SourceRecordSyncRunner.test.js +1 -1
  45. package/dist/test/SourceRecordSyncRunner.test.js.map +1 -1
  46. package/dist/test/SourceRecordSyncSeam.test.d.ts +2 -0
  47. package/dist/test/SourceRecordSyncSeam.test.d.ts.map +1 -0
  48. package/dist/test/SourceRecordSyncSeam.test.js +245 -0
  49. package/dist/test/SourceRecordSyncSeam.test.js.map +1 -0
  50. package/dist/test/reusable/SourceRecordSyncTests.js +1 -1
  51. package/dist/test/reusable/SourceRecordSyncTests.js.map +1 -1
  52. package/generated/index.ts +1 -1
  53. package/generated/source/index.ts +59 -0
  54. package/generated/test/index.ts +1 -1
  55. package/index.ts +5 -3
  56. package/package.json +12 -4
  57. package/{src/source → source}/SourceRecordSyncRunner.ts +42 -61
  58. package/source/index.ts +9 -0
  59. package/source.d.ts +2 -0
  60. package/source.js +4 -0
  61. package/src/CursorWindowPager.ts +17 -0
  62. package/src/Db.ts +5 -3
  63. package/src/MigrationRunner.ts +2 -2
  64. package/src/source/SourceRecordSync.ts +54 -0
  65. package/test/BrowserIndexNoNodeBuiltins.test.ts +123 -0
  66. package/test/CursorWindowPager.test.ts +48 -1
  67. package/test/SourceRecordLoaderQualifiedName.test.ts +4 -3
  68. package/test/SourceRecordSyncRunner.test.ts +1 -1
  69. package/test/SourceRecordSyncSeam.test.ts +140 -0
  70. package/test/reusable/SourceRecordSyncTests.ts +1 -1
  71. package/tsconfig.json +3 -2
  72. package/dist/src/source/SourceRecordSyncRunner.d.ts.map +0 -1
  73. package/dist/src/source/SourceRecordSyncRunner.js.map +0 -1
package/index.ts CHANGED
@@ -17,9 +17,11 @@ export * from './src/RecordIterator';
17
17
  export * from './src/CursorWindowPager';
18
18
  export * from './src/source/SourceRecord';
19
19
  export * from './src/source/SourceRecordRepo';
20
- // The sync runner behind Db.init's source-record leg. Public so consumer test harnesses can run
21
- // one boot of the sync directly (the exports map blocks deep dist imports).
22
- export { SourceRecordSyncRunner } from './src/source/SourceRecordSyncRunner';
20
+ // The source-record sync's seam: its summary types and `getSourceRecordSync`. The runner itself is
21
+ // server-only code (it reads package.json files off the booting process) behind the
22
+ // `@proteinjs/db/source` subpath, which a server's boot imports once; a consumer test harness that
23
+ // runs one boot of the sync directly imports it from there too.
24
+ export * from './src/source/SourceRecordSync';
23
25
  export * from './src/MigrationRunner';
24
26
  export * from './src/QueryBuilderFactory';
25
27
  export * from './src/TableWatcher';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db",
3
- "version": "1.57.0",
3
+ "version": "1.58.1",
4
4
  "main": "./dist/generated/index.js",
5
5
  "types": "./dist/generated/index.d.ts",
6
6
  "exports": {
@@ -11,12 +11,19 @@
11
11
  "./test": {
12
12
  "types": "./dist/generated/test/index.d.ts",
13
13
  "default": "./dist/generated/test/index.js"
14
+ },
15
+ "./source": {
16
+ "types": "./dist/generated/source/index.d.ts",
17
+ "default": "./dist/generated/source/index.js"
14
18
  }
15
19
  },
16
20
  "typesVersions": {
17
21
  "*": {
18
22
  "test": [
19
23
  "dist/generated/test/index.d.ts"
24
+ ],
25
+ "source": [
26
+ "dist/generated/source/index.d.ts"
20
27
  ]
21
28
  }
22
29
  },
@@ -36,7 +43,7 @@
36
43
  "license": "MIT",
37
44
  "scripts": {
38
45
  "clean": "rm -rf dist/ node_modules/ generated/",
39
- "build": "reflection-build && REFLECTION_SOURCE_DIRS=test,src REFLECTION_EXPORT_FROM=test/index.ts reflection-build && tsc",
46
+ "build": "reflection-build && REFLECTION_SOURCE_DIRS=test,src REFLECTION_EXPORT_FROM=test/index.ts reflection-build && REFLECTION_SOURCE_DIRS=source,src REFLECTION_EXPORT_FROM=source/index.ts reflection-build && tsc",
40
47
  "watch": "reflection-watch",
41
48
  "test": "jest --passWithNoTests"
42
49
  },
@@ -64,7 +71,8 @@
64
71
  "eslint-plugin-prettier": "5.1.3",
65
72
  "jest": "29.7.0",
66
73
  "ts-jest": "29.1.1",
67
- "typescript": "5.2.2"
74
+ "typescript": "5.2.2",
75
+ "webpack": "5.88.2"
68
76
  },
69
- "gitHead": "c33ab303e81e366f73bb2cfac13f818567c37306"
77
+ "gitHead": "0c9fe556af1052933bb3e6fef08f9e33c129c2a6"
70
78
  }
@@ -1,10 +1,13 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
1
3
  import { Logger } from '@proteinjs/logger';
2
4
  import { QueryBuilder } from '@proteinjs/db-query';
3
- import { getSourceRecordLoaders, SourceRecord, getSourceRecordTables } from './SourceRecord';
4
- import { Table } from '../Table';
5
- import { Db, getDbAsSystem } from '../Db';
6
- import { SourceRecordRepo } from './SourceRecordRepo';
7
- import { RecordSerializer } from '../Record';
5
+ import { getSourceRecordLoaders, SourceRecord, getSourceRecordTables } from '../src/source/SourceRecord';
6
+ import { Table } from '../src/Table';
7
+ import { Db, getDbAsSystem } from '../src/Db';
8
+ import { SourceRecordRepo } from '../src/source/SourceRecordRepo';
9
+ import { RecordSerializer } from '../src/Record';
10
+ import { SourceRecordLoadSummary, SourceRecordSync } from '../src/source/SourceRecordSync';
8
11
 
9
12
  type DeclaredRecord = {
10
13
  /** The owning package (the declaring loader's package) — the grain the sync prunes within. */
@@ -19,29 +22,12 @@ type SourceRecordsMap = {
19
22
  [tableName: string]: { table: Table<any>; records: DeclaredRecord[] };
20
23
  };
21
24
 
22
- /** What one boot of the sync did to one source-record table. */
23
- export type SourceRecordTableLoadSummary = {
24
- inserts: number;
25
- updates: number;
26
- unchanged: number;
27
- adopted: number;
28
- /**
29
- * Declarations whose natural key is held by a row the sync does not own (written at runtime —
30
- * a person's own row, say): refused for this boot, that row left exactly as it is (see
31
- * {@link SourceRecordOptions.naturalKey}).
32
- */
33
- refused: number;
34
- deletes: number;
35
- removedUpdates: number;
36
- /** Rows stamped by a newer version of their declaring package — left exactly as they are. */
37
- skippedNewer: number;
38
- /** Source-loaded rows with no owner stamp that no declaration in this build claims. */
39
- unowned: number;
40
- };
41
-
42
- export type SourceRecordLoadSummary = { [tableName: string]: SourceRecordTableLoadSummary };
43
-
44
- export class SourceRecordSyncRunner {
25
+ /**
26
+ * The one {@link SourceRecordSync}: server-only code (it reads package.json files off the booting
27
+ * process) behind the `@proteinjs/db/source` subpath — a server's boot imports that subpath once,
28
+ * and reflection hands this runner to `Db.init` through `getSourceRecordSync`.
29
+ */
30
+ export class SourceRecordSyncRunner implements SourceRecordSync {
45
31
  private logger = new Logger({ name: this.constructor.name });
46
32
 
47
33
  /** Memo of resolved package versions — one resolution (and at most one warning) per source. */
@@ -627,44 +613,39 @@ export class SourceRecordSyncRunner {
627
613
  * `<pkg>/package.json` is usually not requireable), then the nearest package.json whose
628
614
  * `name` matches is read walking up from it.
629
615
  *
630
- * Returns undefined where neither road resolves — no Node `require` (browser bundles), or a
631
- * package not resolvable from cwd. The sync then has no place in the version order for that
632
- * package: it never touches its version-stamped rows, and what it writes is unversioned
633
- * (see {@link isNewerStamp}). Logged once per boot so a misconfigured cwd is visible.
616
+ * Returns undefined where neither road resolves — a package not resolvable from cwd. The sync
617
+ * then has no place in the version order for that package: it never touches its
618
+ * version-stamped rows, and what it writes is unversioned (see {@link isNewerStamp}). Logged
619
+ * once per boot so a misconfigured cwd is visible.
620
+ *
621
+ * Plain Node here — `fs`, `path` and the CJS `require.resolve` — because this module is
622
+ * server-only code behind the `@proteinjs/db/source` subpath: the package's browser index never
623
+ * imports it (test/BrowserIndexNoNodeBuiltins.test.ts holds that line), so nothing here hides
624
+ * from a bundler.
634
625
  */
635
626
  private resolveSourceVersion(source: string): string | undefined {
636
- // The ambient CJS `require` — the only require carrying `.resolve` (module.require does
637
- // not). Every use below goes through the variable, so bundlers never see a statically
638
- // analyzable `require(...)` call; in a browser bundle the runtime attempts throw into
639
- // their catches and the method degrades to undefined.
640
- const nodeRequire: NodeRequire | undefined =
641
- typeof require === 'function' && typeof require.resolve === 'function' ? require : undefined;
642
- const cwd = typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : undefined;
643
- if (nodeRequire && cwd) {
644
- try {
645
- const path = nodeRequire('path');
646
- const fs = nodeRequire('fs');
647
- const ownVersion = this.packageVersionAt(fs, path.join(cwd, 'package.json'), source);
648
- if (ownVersion) {
649
- return ownVersion;
650
- }
627
+ const cwd = process.cwd();
628
+ try {
629
+ const ownVersion = this.packageVersionAt(path.join(cwd, 'package.json'), source);
630
+ if (ownVersion) {
631
+ return ownVersion;
632
+ }
651
633
 
652
- const entryPath = nodeRequire.resolve(source, { paths: [cwd] });
653
- // Walk up from the entry file to the package's own package.json (the name check skips
654
- // nested stubs like a dist/package.json); stop at the filesystem root.
655
- for (let directory = path.dirname(entryPath); ; directory = path.dirname(directory)) {
656
- const version = this.packageVersionAt(fs, path.join(directory, 'package.json'), source);
657
- if (version) {
658
- return version;
659
- }
634
+ const entryPath = require.resolve(source, { paths: [cwd] });
635
+ // Walk up from the entry file to the package's own package.json (the name check skips
636
+ // nested stubs like a dist/package.json); stop at the filesystem root.
637
+ for (let directory = path.dirname(entryPath); ; directory = path.dirname(directory)) {
638
+ const version = this.packageVersionAt(path.join(directory, 'package.json'), source);
639
+ if (version) {
640
+ return version;
641
+ }
660
642
 
661
- if (path.dirname(directory) === directory) {
662
- break;
663
- }
643
+ if (path.dirname(directory) === directory) {
644
+ break;
664
645
  }
665
- } catch {
666
- // Unresolvable from cwd — reported below.
667
646
  }
647
+ } catch {
648
+ // Unresolvable from cwd — reported below.
668
649
  }
669
650
 
670
651
  this.logger.warn({
@@ -674,7 +655,7 @@ export class SourceRecordSyncRunner {
674
655
  }
675
656
 
676
657
  /** The `version` of the package.json at `packageJsonPath` when it exists and names `source`. */
677
- private packageVersionAt(fs: typeof import('fs'), packageJsonPath: string, source: string): string | undefined {
658
+ private packageVersionAt(packageJsonPath: string, source: string): string | undefined {
678
659
  if (!fs.existsSync(packageJsonPath)) {
679
660
  return undefined;
680
661
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@proteinjs/db/source` — the server-only half of the source-record sync: the runner behind
3
+ * `Db.init`'s source-record leg. It reads package.json files off the booting process (the
4
+ * version stamp on every row it writes), so it never rides the package's browser index; a
5
+ * server's boot imports this subpath once, which registers the runner as the package's
6
+ * `SourceRecordSync` — the seam `Db.init` and the pre-schema-sync migration phase resolve through
7
+ * reflection. A test harness that runs one boot of the sync directly imports the runner from here.
8
+ */
9
+ export * from './SourceRecordSyncRunner';
package/source.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Types counterpart of source.js — see that file for why this stub exists.
2
+ export * from './dist/generated/source/index';
package/source.js ADDED
@@ -0,0 +1,4 @@
1
+ // Node10/classic-resolution stub for the `./source` subpath, owned by reflection-build.
2
+ // Consumers that bypass the exports map (node10 resolution, tsconfig paths mappings) resolve
3
+ // this file path first, so every install shape loads the built dist — never sources.
4
+ module.exports = require('./dist/generated/source/index.js');
@@ -76,6 +76,23 @@ export class CursorWindowPager<T extends Record> {
76
76
  return { rows, nextAnchor: this.deriveNextAnchor(rows, windowSize) };
77
77
  }
78
78
 
79
+ /**
80
+ * THE ROWS PAST AN ANCHOR, COUNTED: how many rows the windows after `anchor` would serve — under this pager's
81
+ * own conditions and its lexicographic continuation (the same WHERE the windows read with, never a second
82
+ * filter), with no sort and no pagination: one COUNT at the database. A null anchor counts the whole data
83
+ * set. A consumer that sizes room for the rows a reader has not loaded yet (a transcript's rows above its
84
+ * loaded start, sized before the first paint) reads this beside the window it just loaded and holds an exact
85
+ * count where a guess would refill or collapse later.
86
+ */
87
+ async countPast(anchor: CursorAnchor | null): Promise<number> {
88
+ const qb = this.createQuery();
89
+ if (anchor) {
90
+ qb.or(this.anchorConditions(anchor));
91
+ }
92
+ const db = this.options?.db ?? getDb();
93
+ return await db.getRowCount(this.table, qb);
94
+ }
95
+
79
96
  static encodeAnchor(anchor: CursorAnchor): string {
80
97
  return Serializer.serialize(anchor);
81
98
  }
package/src/Db.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  import { ReverseCascadeEdgeIndex } from './ReverseCascadeEdgeIndex';
13
13
  import { Record, RecordSerializer, SerializedRecord } from './Record';
14
14
  import { Logger } from '@proteinjs/logger';
15
- import { SourceRecordSyncRunner } from './source/SourceRecordSyncRunner';
15
+ import { getSourceRecordSync } from './source/SourceRecordSync';
16
16
  import { ParameterizationConfig, ParamType, QueryBuilder, Statement, StatementFactory } from '@proteinjs/db-query';
17
17
  import { QueryBuilderFactory } from './QueryBuilderFactory';
18
18
  import { StatementConfigFactory } from './StatementConfigFactory';
@@ -211,7 +211,9 @@ export class Db<R extends Record = Record> implements DbService<R> {
211
211
  * backfill, which fails loudly over violating data); an immediate no-op unless a migration
212
212
  * declares {@link Migration.preSchemaSync};
213
213
  * 3. schema sync — create/alter every registered table (the DDL);
214
- * 4. source-record sync.
214
+ * 4. source-record sync — through {@link getSourceRecordSync}: the runner is server-only code
215
+ * behind the `@proteinjs/db/source` subpath (the browser index never carries it), registered
216
+ * by the server's one import of that subpath; a boot without it fails here, loudly.
215
217
  */
216
218
  async init(): Promise<void> {
217
219
  await this.dbDriver.createDbIfNotExists();
@@ -225,7 +227,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
225
227
  const { MigrationRunner } = await import('./MigrationRunner');
226
228
  await new MigrationRunner().runPreSchemaSyncMigrations(this.dbDriver.getTableManager());
227
229
  await this.dbDriver.getTableManager().loadTables();
228
- await new SourceRecordSyncRunner().load();
230
+ await getSourceRecordSync().load();
229
231
  }
230
232
 
231
233
  async tableExists<T extends R>(table: Table<T>): Promise<boolean> {
@@ -2,7 +2,7 @@ import { Moment, moment } from './opt/moment';
2
2
  import { Db, getDb, getDbAsSystem } from './Db';
3
3
  import { Table } from './Table';
4
4
  import { SourceRecordRepo } from './source/SourceRecordRepo';
5
- import { SourceRecordSyncRunner } from './source/SourceRecordSyncRunner';
5
+ import { getSourceRecordSync } from './source/SourceRecordSync';
6
6
  import { getSourceRecordLoaders } from './source/SourceRecord';
7
7
  import { MigrationRunnerService, getMigrationRunnerService } from './services/MigrationRunnerService';
8
8
  import { Migration, MigrationEstimate, MigrationTable } from './tables/MigrationTable';
@@ -195,7 +195,7 @@ export class MigrationRunner implements MigrationRunnerService {
195
195
  }
196
196
 
197
197
  await tableManager.loadTable(migrationTable);
198
- await new SourceRecordSyncRunner().load(migrationTable);
198
+ await getSourceRecordSync().load(migrationTable);
199
199
  this.logger.info({
200
200
  message: `Running ${flagged.length} pre-schema-sync migration${flagged.length === 1 ? '' : 's'} before schema sync`,
201
201
  obj: { ids: flagged.map((migration) => migration.id) },
@@ -0,0 +1,54 @@
1
+ import { Loadable, SourceRepository } from '@proteinjs/reflection';
2
+ import type { Table } from '../Table';
3
+
4
+ /** What one boot of the sync did to one source-record table. */
5
+ export type SourceRecordTableLoadSummary = {
6
+ inserts: number;
7
+ updates: number;
8
+ unchanged: number;
9
+ adopted: number;
10
+ /**
11
+ * Declarations whose natural key is held by a row the sync does not own (written at runtime —
12
+ * a person's own row, say): refused for this boot, that row left exactly as it is (see
13
+ * {@link SourceRecordOptions.naturalKey}).
14
+ */
15
+ refused: number;
16
+ deletes: number;
17
+ removedUpdates: number;
18
+ /** Rows stamped by a newer version of their declaring package — left exactly as they are. */
19
+ skippedNewer: number;
20
+ /** Source-loaded rows with no owner stamp that no declaration in this build claims. */
21
+ unowned: number;
22
+ };
23
+
24
+ export type SourceRecordLoadSummary = { [tableName: string]: SourceRecordTableLoadSummary };
25
+
26
+ /**
27
+ * The source-record sync a boot runs — the seam `Db.init` (and the pre-schema-sync migration
28
+ * phase) reach it through. Its one implementation, `SourceRecordSyncRunner`, is server-only code:
29
+ * it reads package.json files off the booting process to version-stamp every row, so it lives
30
+ * behind the `@proteinjs/db/source` subpath and this package's browser index never carries it
31
+ * (test/BrowserIndexNoNodeBuiltins.test.ts holds that line). A server imports
32
+ * `@proteinjs/db/source` once at boot, which registers the runner here through reflection; the
33
+ * browser index declares only this interface.
34
+ */
35
+ export interface SourceRecordSync extends Loadable {
36
+ /** One boot of the sync, for every source-record table in the build — or for `onlyTable`. */
37
+ load(onlyTable?: Table<any>): Promise<SourceRecordLoadSummary>;
38
+ }
39
+
40
+ /**
41
+ * The registered sync — or a loud refusal naming the subpath that registers it. A boot never
42
+ * silently skips its source records: a server that has not imported `@proteinjs/db/source` fails
43
+ * here, at `Db.init`, with the fix in the message.
44
+ */
45
+ export const getSourceRecordSync = (): SourceRecordSync => {
46
+ const sync = SourceRepository.get().object<SourceRecordSync>('@proteinjs/db/SourceRecordSync');
47
+ if (!sync) {
48
+ throw new Error(
49
+ `Unable to find a @proteinjs/db/SourceRecordSync implementation. The source-record sync is server-only code behind the '@proteinjs/db/source' subpath: import '@proteinjs/db/source' once in the server's boot (it registers SourceRecordSyncRunner), and the boot's source records load.`
50
+ );
51
+ }
52
+
53
+ return sync;
54
+ };
@@ -0,0 +1,123 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import webpack from 'webpack';
5
+
6
+ /**
7
+ * THE LAW: this package's browser-facing index never reaches a Node builtin. `@proteinjs/db` is
8
+ * one package for both halves — the browser's Db talks through DbService, the server's through a
9
+ * driver — and its public entry (`dist/generated/index.js`, what a consumer's webpack bundles)
10
+ * must bundle for the browser as it stands; a consumer app has no business patching the
11
+ * library's reach with a `resolve.fallback`. The source-record sync (it reads package.json files
12
+ * off the booting process) is the server-only code that broke this line in db 1.55–1.58 — a
13
+ * webpack 5 client bundle met its `fs` and booted to nothing — and now lives behind the
14
+ * `@proteinjs/db/source` subpath. This guard holds the line for every module to come.
15
+ *
16
+ * Mechanics: webpack 5.88.2 — the consumer's bundler, at app-ui's version — bundles the built
17
+ * browser index for `target: 'web'` with this package's own dependencies external, so the only
18
+ * resolution left to fail is a Node builtin's, which webpack 5 refuses by name ("Module not
19
+ * found: Error: Can't resolve 'fs'"). It refuses as a WARNING when the reach sits in a try/catch
20
+ * (an optional dependency) — exactly how 1.58.0's runner reached `fs`, through a renamed
21
+ * `require` that esbuild never sees but webpack's parser tracks — so errors AND warnings must be
22
+ * empty. The module list then pins the subpath out of the graph by path.
23
+ *
24
+ * The one named exception: four encryption modules import `crypto` (since before 1.54.1), and
25
+ * consumers substitute a browser build for it (app-ui's webpack: `fallback.crypto:
26
+ * crypto-browserify`). Pinned to exactly those four files — a fifth reader of `crypto`, or any
27
+ * other builtin anywhere, is red.
28
+ */
29
+ const packageDir = path.join(__dirname, '..');
30
+ const BROWSER_INDEX = 'dist/generated/index.js';
31
+ const SERVER_ONLY_SUBPATH = /(^|\/)dist\/source\//;
32
+ const CRYPTO_READERS = [
33
+ 'dist/src/encryption/DataKeyStore.js',
34
+ 'dist/src/encryption/EncryptionEnvelope.js',
35
+ 'dist/src/encryption/InMemoryMasterKeyProvider.js',
36
+ 'dist/src/encryption/SearchTokenizer.js',
37
+ ];
38
+
39
+ /** What one browser bundle of the index came to. */
40
+ type Bundle = { errors: string[]; warnings: string[]; modules: string[]; cryptoReaders: string[] };
41
+
42
+ /** `fs.rmSync` (Node 14.14+; this package's @types/node predates it). */
43
+ const removeDirectory = (directory: string) =>
44
+ (fs as unknown as { rmSync(target: string, options: { recursive: true; force: true }): void }).rmSync(directory, {
45
+ recursive: true,
46
+ force: true,
47
+ });
48
+
49
+ /** This package's dependencies: a consumer resolves those itself; never this bundle's concern. */
50
+ const dependencies = (): string[] =>
51
+ Object.keys(JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')).dependencies);
52
+
53
+ const bundleBrowserIndex = (): Promise<Bundle> =>
54
+ new Promise((resolve, reject) => {
55
+ const names = dependencies();
56
+ const cryptoReaders = new Set<string>();
57
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-browser-index-'));
58
+ webpack(
59
+ {
60
+ mode: 'none',
61
+ target: 'web',
62
+ context: packageDir,
63
+ entry: path.join(packageDir, BROWSER_INDEX),
64
+ output: { path: outDir, filename: 'browser.js' },
65
+ externals: [
66
+ ({ request, contextInfo }, callback) => {
67
+ if (names.some((name) => request === name || !!request?.startsWith(`${name}/`))) {
68
+ return callback(undefined, `commonjs ${request}`);
69
+ }
70
+
71
+ // The crypto exception, pinned to its readers; every other builtin stays webpack's own refusal.
72
+ if (request && /^(node:)?crypto$/.test(request)) {
73
+ const issuer = path.relative(packageDir, contextInfo?.issuer ?? '');
74
+ cryptoReaders.add(issuer);
75
+ if (CRYPTO_READERS.includes(issuer)) {
76
+ return callback(undefined, `commonjs ${request}`);
77
+ }
78
+ }
79
+
80
+ callback();
81
+ },
82
+ ],
83
+ },
84
+ (error, stats) => {
85
+ removeDirectory(outDir);
86
+ if (error || !stats) {
87
+ return reject(error ?? new Error('webpack produced no stats'));
88
+ }
89
+
90
+ const json = stats.toJson({ all: false, errors: true, warnings: true, modules: true });
91
+ const line = ({ message, moduleName }: { message: string; moduleName?: string }) =>
92
+ `${message.split('\n')[0]} (${moduleName})`;
93
+ resolve({
94
+ errors: (json.errors ?? []).map(line),
95
+ warnings: (json.warnings ?? []).map(line),
96
+ modules: (json.modules ?? []).map((module) => module.name ?? ''),
97
+ cryptoReaders: Array.from(cryptoReaders).sort(),
98
+ });
99
+ }
100
+ );
101
+ });
102
+
103
+ describe('the browser index (dist/generated/index.js) bundles for the browser', () => {
104
+ let bundle: Bundle;
105
+
106
+ beforeAll(async () => {
107
+ bundle = await bundleBrowserIndex();
108
+ }, 60000);
109
+
110
+ it('reaches no Node builtin: webpack resolved every module with only the dependencies external', () => {
111
+ expect(bundle.errors).toEqual([]);
112
+ expect(bundle.warnings).toEqual([]);
113
+ });
114
+
115
+ it('never imports the server-only source-record sync (the @proteinjs/db/source subpath)', () => {
116
+ expect(bundle.modules.length).toBeGreaterThan(0);
117
+ expect(bundle.modules.filter((module) => SERVER_ONLY_SUBPATH.test(module))).toEqual([]);
118
+ });
119
+
120
+ it('pins the crypto exception to exactly its four readers', () => {
121
+ expect(bundle.cryptoReaders).toEqual(CRYPTO_READERS.slice().sort());
122
+ });
123
+ });
@@ -23,13 +23,18 @@ type Row = {
23
23
  const table = { name: 'pager_test' } as Table<Row & { created: moment.Moment; updated: moment.Moment }>;
24
24
  type PagerRow = Row & { created: moment.Moment; updated: moment.Moment };
25
25
 
26
- const staged: { rows: Row[] } = { rows: [] };
26
+ const staged: { rows: Row[]; count: number } = { rows: [], count: 0 };
27
27
  const executed: QueryBuilder<PagerRow>[] = [];
28
+ const counted: QueryBuilder<PagerRow>[] = [];
28
29
  const stubDb = {
29
30
  query: async (_table: unknown, qb: QueryBuilder<PagerRow>) => {
30
31
  executed.push(qb);
31
32
  return staged.rows;
32
33
  },
34
+ getRowCount: async (_table: unknown, qb: QueryBuilder<PagerRow>) => {
35
+ counted.push(qb);
36
+ return staged.count;
37
+ },
33
38
  } as unknown as Db;
34
39
 
35
40
  const createQuery = () => new QueryBuilder<PagerRow>(table.name);
@@ -183,3 +188,45 @@ describe('loadWindow', () => {
183
188
  await expect(pager().loadWindow(null, 0)).rejects.toThrow(/window size/i);
184
189
  });
185
190
  });
191
+
192
+ describe('countPast — the rows past an anchor, counted under the windows’ own WHERE', () => {
193
+ beforeEach(() => {
194
+ counted.length = 0;
195
+ });
196
+
197
+ it('an anchored count carries the lexicographic continuation and nothing else: no sort, no pagination; the db’s COUNT is the answer', async () => {
198
+ staged.count = 26;
199
+ const remaining = await pager([{ field: 'seq', desc: true }]).countPast({ values: [40, 'row-4'] });
200
+ expect(remaining).toBe(26);
201
+ expect(counted).toHaveLength(1);
202
+ const qb = counted[0];
203
+ // The same continuation a window after this anchor reads with: seq < 40 OR (seq = 40 AND id > 'row-4').
204
+ expect(conditionNodes(qb)).toEqual(
205
+ expect.arrayContaining([
206
+ expect.objectContaining({ field: 'seq', operator: '<', value: 40 }),
207
+ expect.objectContaining({ field: 'seq', operator: '=', value: 40 }),
208
+ expect.objectContaining({ field: 'id', operator: '>', value: 'row-4' }),
209
+ ])
210
+ );
211
+ expect(conditionNodes(qb)).toHaveLength(3);
212
+ expect(paginationNode(qb)).toBeUndefined();
213
+ expect(sortNodes(qb)).toHaveLength(0);
214
+ });
215
+
216
+ it('a null anchor counts the whole data set: no conditions beyond the consumer’s own', async () => {
217
+ staged.count = 3;
218
+ expect(await pager([{ field: 'seq', desc: true }]).countPast(null)).toBe(3);
219
+ expect(counted).toHaveLength(1);
220
+ expect(conditionNodes(counted[0])).toHaveLength(0);
221
+ expect(paginationNode(counted[0])).toBeUndefined();
222
+ });
223
+
224
+ it('every count builds on a FRESH query — a window’s cursor conditions never leak into it', async () => {
225
+ const instance = pager([{ field: 'seq', desc: true }]);
226
+ staged.rows = [];
227
+ await instance.loadWindow({ values: [40, 'row-4'] }, 10);
228
+ staged.count = 0;
229
+ await instance.countPast(null);
230
+ expect(conditionNodes(counted[0])).toHaveLength(0);
231
+ });
232
+ });
@@ -10,11 +10,12 @@ import { graphSerializer } from '@proteinjs/util';
10
10
  * name, so a second declaration of the name anywhere in the package's reflected sources leaves
11
11
  * which one the artifact records to the build's file order — and the loser disappears without
12
12
  * a trace. These tests read the artifacts the build wrote (`generated/index.ts` for the package
13
- * entry, `generated/test/index.ts` for the `./test` subpath, both built from `src/`) and assert
14
- * the name holds the declaration interface, not some other declaration that shares it.
13
+ * entry, `generated/test/index.ts` for the `./test` subpath, `generated/source/index.ts` for the
14
+ * `./source` subpath — each built over `src/`) and assert the name holds the declaration
15
+ * interface, not some other declaration that shares it.
15
16
  */
16
17
  const QUALIFIED_NAME = '@proteinjs/db/SourceRecordLoader';
17
- const ARTIFACTS = ['generated/index.ts', 'generated/test/index.ts'];
18
+ const ARTIFACTS = ['generated/index.ts', 'generated/test/index.ts', 'generated/source/index.ts'];
18
19
 
19
20
  /** The fields of an emitted node value these tests read. */
20
21
  type EmittedDeclaration = { packageName: string; name: string; qualifiedName: string; sourceType: SourceType };
@@ -1,4 +1,4 @@
1
- import { SourceRecordSyncRunner } from '../src/source/SourceRecordSyncRunner';
1
+ import { SourceRecordSyncRunner } from '../source/SourceRecordSyncRunner';
2
2
 
3
3
  /**
4
4
  * Covers the two helpers that drive SourceRecordSyncRunner's change-detection: