@object-ui/data-objectstack 3.3.1 → 3.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 3.4.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [f1ca238]
8
+ - Updated dependencies [de881ef]
9
+ - @object-ui/types@3.4.0
10
+ - @object-ui/core@3.4.0
11
+
12
+ ## 3.3.2
13
+
14
+ ### Patch Changes
15
+
16
+ - @object-ui/types@3.3.2
17
+ - @object-ui/core@3.3.2
18
+
3
19
  ## 3.3.1
4
20
 
5
21
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -844,6 +844,13 @@ async function getSharedDiscovery(baseUrl, fetcher) {
844
844
  function clearSharedDiscoveryCache() {
845
845
  discoveryCache.clear();
846
846
  }
847
+ function stableStringify(value) {
848
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
849
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
850
+ const obj = value;
851
+ const keys = Object.keys(obj).sort();
852
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
853
+ }
847
854
  var ObjectStackAdapter = class {
848
855
  constructor(config) {
849
856
  __publicField(this, "client");
@@ -860,6 +867,11 @@ var ObjectStackAdapter = class {
860
867
  __publicField(this, "baseUrl");
861
868
  __publicField(this, "token");
862
869
  __publicField(this, "fetchImpl");
870
+ // In-flight find() requests keyed by resource + serialized params.
871
+ // Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
872
+ // multiple sibling components requesting the same dataset on first paint)
873
+ // into a single network round trip.
874
+ __publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
863
875
  this.client = new import_client.ObjectStackClient(config);
864
876
  this.metadataCache = new MetadataCache(config.cache);
865
877
  this.autoReconnect = config.autoReconnect ?? true;
@@ -997,14 +1009,26 @@ var ObjectStackAdapter = class {
997
1009
  * Converts OData-style params to ObjectStack query options.
998
1010
  */
999
1011
  async find(resource, params) {
1000
- await this.connect();
1001
- if (params?.$expand && params.$expand.length > 0) {
1002
- const result2 = await this.rawFindWithPopulate(resource, params);
1003
- return this.normalizeQueryResult(result2, params);
1004
- }
1005
- const queryOptions = this.convertQueryParams(params);
1006
- const result = await this.client.data.find(resource, queryOptions);
1007
- return this.normalizeQueryResult(result, params);
1012
+ const key = `${resource}::${stableStringify(params)}`;
1013
+ const existing = this.inflightFinds.get(key);
1014
+ if (existing) return existing;
1015
+ const promise = (async () => {
1016
+ await this.connect();
1017
+ if (params?.$expand && params.$expand.length > 0) {
1018
+ const result2 = await this.rawFindWithPopulate(resource, params);
1019
+ return this.normalizeQueryResult(result2, params);
1020
+ }
1021
+ const queryOptions = this.convertQueryParams(params);
1022
+ const result = await this.client.data.find(resource, queryOptions);
1023
+ return this.normalizeQueryResult(result, params);
1024
+ })();
1025
+ this.inflightFinds.set(key, promise);
1026
+ promise.finally(() => {
1027
+ if (this.inflightFinds.get(key) === promise) {
1028
+ this.inflightFinds.delete(key);
1029
+ }
1030
+ });
1031
+ return promise;
1008
1032
  }
1009
1033
  /**
1010
1034
  * Find a single record by ID.
package/dist/index.d.cts CHANGED
@@ -723,6 +723,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
723
723
  private baseUrl;
724
724
  private token?;
725
725
  private fetchImpl;
726
+ private inflightFinds;
726
727
  constructor(config: {
727
728
  baseUrl: string;
728
729
  token?: string;
package/dist/index.d.ts CHANGED
@@ -723,6 +723,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
723
723
  private baseUrl;
724
724
  private token?;
725
725
  private fetchImpl;
726
+ private inflightFinds;
726
727
  constructor(config: {
727
728
  baseUrl: string;
728
729
  token?: string;
package/dist/index.js CHANGED
@@ -802,6 +802,13 @@ async function getSharedDiscovery(baseUrl, fetcher) {
802
802
  function clearSharedDiscoveryCache() {
803
803
  discoveryCache.clear();
804
804
  }
805
+ function stableStringify(value) {
806
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
807
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
808
+ const obj = value;
809
+ const keys = Object.keys(obj).sort();
810
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
811
+ }
805
812
  var ObjectStackAdapter = class {
806
813
  constructor(config) {
807
814
  __publicField(this, "client");
@@ -818,6 +825,11 @@ var ObjectStackAdapter = class {
818
825
  __publicField(this, "baseUrl");
819
826
  __publicField(this, "token");
820
827
  __publicField(this, "fetchImpl");
828
+ // In-flight find() requests keyed by resource + serialized params.
829
+ // Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
830
+ // multiple sibling components requesting the same dataset on first paint)
831
+ // into a single network round trip.
832
+ __publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
821
833
  this.client = new ObjectStackClient(config);
822
834
  this.metadataCache = new MetadataCache(config.cache);
823
835
  this.autoReconnect = config.autoReconnect ?? true;
@@ -955,14 +967,26 @@ var ObjectStackAdapter = class {
955
967
  * Converts OData-style params to ObjectStack query options.
956
968
  */
957
969
  async find(resource, params) {
958
- await this.connect();
959
- if (params?.$expand && params.$expand.length > 0) {
960
- const result2 = await this.rawFindWithPopulate(resource, params);
961
- return this.normalizeQueryResult(result2, params);
962
- }
963
- const queryOptions = this.convertQueryParams(params);
964
- const result = await this.client.data.find(resource, queryOptions);
965
- return this.normalizeQueryResult(result, params);
970
+ const key = `${resource}::${stableStringify(params)}`;
971
+ const existing = this.inflightFinds.get(key);
972
+ if (existing) return existing;
973
+ const promise = (async () => {
974
+ await this.connect();
975
+ if (params?.$expand && params.$expand.length > 0) {
976
+ const result2 = await this.rawFindWithPopulate(resource, params);
977
+ return this.normalizeQueryResult(result2, params);
978
+ }
979
+ const queryOptions = this.convertQueryParams(params);
980
+ const result = await this.client.data.find(resource, queryOptions);
981
+ return this.normalizeQueryResult(result, params);
982
+ })();
983
+ this.inflightFinds.set(key, promise);
984
+ promise.finally(() => {
985
+ if (this.inflightFinds.get(key) === promise) {
986
+ this.inflightFinds.delete(key);
987
+ }
988
+ });
989
+ return promise;
966
990
  }
967
991
  /**
968
992
  * Find a single record by ID.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@objectstack/client": "^4.0.4",
26
- "@object-ui/core": "3.3.1",
27
- "@object-ui/types": "3.3.1"
26
+ "@object-ui/core": "3.4.0",
27
+ "@object-ui/types": "3.4.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",
package/src/index.ts CHANGED
@@ -88,6 +88,20 @@ export type BatchProgressListener = (event: BatchProgressEvent) => void;
88
88
  // Re-export FileUploadResult from types for consumers
89
89
  export type { FileUploadResult } from '@object-ui/types';
90
90
 
91
+ /**
92
+ * Deterministic JSON.stringify with sorted object keys, used to build cache
93
+ * keys for in-flight request coalescing. Produces identical output for
94
+ * `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` so callers that build params in
95
+ * different orders still hit the same key.
96
+ */
97
+ function stableStringify(value: unknown): string {
98
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
99
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
100
+ const obj = value as Record<string, unknown>;
101
+ const keys = Object.keys(obj).sort();
102
+ return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
103
+ }
104
+
91
105
  /**
92
106
  * ObjectStack Data Source Adapter
93
107
  *
@@ -132,6 +146,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
132
146
  private baseUrl: string;
133
147
  private token?: string;
134
148
  private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
149
+ // In-flight find() requests keyed by resource + serialized params.
150
+ // Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
151
+ // multiple sibling components requesting the same dataset on first paint)
152
+ // into a single network round trip.
153
+ private inflightFinds = new Map<string, Promise<QueryResult<T>>>();
135
154
 
136
155
  constructor(config: {
137
156
  baseUrl: string;
@@ -323,22 +342,38 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
323
342
  * Converts OData-style params to ObjectStack query options.
324
343
  */
325
344
  async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
326
- await this.connect();
345
+ const key = `${resource}::${stableStringify(params)}`;
346
+ const existing = this.inflightFinds.get(key);
347
+ if (existing) return existing;
327
348
 
328
- // When $expand is requested, use a raw GET request to the REST API with
329
- // `populate` as a URL query param. The server's REST plugin routes
330
- // GET /data/:object to protocol.findData({ object, query: req.query }),
331
- // which parses `populate` (comma-separated) into an array for lookup expansion.
332
- // We use a raw request because the client SDK's data.find() QueryOptions
333
- // interface does not include populate/expand fields.
334
- if (params?.$expand && params.$expand.length > 0) {
335
- const result = await this.rawFindWithPopulate(resource, params);
349
+ const promise = (async () => {
350
+ await this.connect();
351
+
352
+ // When $expand is requested, use a raw GET request to the REST API with
353
+ // `populate` as a URL query param. The server's REST plugin routes
354
+ // GET /data/:object to protocol.findData({ object, query: req.query }),
355
+ // which parses `populate` (comma-separated) into an array for lookup expansion.
356
+ // We use a raw request because the client SDK's data.find() QueryOptions
357
+ // interface does not include populate/expand fields.
358
+ if (params?.$expand && params.$expand.length > 0) {
359
+ const result = await this.rawFindWithPopulate(resource, params);
360
+ return this.normalizeQueryResult(result, params);
361
+ }
362
+
363
+ const queryOptions = this.convertQueryParams(params);
364
+ const result: unknown = await this.client.data.find<T>(resource, queryOptions);
336
365
  return this.normalizeQueryResult(result, params);
337
- }
366
+ })();
338
367
 
339
- const queryOptions = this.convertQueryParams(params);
340
- const result: unknown = await this.client.data.find<T>(resource, queryOptions);
341
- return this.normalizeQueryResult(result, params);
368
+ this.inflightFinds.set(key, promise);
369
+ promise.finally(() => {
370
+ // Only clear if the entry still points at this promise; a later call
371
+ // that started after settle may have already replaced it.
372
+ if (this.inflightFinds.get(key) === promise) {
373
+ this.inflightFinds.delete(key);
374
+ }
375
+ });
376
+ return promise;
342
377
  }
343
378
 
344
379
  /**