@expofp/resolve 3.11.11 → 3.12.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/dist/resolve.d.ts CHANGED
@@ -1,3 +1,24 @@
1
1
  import { type Ref, type ResolveOptions } from './types.js';
2
+ /**
3
+ * Resolve a `$ref` to its document. Non-ref values pass through (they ARE the
4
+ * value). A ref carrying a `process` hook has it applied to the raw document —
5
+ * that is the whole extension surface; this package knows nothing about
6
+ * schemas or assets. The result is deep-frozen and memoized by `$ref` for the
7
+ * sync {@link getResolved}; repeated resolves of the same ref reuse the fetch
8
+ * cache, so they cost no extra network round-trip. The memo is keyed by URL
9
+ * alone — resolving the same URL through refs with different `process` hooks
10
+ * settles whichever ran last.
11
+ */
2
12
  export declare function resolve<T>(data: Ref<T> | T, options?: ResolveOptions): Promise<T>;
13
+ /**
14
+ * The settled value of an already-resolved `$ref`, synchronously. Throws when
15
+ * the ref has not settled (never resolved, still in flight, or failed) — a
16
+ * boot-ordering mistake that should surface loudly. Non-ref values pass
17
+ * through. Probe with {@link isResolved} where absence is expected.
18
+ */
19
+ export declare function getResolved<T>(data: Ref<T> | T): T;
20
+ /** Whether {@link getResolved} would succeed. Non-ref values are always "resolved". */
21
+ export declare function isResolved(data: unknown): boolean;
22
+ /** Drop every cached fetch and settled value (tests; long-lived kiosk refreshes). */
23
+ export declare function clearResolveCache(): void;
3
24
  //# sourceMappingURL=resolve.d.ts.map
package/dist/resolve.js CHANGED
@@ -1,17 +1,94 @@
1
- import { importJson } from '@expofp/utils';
1
+ import { deepFreeze, importJsAsJson, importJson } from '@expofp/utils';
2
2
  const globalResolveCache = new Map();
3
+ /** Settled results by `$ref` — the sync {@link getResolved} reads these. */
4
+ const settledValues = new Map();
5
+ /**
6
+ * Resolve a `$ref` to its document. Non-ref values pass through (they ARE the
7
+ * value). A ref carrying a `process` hook has it applied to the raw document —
8
+ * that is the whole extension surface; this package knows nothing about
9
+ * schemas or assets. The result is deep-frozen and memoized by `$ref` for the
10
+ * sync {@link getResolved}; repeated resolves of the same ref reuse the fetch
11
+ * cache, so they cost no extra network round-trip. The memo is keyed by URL
12
+ * alone — resolving the same URL through refs with different `process` hooks
13
+ * settles whichever ran last.
14
+ */
3
15
  // TODO: use json pointer lib to resolve within documents
4
16
  export async function resolve(data, options) {
5
- const importOptions = {
6
- fetchCache: options?.refCache ?? globalResolveCache,
7
- forceFetch: options?.forceFetch,
8
- signal: options?.signal,
9
- importCallback: options?.importCallback,
10
- };
11
- if (typeof data !== 'object' || data === null || !('$ref' in data)) {
17
+ if (!isRefShaped(data)) {
12
18
  return data;
13
- // throw new Error('resolve: data is not a $ref object');
14
19
  }
15
- const module = await importJson(data.$ref, importOptions);
16
- return module;
20
+ const ref = data.$ref;
21
+ // Legacy `.js` refs (`data.js`, `fp.svg.js`, …) are scripts that assign
22
+ // browser globals rather than being JSON documents. Evaluate them and
23
+ // return the assigned globals.
24
+ const raw = await (isJsRef(ref)
25
+ ? resolveJsRef(ref, options)
26
+ : importJson(ref, {
27
+ fetchCache: options?.refCache ?? globalResolveCache,
28
+ forceFetch: options?.forceFetch,
29
+ signal: options?.signal,
30
+ importCallback: options?.importCallback,
31
+ }));
32
+ const value = data.process ? await data.process(raw, ref) : raw;
33
+ deepFreeze(value);
34
+ settledValues.set(ref, value);
35
+ return value;
36
+ }
37
+ /**
38
+ * The settled value of an already-resolved `$ref`, synchronously. Throws when
39
+ * the ref has not settled (never resolved, still in flight, or failed) — a
40
+ * boot-ordering mistake that should surface loudly. Non-ref values pass
41
+ * through. Probe with {@link isResolved} where absence is expected.
42
+ */
43
+ export function getResolved(data) {
44
+ if (!isRefShaped(data)) {
45
+ return data;
46
+ }
47
+ if (!settledValues.has(data.$ref)) {
48
+ throw new Error(`resolve: $ref not resolved yet: ${data.$ref}`);
49
+ }
50
+ return settledValues.get(data.$ref);
51
+ }
52
+ /** Whether {@link getResolved} would succeed. Non-ref values are always "resolved". */
53
+ export function isResolved(data) {
54
+ return isRefShaped(data) ? settledValues.has(data.$ref) : true;
55
+ }
56
+ /** Drop every cached fetch and settled value (tests; long-lived kiosk refreshes). */
57
+ export function clearResolveCache() {
58
+ globalResolveCache.clear();
59
+ settledValues.clear();
60
+ }
61
+ function isRefShaped(data) {
62
+ return (typeof data === 'object' && data !== null && '$ref' in data && typeof data.$ref === 'string');
63
+ }
64
+ /**
65
+ * Matches a `.js` path — bare (`…/data.js`) or with a query/hash
66
+ * (`…/data.js?v=1`). Anchored to the `.js` extension, not a substring test, so
67
+ * `.json` refs are left alone.
68
+ */
69
+ function isJsRef(ref) {
70
+ return /\.js(\?|#|$)/i.test(ref);
71
+ }
72
+ /** Evaluate a legacy `.js` ref to its globals, deduped through the ref cache. */
73
+ function resolveJsRef(ref, options) {
74
+ const cache = options?.refCache ?? globalResolveCache;
75
+ const cacheKey = '__loadJs__' + ref;
76
+ let promise = options?.forceFetch ? undefined : cache.get(cacheKey);
77
+ if (!promise) {
78
+ const fetched = importJsAsJson(ref, { signal: options?.signal });
79
+ cache.set(cacheKey, fetched);
80
+ // A rejection must not poison the URL for the cache's life (one transient
81
+ // fetch drop would replay as a permanent failure) — evict it so the next
82
+ // resolve retries. Guarded: a forceFetch may have replaced the entry.
83
+ fetched.catch(() => {
84
+ if (cache.get(cacheKey) === fetched)
85
+ cache.delete(cacheKey);
86
+ });
87
+ promise = fetched;
88
+ }
89
+ // Fire importCallback once per resolve call (after load), mirroring importJson.
90
+ return promise.then((result) => {
91
+ options?.importCallback?.(ref);
92
+ return result;
93
+ });
17
94
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  export interface Ref<T> {
2
2
  $ref: string;
3
+ /**
4
+ * Post-resolution hook: turns the raw fetched/evaluated document into the
5
+ * value to memoize (validate, absolutize resource URLs, …). Attached in
6
+ * code by whoever mints the ref — `JSON.stringify` drops it, so serialized
7
+ * refs stay plain `{ $ref }` data.
8
+ */
9
+ process?: (raw: unknown, refUrl: string) => unknown;
3
10
  __type?: T;
4
11
  }
5
12
  export type RefTarget<R> = R extends Ref<infer T> ? T : never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/resolve",
3
- "version": "3.11.11",
3
+ "version": "3.12.0",
4
4
  "type": "module",
5
5
  "description": "ExpoFP SDK internal: asset and resource resolution",
6
6
  "homepage": "https://developer.expofp.com/",
@@ -27,6 +27,6 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "tslib": "^2.3.0",
30
- "@expofp/utils": "3.11.11"
30
+ "@expofp/utils": "3.12.0"
31
31
  }
32
32
  }