@object-ui/data-objectstack 17.5.0 → 17.7.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 (53) hide show
  1. package/CHANGELOG.md +2311 -0
  2. package/README.md +557 -11
  3. package/dist/index.cjs +1663 -463
  4. package/dist/index.d.cts +1780 -518
  5. package/dist/index.d.ts +1780 -518
  6. package/dist/index.js +1648 -457
  7. package/package.json +9 -9
  8. package/dist/tsconfig.tsbuildinfo +0 -1
  9. package/src/aggregate-capability.test.ts +0 -177
  10. package/src/apiAccessDenied.test.ts +0 -233
  11. package/src/appAccessProbe.test.ts +0 -147
  12. package/src/bulkDelete.test.ts +0 -75
  13. package/src/bulkUpdate.test.ts +0 -93
  14. package/src/cache/MetadataCache.test.ts +0 -477
  15. package/src/cache/MetadataCache.ts +0 -289
  16. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  17. package/src/connection.test.ts +0 -188
  18. package/src/contracts.ts +0 -115
  19. package/src/deleteView.homes.test.ts +0 -389
  20. package/src/deleteViewContract.types.test.ts +0 -246
  21. package/src/errors.test.ts +0 -426
  22. package/src/errors.ts +0 -293
  23. package/src/exportDownload.test.ts +0 -141
  24. package/src/filter-entry-translation.test.ts +0 -360
  25. package/src/filter-operator-ast-parity.test.ts +0 -135
  26. package/src/getObjectSchema.test.ts +0 -145
  27. package/src/httpFailureLogging.test.ts +0 -109
  28. package/src/importJob.test.ts +0 -71
  29. package/src/index.ts +0 -4442
  30. package/src/integration.ts +0 -200
  31. package/src/listViewOverrides.test.ts +0 -241
  32. package/src/listViews.test.ts +0 -175
  33. package/src/metadata-client.get-envelope.test.ts +0 -171
  34. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  35. package/src/metadata-client.test.ts +0 -303
  36. package/src/metadata-client.ts +0 -1046
  37. package/src/occ.test.ts +0 -101
  38. package/src/onMutation.test.ts +0 -589
  39. package/src/onSaveAdvisory.test.ts +0 -311
  40. package/src/onWriteWarning.test.ts +0 -273
  41. package/src/orderby-serialization.test.ts +0 -115
  42. package/src/queryDataset.test.ts +0 -543
  43. package/src/searchAll.test.ts +0 -120
  44. package/src/security.ts +0 -243
  45. package/src/spec-symbol-batch6.test.ts +0 -326
  46. package/src/studio.ts +0 -152
  47. package/src/updateView.draft.test.ts +0 -242
  48. package/src/userState.test.ts +0 -315
  49. package/src/userState.ts +0 -238
  50. package/src/v3-compat.test.ts +0 -229
  51. package/src/validation-error.test.ts +0 -94
  52. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  53. package/src/viewInvalidationSeam.pin.test.ts +0 -162
@@ -1,289 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Represents a cached schema entry with metadata
11
- */
12
- interface CachedSchema {
13
- data: unknown;
14
- timestamp: number;
15
- accessCount: number;
16
- lastAccessed: number;
17
- }
18
-
19
- /**
20
- * Statistics reported by {@link MetadataCache.getStats} for monitoring.
21
- *
22
- * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
23
- * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
24
- * describes the platform's `ICacheService` — a server-side KV cache measured by
25
- * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
26
- * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
27
- * concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
28
- * Neither type has a key the other has, so this is a name collision, not a
29
- * dialect — deriving would have replaced every field.
30
- */
31
- export interface MetadataCacheStats {
32
- size: number;
33
- maxSize: number;
34
- hits: number;
35
- misses: number;
36
- evictions: number;
37
- /** Number of concurrent fetches that were coalesced onto an in-flight request. */
38
- coalesced: number;
39
- hitRate: number;
40
- }
41
-
42
- /**
43
- * MetadataCache - LRU cache with TTL expiration for schema metadata
44
- *
45
- * Features:
46
- * - LRU (Least Recently Used) eviction policy
47
- * - TTL (Time To Live) based expiration (fixed from creation, not sliding)
48
- * - Memory limit controls
49
- * - Async-safe operations
50
- * - Performance statistics tracking
51
- *
52
- * Concurrent requests for the same uncached key are deduplicated via an
53
- * internal in-flight Promise map: only the first call invokes the fetcher,
54
- * and subsequent callers receive the same Promise.
55
- *
56
- * @example
57
- * ```typescript
58
- * const cache = new MetadataCache({ maxSize: 100, ttl: 300000 });
59
- *
60
- * const schema = await cache.get('users', async () => {
61
- * return await fetchSchemaFromServer('users');
62
- * });
63
- *
64
- * console.log(cache.getStats());
65
- * ```
66
- */
67
- export class MetadataCache {
68
- private cache: Map<string, CachedSchema>;
69
- private inflight: Map<string, Promise<unknown>>;
70
- private maxSize: number;
71
- private ttl: number;
72
- private stats: {
73
- hits: number;
74
- misses: number;
75
- evictions: number;
76
- coalesced: number;
77
- };
78
-
79
- /**
80
- * Create a new MetadataCache instance
81
- *
82
- * @param options - Configuration options
83
- * @param options.maxSize - Maximum number of entries (default: 100)
84
- * @param options.ttl - Time to live in milliseconds (default: 5 minutes)
85
- */
86
- constructor(options: { maxSize?: number; ttl?: number } = {}) {
87
- this.cache = new Map();
88
- this.inflight = new Map();
89
- this.maxSize = options.maxSize || 100;
90
- this.ttl = options.ttl || 5 * 60 * 1000; // 5 minutes default
91
- this.stats = {
92
- hits: 0,
93
- misses: 0,
94
- evictions: 0,
95
- coalesced: 0,
96
- };
97
- }
98
-
99
- /**
100
- * Get a value from cache or fetch it using the provided fetcher function
101
- *
102
- * @param key - Cache key
103
- * @param fetcher - Async function to fetch data if not in cache
104
- * @returns Promise resolving to the cached or fetched data
105
- */
106
- async get<T = unknown>(key: string, fetcher: () => Promise<T>): Promise<T> {
107
- const now = Date.now();
108
- const cached = this.cache.get(key);
109
-
110
- // Check if cache entry exists and is not expired
111
- if (cached) {
112
- const age = now - cached.timestamp;
113
-
114
- if (age < this.ttl) {
115
- // Cache hit - update access metadata
116
- cached.accessCount++;
117
- cached.lastAccessed = now;
118
- this.stats.hits++;
119
-
120
- // Move to end (most recently used) by re-inserting
121
- this.cache.delete(key);
122
- this.cache.set(key, cached);
123
-
124
- return cached.data as T;
125
- } else {
126
- // Expired entry - remove it
127
- this.cache.delete(key);
128
- }
129
- }
130
-
131
- // Cache miss - dedupe concurrent fetches for the same key
132
- const existing = this.inflight.get(key);
133
- if (existing) {
134
- this.stats.coalesced++;
135
- return existing as Promise<T>;
136
- }
137
-
138
- this.stats.misses++;
139
- const promise = (async () => {
140
- try {
141
- const data = await fetcher();
142
- this.set(key, data);
143
- return data;
144
- } finally {
145
- this.inflight.delete(key);
146
- }
147
- })();
148
- this.inflight.set(key, promise as Promise<unknown>);
149
- return promise;
150
- }
151
-
152
- /**
153
- * Prime the cache with a pre-fetched value. Useful when a bulk endpoint
154
- * (e.g. list of all object schemas) returns data that would otherwise
155
- * be fetched again per item.
156
- */
157
- prime(key: string, data: unknown): void {
158
- this.set(key, data);
159
- }
160
-
161
- /**
162
- * Set a value in the cache
163
- *
164
- * @param key - Cache key
165
- * @param data - Data to cache
166
- */
167
- private set(key: string, data: unknown): void {
168
- const now = Date.now();
169
-
170
- // Check if we need to evict entries
171
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
172
- this.evictLRU();
173
- }
174
-
175
- // Add or update the entry
176
- this.cache.set(key, {
177
- data,
178
- timestamp: now,
179
- accessCount: 1,
180
- lastAccessed: now,
181
- });
182
- }
183
-
184
- /**
185
- * Evict the least recently used entry
186
- */
187
- private evictLRU(): void {
188
- // The first entry in the Map is the least recently used
189
- // (since we move accessed items to the end)
190
- const firstKey = this.cache.keys().next().value;
191
-
192
- if (firstKey !== undefined) {
193
- this.cache.delete(firstKey);
194
- this.stats.evictions++;
195
- }
196
- }
197
-
198
- /**
199
- * Invalidate a specific cache entry or all entries
200
- *
201
- * @param key - Optional key to invalidate. If omitted, invalidates all entries
202
- */
203
- invalidate(key?: string): void {
204
- if (key) {
205
- this.cache.delete(key);
206
- } else {
207
- this.cache.clear();
208
- }
209
- }
210
-
211
- /**
212
- * Clear all cache entries and reset statistics
213
- */
214
- clear(): void {
215
- this.cache.clear();
216
- this.inflight.clear();
217
- this.stats = {
218
- hits: 0,
219
- misses: 0,
220
- evictions: 0,
221
- coalesced: 0,
222
- };
223
- }
224
-
225
- /**
226
- * Get cache statistics
227
- *
228
- * @returns Cache statistics including hit rate
229
- */
230
- getStats(): MetadataCacheStats {
231
- const total = this.stats.hits + this.stats.misses;
232
- const hitRate = total > 0 ? this.stats.hits / total : 0;
233
-
234
- return {
235
- size: this.cache.size,
236
- maxSize: this.maxSize,
237
- hits: this.stats.hits,
238
- misses: this.stats.misses,
239
- evictions: this.stats.evictions,
240
- coalesced: this.stats.coalesced,
241
- hitRate: hitRate,
242
- };
243
- }
244
-
245
- /**
246
- * Get a cached value synchronously without triggering a fetch.
247
- * Returns undefined if not in cache or expired.
248
- */
249
- getCachedSync<V = unknown>(key: string): V | undefined {
250
- const entry = this.cache.get(key);
251
- if (!entry) return undefined;
252
-
253
- // Check TTL
254
- if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) {
255
- this.cache.delete(key);
256
- return undefined;
257
- }
258
-
259
- // Update access order for LRU
260
- this.cache.delete(key);
261
- this.cache.set(key, entry);
262
- this.stats.hits++;
263
-
264
- return entry.data as V;
265
- }
266
-
267
- /**
268
- * Check if a key exists in the cache (and is not expired)
269
- *
270
- * @param key - Cache key to check
271
- * @returns true if the key exists and is not expired
272
- */
273
- has(key: string): boolean {
274
- const cached = this.cache.get(key);
275
-
276
- if (!cached) {
277
- return false;
278
- }
279
-
280
- const age = Date.now() - cached.timestamp;
281
-
282
- if (age >= this.ttl) {
283
- this.cache.delete(key);
284
- return false;
285
- }
286
-
287
- return true;
288
- }
289
- }
@@ -1,171 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * The retired cloud surface stays retired — negative pin (objectui#4152).
11
- *
12
- * `packages/data-objectstack/src/cloud.ts` exported a `CloudOperations` class
13
- * and three `Cloud*` types, all re-exported from this package's `index.ts`, so
14
- * they were published surface of `@object-ui/data-objectstack`. Every one of
15
- * the class's four methods optional-chained into `client.cloud?.…` — a
16
- * namespace no released `@objectstack/client` has ever exported (measured again
17
- * at `17.0.0-rc.6` while removing it: the module exports exactly
18
- * `ObjectStackClient`, `ScopedProjectClient`, `RealtimeAPI`, `QueryBuilder`,
19
- * `FilterBuilder`, `createQuery`, `createFilter`, and a constructed instance's
20
- * `.cloud` is `undefined`) — and then returned a fallback literal:
21
- *
22
- * | method | what it returned, always |
23
- * |:--|:--|
24
- * | `deploy` | `{ deploymentId: 'deploy-' + Date.now(), status: 'pending' }` |
25
- * | `getDeploymentStatus` | `{ status: 'unknown' }` |
26
- * | `searchMarketplace` | `[]` |
27
- * | `installPlugin` | `{ success: false }` |
28
- *
29
- * The maintainer ruling of 2026-08-11 removed it. The reason worth keeping in
30
- * front of whoever reads this file: `deploy()` did not degrade to an error, it
31
- * **manufactured a plausible success**. A caller — especially an AI one, which
32
- * will not get suspicious — received a well-formed `deploymentId` for an
33
- * operation that never left the process, then polled it forever against
34
- * `{ status: 'unknown' }`. Fabricated success is banned by that ruling; a stub
35
- * that must exist for compile compatibility throws instead.
36
- *
37
- * ## Why a negative pin, and why it reads BOTH surfaces
38
- *
39
- * Nothing else can see this regression. The gates in this repo run from a
40
- * declaration to its usage; a re-added export has a declaration and no usage,
41
- * which is exactly the shape that has no alarm on it — the same gap
42
- * objectui#4145's retirement pin was built for.
43
- *
44
- * The two halves are not redundant, because a class and a type do not fail the
45
- * same way:
46
- *
47
- * - the **runtime** half enumerates `./index`'s bindings, which catches
48
- * `CloudOperations` (a class survives to runtime) but is structurally blind
49
- * to `CloudDeploymentConfig` and friends, since `export type` erases;
50
- * - the **source-text** half reads `index.ts` for the names and for a
51
- * re-export of `./cloud`, which is the only instrument that can see a
52
- * returning *type*.
53
- *
54
- * Drop either one and half the retired surface can come back green.
55
- *
56
- * ## Why the source is read through `ts.sys` rather than `node:fs`
57
- *
58
- * Inherited from the pin this file replaces: this package's `tsconfig.json`
59
- * compiles its whole test tree, and keeping `@types/node` out of a browser-side
60
- * adapter's type environment is deliberate. TypeScript's own system host is
61
- * typed by the `typescript` devDependency.
62
- *
63
- * ## What happened to objectui#3720's pin
64
- *
65
- * `cloud-environment-vocabulary.pin.test.ts` pinned the doc comment on
66
- * `CloudDeploymentConfig.environment` — the deploy-target vocabulary and the
67
- * `staging` trap. Its subject was deleted here, so it retired with it rather
68
- * than being re-pointed: every fact it held was a claim ABOUT that comment, and
69
- * its spec-side assertions (`DiscoveryEnvironmentSchema` / `EnvironmentTypeSchema`
70
- * member lists) existed only to keep those claims honest — with the comment
71
- * gone they would pin an upstream library's enums on behalf of no local reader,
72
- * which is the same phantom shape objectui#4152 is closing. #3720's own
73
- * conclusion is unaffected and now moot: it found no producer-side deploy-target
74
- * type to converge onto, because the producer did not exist. This file records
75
- * the same measurement one step further along.
76
- *
77
- * ## If a real cloud API lands
78
- *
79
- * Then this pin is what you delete, deliberately, in the PR that binds the new
80
- * `@objectstack/client` surface — with methods that call it and fail loudly when
81
- * it is absent. Re-adding the names alone, ahead of a producer, is the thing
82
- * being prevented.
83
- */
84
-
85
- import { describe, it, expect } from 'vitest';
86
- import ts from 'typescript';
87
-
88
- import * as adapter from './index';
89
-
90
- /** Every name the retirement removed from the package's public surface. */
91
- const RETIRED_EXPORTS = [
92
- 'CloudOperations',
93
- 'CloudDeploymentConfig',
94
- 'CloudHostingConfig',
95
- 'CloudMarketplaceEntry',
96
- ] as const;
97
-
98
- /** This file, as an absolute path — `index.ts` is its sibling. */
99
- const HERE = new URL(import.meta.url).pathname;
100
- const INDEX_TS = HERE.replace(/[^/]+$/, 'index.ts');
101
- const CLOUD_TS = HERE.replace(/[^/]+$/, 'cloud.ts');
102
-
103
- function readIndexSource(): string {
104
- const raw = ts.sys.readFile(INDEX_TS);
105
- if (!raw) throw new Error(`cannot read ${INDEX_TS}`);
106
- return raw;
107
- }
108
-
109
- const INDEX_SOURCE = readIndexSource();
110
-
111
- describe('the probe itself works', () => {
112
- it('read a plausible index.ts', () => {
113
- expect(INDEX_SOURCE.length).toBeGreaterThan(1000);
114
- expect(INDEX_SOURCE).toContain('export class ObjectStackAdapter');
115
- });
116
- });
117
-
118
- describe('the cloud surface is gone from the runtime exports', () => {
119
- it.each(RETIRED_EXPORTS)('`%s` is not a runtime binding of the package entry', (name) => {
120
- expect(
121
- Object.prototype.hasOwnProperty.call(adapter, name),
122
- `\`${name}\` is exported from @object-ui/data-objectstack again. It was ` +
123
- `retired by objectui#4152 because it optional-chained into a ` +
124
- `\`client.cloud\` namespace @objectstack/client does not have, and ` +
125
- `returned a fabricated success instead of failing. If a real cloud API ` +
126
- `has landed, bind it and delete this pin deliberately — do not restore ` +
127
- `the old fallbacks.`,
128
- ).toBe(false);
129
- });
130
-
131
- it('exports no other `Cloud`-prefixed binding either', () => {
132
- const strays = Object.keys(adapter).filter((name) => name.startsWith('Cloud'));
133
- expect(
134
- strays,
135
- `unexpected \`Cloud*\` export(s) on the package entry. objectui#4152 ` +
136
- `removed this prefix wholesale; a new one needs a producer behind it.`,
137
- ).toEqual([]);
138
- });
139
- });
140
-
141
- describe('the cloud surface is gone from the source, types included', () => {
142
- it('index.ts re-exports nothing from `./cloud`', () => {
143
- expect(
144
- /from\s+'\.\/cloud'/.test(INDEX_SOURCE),
145
- `index.ts re-exports from './cloud' again (objectui#4152 removed that ` +
146
- `module). \`export type\` erases at runtime, so the assertions above ` +
147
- `cannot see a returning type — this one is what catches it.`,
148
- ).toBe(false);
149
- });
150
-
151
- it.each(RETIRED_EXPORTS)('index.ts does not name `%s`', (name) => {
152
- // The retirement note left behind in index.ts mentions the family in prose
153
- // (`Cloud*Config`), never a retired identifier, so a hit here is a real
154
- // re-export rather than the comment.
155
- expect(
156
- INDEX_SOURCE.includes(name),
157
- `index.ts mentions \`${name}\` again. If this is a comment rather than an ` +
158
- `export, rewrite the comment — this pin deliberately cannot tell the ` +
159
- `difference, because a type export is invisible to every other check.`,
160
- ).toBe(false);
161
- });
162
-
163
- it('the cloud.ts module itself is gone', () => {
164
- expect(
165
- ts.sys.fileExists(CLOUD_TS),
166
- `${CLOUD_TS} is back. The module header claimed to integrate ` +
167
- `"@objectstack/spec v3.0.0" cloud schemas the package never consumed; ` +
168
- `objectui#4152 deleted both the claim and the surface.`,
169
- ).toBe(false);
170
- });
171
- });
@@ -1,188 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, beforeEach, vi } from 'vitest';
10
- import {
11
- ObjectStackAdapter,
12
- ConnectionState,
13
- ConnectionStateEvent,
14
- BatchProgressEvent,
15
- clearSharedDiscoveryCache,
16
- } from './index';
17
-
18
- describe('Connection State Monitoring', () => {
19
- let adapter: ObjectStackAdapter;
20
-
21
- beforeEach(() => {
22
- adapter = new ObjectStackAdapter({
23
- baseUrl: 'http://localhost:3000',
24
- autoReconnect: false, // Disable auto-reconnect for testing
25
- });
26
- });
27
-
28
- it('should initialize with disconnected state', () => {
29
- expect(adapter.getConnectionState()).toBe('disconnected');
30
- expect(adapter.isConnected()).toBe(false);
31
- });
32
-
33
- it('should allow subscribing to connection state changes', () => {
34
- const listener = vi.fn();
35
- const unsubscribe = adapter.onConnectionStateChange(listener);
36
-
37
- expect(typeof unsubscribe).toBe('function');
38
- expect(listener).not.toHaveBeenCalled();
39
-
40
- // Cleanup
41
- unsubscribe();
42
- });
43
-
44
- it('should allow subscribing to batch progress events', () => {
45
- const listener = vi.fn();
46
- const unsubscribe = adapter.onBatchProgress(listener);
47
-
48
- expect(typeof unsubscribe).toBe('function');
49
- expect(listener).not.toHaveBeenCalled();
50
-
51
- // Cleanup
52
- unsubscribe();
53
- });
54
-
55
- it('should unsubscribe connection state listener', () => {
56
- const listener = vi.fn();
57
- const unsubscribe = adapter.onConnectionStateChange(listener);
58
-
59
- // Unsubscribe
60
- unsubscribe();
61
-
62
- // Listener should not be called after unsubscribe
63
- // (We can't easily test this without triggering a connection state change)
64
- });
65
-
66
- it('should unsubscribe batch progress listener', () => {
67
- const listener = vi.fn();
68
- const unsubscribe = adapter.onBatchProgress(listener);
69
-
70
- // Unsubscribe
71
- unsubscribe();
72
-
73
- // Listener should not be called after unsubscribe
74
- });
75
-
76
- it('should support auto-reconnect configuration', () => {
77
- const adapterWithReconnect = new ObjectStackAdapter({
78
- baseUrl: 'http://localhost:3000',
79
- autoReconnect: true,
80
- maxReconnectAttempts: 5,
81
- reconnectDelay: 2000,
82
- });
83
-
84
- expect(adapterWithReconnect.getConnectionState()).toBe('disconnected');
85
- });
86
- });
87
-
88
- describe('Batch Progress Events', () => {
89
- let adapter: ObjectStackAdapter;
90
-
91
- beforeEach(() => {
92
- adapter = new ObjectStackAdapter({
93
- baseUrl: 'http://localhost:3000',
94
- });
95
- });
96
-
97
- it('should allow subscribing to batch progress', () => {
98
- const listener = vi.fn();
99
- const unsubscribe = adapter.onBatchProgress(listener);
100
-
101
- expect(typeof unsubscribe).toBe('function');
102
-
103
- // Cleanup
104
- unsubscribe();
105
- });
106
- });
107
-
108
- describe('getDiscovery', () => {
109
- beforeEach(() => {
110
- clearSharedDiscoveryCache();
111
- });
112
-
113
- it('should return discoveryInfo from the underlying client after connect', async () => {
114
- const mockDiscovery = {
115
- name: 'test-server',
116
- version: '1.0.0',
117
- services: {
118
- auth: { enabled: false, status: 'unavailable' },
119
- data: { enabled: true, status: 'available' },
120
- },
121
- };
122
-
123
- const fetchImpl = vi.fn(async () =>
124
- ({
125
- ok: true,
126
- json: async () => ({ success: true, data: mockDiscovery }),
127
- }) as unknown as Response,
128
- );
129
-
130
- const adapter = new ObjectStackAdapter({
131
- baseUrl: 'http://localhost:3000',
132
- autoReconnect: false,
133
- fetch: fetchImpl,
134
- });
135
-
136
- await adapter.connect();
137
-
138
- const discovery = await adapter.getDiscovery();
139
- expect(discovery).toEqual(mockDiscovery);
140
- expect((discovery as any)?.services?.auth?.enabled).toBe(false);
141
- expect(fetchImpl).toHaveBeenCalledTimes(1);
142
- expect((fetchImpl.mock.calls[0] as unknown as unknown[])[0]).toContain('/api/v1/discovery');
143
- });
144
-
145
- it('should return null when connection fails', async () => {
146
- const fetchImpl = vi.fn(async () => {
147
- throw new Error('Connection failed');
148
- });
149
-
150
- const adapter = new ObjectStackAdapter({
151
- baseUrl: 'http://localhost:3000',
152
- autoReconnect: false,
153
- fetch: fetchImpl as any,
154
- });
155
-
156
- await expect(adapter.connect()).rejects.toThrow();
157
-
158
- const discovery = await adapter.getDiscovery();
159
- expect(discovery).toBeNull();
160
- });
161
-
162
- it('should share a single discovery fetch across adapters with the same baseUrl', async () => {
163
- const mockDiscovery = { name: 'shared', version: '1.0.0' };
164
- const fetchImpl = vi.fn(async () =>
165
- ({
166
- ok: true,
167
- json: async () => ({ success: true, data: mockDiscovery }),
168
- }) as unknown as Response,
169
- );
170
-
171
- const a1 = new ObjectStackAdapter({
172
- baseUrl: 'http://localhost:3000',
173
- autoReconnect: false,
174
- fetch: fetchImpl,
175
- });
176
- const a2 = new ObjectStackAdapter({
177
- baseUrl: 'http://localhost:3000',
178
- autoReconnect: false,
179
- fetch: fetchImpl,
180
- });
181
-
182
- await Promise.all([a1.connect(), a2.connect()]);
183
-
184
- expect(fetchImpl).toHaveBeenCalledTimes(1);
185
- expect(await a1.getDiscovery()).toEqual(mockDiscovery);
186
- expect(await a2.getDiscovery()).toEqual(mockDiscovery);
187
- });
188
- });