@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.
- package/CHANGELOG.md +2311 -0
- package/README.md +557 -11
- package/dist/index.cjs +1663 -463
- package/dist/index.d.cts +1780 -518
- package/dist/index.d.ts +1780 -518
- package/dist/index.js +1648 -457
- package/package.json +9 -9
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/aggregate-capability.test.ts +0 -177
- package/src/apiAccessDenied.test.ts +0 -233
- package/src/appAccessProbe.test.ts +0 -147
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/deleteView.homes.test.ts +0 -389
- package/src/deleteViewContract.types.test.ts +0 -246
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -135
- package/src/getObjectSchema.test.ts +0 -145
- package/src/httpFailureLogging.test.ts +0 -109
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -4442
- package/src/integration.ts +0 -200
- package/src/listViewOverrides.test.ts +0 -241
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.get-envelope.test.ts +0 -171
- package/src/metadata-client.saveAdvisories.test.ts +0 -226
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -1046
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -589
- package/src/onSaveAdvisory.test.ts +0 -311
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -543
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -326
- package/src/studio.ts +0 -152
- package/src/updateView.draft.test.ts +0 -242
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -229
- package/src/validation-error.test.ts +0 -94
- package/src/viewCacheInvalidation.pin.test.ts +0 -260
- 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
|
-
});
|
package/src/connection.test.ts
DELETED
|
@@ -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
|
-
});
|