assign-gingerly 0.0.58 → 0.0.60

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 (42) hide show
  1. package/README.md +511 -142
  2. package/assignFrom-extension.js +25 -0
  3. package/assignFrom-extension.ts +53 -0
  4. package/assignFrom.js +306 -133
  5. package/assignFrom.ts +318 -232
  6. package/assignFromAsync-extension.js +28 -0
  7. package/assignFromAsync-extension.ts +58 -0
  8. package/assignFromAsync.js +120 -0
  9. package/assignFromAsync.ts +256 -0
  10. package/assignGingerly.js +50 -0
  11. package/assignGingerly.ts +51 -0
  12. package/builtInEmoji.js +25 -0
  13. package/builtInEmoji.ts +33 -0
  14. package/getValues.js +223 -0
  15. package/getValues.ts +255 -0
  16. package/handlers/join.ts +1 -1
  17. package/handlers/lazyLoad.js +33 -2
  18. package/handlers/lazyLoad.ts +47 -2
  19. package/handlers/lazyLoadSwitch.ts +1 -1
  20. package/handlers/manageTemplateList.js +226 -0
  21. package/handlers/manageTemplateList.ts +263 -0
  22. package/handlers/microDataJoin.ts +1 -1
  23. package/index.js +1 -0
  24. package/index.ts +2 -0
  25. package/inferencer/inferencer.js +9 -21
  26. package/inferencer/inferencer.ts +10 -21
  27. package/inferredAssignments.js +35 -5
  28. package/inferredAssignments.ts +58 -8
  29. package/markerUtils.js +136 -127
  30. package/package.json +30 -1
  31. package/playwright.config.ts +3 -2
  32. package/processHandlerCommands.js +36 -4
  33. package/processHandlerCommands.ts +38 -8
  34. package/resolveIdRef.js +70 -19
  35. package/resolveIdRef.ts +73 -21
  36. package/resolveValues.js +41 -125
  37. package/resolveValues.ts +131 -255
  38. package/types/assign-gingerly/types.d.ts +61 -0
  39. package/waitForSettled.js +57 -0
  40. package/waitForSettled.ts +65 -0
  41. package/withIdsCorrector.js +47 -0
  42. package/withIdsCorrector.ts +59 -0
@@ -5,10 +5,11 @@
5
5
  */
6
6
 
7
7
  import { resolveValues } from './resolveValues.js';
8
+ import { getValues } from './getValues.js';
8
9
  import { evaluatePathWithMethods } from './assignGingerly.js';
9
10
  import { isAllowedImportPath } from './isAllowedImportPath.js';
10
11
  import type { AssignPermissions } from './isAllowedImportPath.js';
11
- import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFrom.js';
12
+ import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
12
13
 
13
14
  /**
14
15
  * Map of built-in handler names to their module paths.
@@ -19,6 +20,7 @@ const BUILT_IN_MAP: Record<string, string> = {
19
20
  'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
20
21
  'builtIns.join': './handlers/join.js',
21
22
  'builtIns.microDataJoin': './handlers/microDataJoin.js',
23
+ 'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
22
24
  };
23
25
 
24
26
  /**
@@ -41,15 +43,25 @@ function findHandlerInModule(module: any): AssignFromHandlerConstructor | undefi
41
43
  return undefined;
42
44
  }
43
45
 
46
+ /**
47
+ * Cache for loaded built-in handler classes — avoids await on subsequent calls.
48
+ */
49
+ const handlerCache = new Map<string, AssignFromHandlerConstructor>();
50
+
44
51
  /**
45
52
  * Dynamically load a built-in handler by name.
46
53
  * Returns the handler constructor, or undefined if the name isn't a recognized built-in.
54
+ * Cached after first load — subsequent calls are synchronous.
47
55
  */
48
56
  async function loadBuiltIn(name: string): Promise<AssignFromHandlerConstructor | undefined> {
57
+ const cached = handlerCache.get(name);
58
+ if (cached) return cached;
49
59
  const path = BUILT_IN_MAP[name];
50
60
  if (!path) return undefined;
51
61
  const module = await import(path);
52
- return findHandlerInModule(module);
62
+ const cls = findHandlerInModule(module);
63
+ if (cls) handlerCache.set(name, cls);
64
+ return cls;
53
65
  }
54
66
 
55
67
  /**
@@ -69,8 +81,14 @@ async function resolveFromHandlers(
69
81
  return entry as AssignFromHandlerConstructor;
70
82
  }
71
83
 
72
- // Import path string — validate and dynamically import
84
+ // String value — could be a built-in alias or an import path
73
85
  if (typeof entry === 'string') {
86
+ // Built-in alias: redirect to built-in loader
87
+ if (entry.startsWith('builtIns.')) {
88
+ return loadBuiltIn(entry);
89
+ }
90
+
91
+ // Import path string — validate and dynamically import
74
92
  if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
75
93
  throw new Error(
76
94
  `assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
@@ -107,6 +125,7 @@ export async function processHandlerCommands(
107
125
  options: AssignFromOptions,
108
126
  permissions?: AssignPermissions
109
127
  ): Promise<void> {
128
+
110
129
  for (const key of handlerKeys) {
111
130
  const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
112
131
  const rhs = pattern[key];
@@ -178,35 +197,46 @@ export async function processHandlerCommands(
178
197
  } else {
179
198
  lhsTarget = target;
180
199
  }
181
-
182
200
  // Execute handlers sequentially, sharing the same lhsTarget
183
201
  for (const config of configs) {
202
+ //return; //1.3ms
184
203
  // 1. Check options.handlers (local, per-call)
185
204
  let HandlerClass = await resolveFromHandlers(config.do, options.handlers, permissions);
186
-
205
+ //return; // 1.4
187
206
  // 2. Fallback to built-in auto-load
188
207
  if (!HandlerClass && config.do.startsWith('builtIns.')) {
189
208
  HandlerClass = await loadBuiltIn(config.do);
190
209
  }
210
+ //return; //1.4
191
211
 
192
212
  if (!HandlerClass) {
193
213
  throw new Error(`assignFrom: unknown handler "${config.do}". Provide it in options.handlers.`);
194
214
  }
195
215
 
196
- // Resolve 'resolve' map if present — uses full resolveValues (paths, protocols, literals)
216
+ // Resolve 'get' map synchronously (no thread yield)
197
217
  let resolvedParams: Record<string, any> = {};
218
+ if (config.get) {
219
+ resolvedParams = getValues(config.get, options.from, {
220
+ withMethods: options.withMethods,
221
+ aka: options.aka,
222
+ protocols: options.protocols
223
+ });
224
+ }
225
+ // Resolve 'resolve' map asynchronously (yields to microtask queue)
198
226
  if (config.resolve) {
199
- resolvedParams = await resolveValues(config.resolve, options.from, {
227
+ const asyncResolved = await resolveValues(config.resolve, options.from, {
200
228
  withMethods: options.withMethods,
201
229
  aka: options.aka,
202
230
  protocols: options.protocols
203
231
  });
232
+ Object.assign(resolvedParams, asyncResolved);
204
233
  }
205
234
 
206
235
  // Instantiate and invoke the handler
207
236
  const handler = new HandlerClass(config);
237
+ //return; //1.5ms
208
238
  const result = await handler.assign(lhsTarget, resolvedParams, options);
209
-
239
+ //return; //1.5ms
210
240
  // Return-value protocol: if handler returns a non-undefined value,
211
241
  // assign it back to the LHS path
212
242
  if (result !== undefined && lhsParent != null && lhsKey != null) {
package/resolveIdRef.js CHANGED
@@ -18,14 +18,15 @@ const idCacheMap = new WeakMap();
18
18
  const idCounterMap = new WeakMap();
19
19
  /**
20
20
  * Generate a unique ID within a rootNode.
21
- * Format: _ag0, _ag1, _ag2, ...
21
+ * Format: -ag:0, -ag:1, -ag:2, ...
22
+ * Starts with '-' and contains ':' to avoid collision with JS identifiers/globalThis properties.
22
23
  */
23
24
  function generateUniqueId(rootNode) {
24
25
  let counter = idCounterMap.get(rootNode) ?? 0;
25
26
  let id;
26
27
  // Ensure uniqueness (skip if ID already exists in the document)
27
28
  do {
28
- id = `_ag${counter}`;
29
+ id = `-ag:${counter}`;
29
30
  counter++;
30
31
  } while (rootNode.getElementById?.(id));
31
32
  idCounterMap.set(rootNode, counter);
@@ -43,6 +44,55 @@ export function resolveIdVariable(varName, target, withIds) {
43
44
  const config = withIds[varName];
44
45
  if (config === undefined)
45
46
  return undefined;
47
+ // For 'at' option path-based configs (array or { path }), resolve directly from target — no ID, no caching
48
+ // These are target-relative and fast (~2-4ns), for when structure is guaranteed stable
49
+ if (Array.isArray(config)) {
50
+ let current = target;
51
+ for (const idx of config) {
52
+ if (!current || !current.children)
53
+ break;
54
+ current = current.children[idx];
55
+ }
56
+ return current instanceof Element ? current : undefined;
57
+ }
58
+ if (typeof config === 'object' && 'path' in config && !('qry' in config)) {
59
+ // Determine if this is from 'at' (no ID assignment) or 'withIds' with path (assigns ID + caches)
60
+ // When called from 'at', we skip ID/caching. When from 'withIds', we assign ID and cache.
61
+ // Distinguish by presence in the options — caller passes the merged map.
62
+ // For now: { path } without 'noId' → assign ID + cache (withIds behavior)
63
+ const rootNode = target.getRootNode?.() ?? target;
64
+ let current = target;
65
+ for (const idx of config.path) {
66
+ if (!current || !current.children)
67
+ break;
68
+ current = current.children[idx];
69
+ }
70
+ let el = current instanceof Element ? current : null;
71
+ // Validation: check if resolved element matches expected selector
72
+ if (config.expect) {
73
+ const didNotMatch = !el || !el.matches(config.expect);
74
+ if (didNotMatch) {
75
+ if (config.fallback) {
76
+ el = target.querySelector?.(config.expect) ?? el;
77
+ }
78
+ // Fire-and-forget: log correction suggestion
79
+ const capturedConfig = config;
80
+ const capturedVarName = varName;
81
+ import('./withIdsCorrector.js').then(module => {
82
+ module.logConfigCorrection(target, capturedVarName, capturedConfig);
83
+ }).catch(() => { });
84
+ }
85
+ }
86
+ if (!el)
87
+ return undefined;
88
+ // Assign ID for stability against future DOM mutations
89
+ let id = el.id;
90
+ if (!id) {
91
+ id = generateUniqueId(rootNode);
92
+ el.id = id;
93
+ }
94
+ return el;
95
+ }
46
96
  const rootNode = target.getRootNode?.() ?? target;
47
97
  // Get or create cache for this rootNode
48
98
  let cache = idCacheMap.get(rootNode);
@@ -50,28 +100,27 @@ export function resolveIdVariable(varName, target, withIds) {
50
100
  cache = new Map();
51
101
  idCacheMap.set(rootNode, cache);
52
102
  }
53
- // Check cache first
54
- const cached = cache.get(varName);
55
- if (cached) {
56
- const el = cached.ref.deref();
57
- if (el)
58
- return el;
59
- // WeakRef was collected — try getElementById fallback
60
- const el2 = rootNode.getElementById?.(cached.id);
61
- if (el2) {
62
- cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
63
- return el2;
64
- }
65
- // Element no longer exists — fall through to re-query
66
- }
67
103
  // First time or cache miss — resolve the element
68
104
  let el = null;
69
105
  if (typeof config === 'string') {
70
106
  // String form: existing ID — use getElementById directly
107
+ // Check cache first (getElementById lookups are cacheable — global to rootNode)
108
+ const cached = cache.get(varName);
109
+ if (cached) {
110
+ const cachedEl = cached.ref.deref();
111
+ if (cachedEl)
112
+ return cachedEl;
113
+ // WeakRef was collected — try getElementById fallback
114
+ const el2 = rootNode.getElementById?.(cached.id);
115
+ if (el2) {
116
+ cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
117
+ return el2;
118
+ }
119
+ }
71
120
  el = rootNode.getElementById?.(config) ?? null;
72
121
  }
73
122
  else {
74
- // Object form: { qry } — run querySelector against target
123
+ // Object form: { qry } — run querySelector against target (target-relative, no cache)
75
124
  el = target.querySelector?.(config.qry) ?? null;
76
125
  }
77
126
  if (!el)
@@ -82,8 +131,10 @@ export function resolveIdVariable(varName, target, withIds) {
82
131
  id = generateUniqueId(rootNode);
83
132
  el.id = id;
84
133
  }
85
- // Cache it
86
- cache.set(varName, { id, ref: new WeakRef(el) });
134
+ // Cache only for string-form (getElementById) — not for qry form (target-relative)
135
+ if (typeof config === 'string') {
136
+ cache.set(varName, { id, ref: new WeakRef(el) });
137
+ }
87
138
  return el;
88
139
  }
89
140
  /**
package/resolveIdRef.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Configuration for a withIds entry.
13
13
  */
14
- export type WithIdConfig = string | { qry: string };
14
+ export type WithIdConfig = string | { qry: string } | number[] | { path: number[]; expect?: string; fallback?: boolean };
15
15
 
16
16
  /**
17
17
  * Module-level cache: rootNode → Map<varName, { id, WeakRef }>
@@ -26,14 +26,15 @@ const idCounterMap = new WeakMap<object, number>();
26
26
 
27
27
  /**
28
28
  * Generate a unique ID within a rootNode.
29
- * Format: _ag0, _ag1, _ag2, ...
29
+ * Format: -ag:0, -ag:1, -ag:2, ...
30
+ * Starts with '-' and contains ':' to avoid collision with JS identifiers/globalThis properties.
30
31
  */
31
32
  function generateUniqueId(rootNode: any): string {
32
33
  let counter = idCounterMap.get(rootNode) ?? 0;
33
34
  let id: string;
34
35
  // Ensure uniqueness (skip if ID already exists in the document)
35
36
  do {
36
- id = `_ag${counter}`;
37
+ id = `-ag:${counter}`;
37
38
  counter++;
38
39
  } while (rootNode.getElementById?.(id));
39
40
  idCounterMap.set(rootNode, counter);
@@ -56,6 +57,57 @@ export function resolveIdVariable(
56
57
  const config = withIds[varName];
57
58
  if (config === undefined) return undefined;
58
59
 
60
+ // For 'at' option path-based configs (array or { path }), resolve directly from target — no ID, no caching
61
+ // These are target-relative and fast (~2-4ns), for when structure is guaranteed stable
62
+ if (Array.isArray(config)) {
63
+ let current: any = target;
64
+ for (const idx of config) {
65
+ if (!current || !current.children) break;
66
+ current = current.children[idx];
67
+ }
68
+ return current instanceof Element ? current : undefined;
69
+ }
70
+
71
+ if (typeof config === 'object' && 'path' in config && !('qry' in config)) {
72
+ // Determine if this is from 'at' (no ID assignment) or 'withIds' with path (assigns ID + caches)
73
+ // When called from 'at', we skip ID/caching. When from 'withIds', we assign ID and cache.
74
+ // Distinguish by presence in the options — caller passes the merged map.
75
+ // For now: { path } without 'noId' → assign ID + cache (withIds behavior)
76
+ const rootNode = target.getRootNode?.() ?? target;
77
+ let current: any = target;
78
+ for (const idx of config.path) {
79
+ if (!current || !current.children) break;
80
+ current = current.children[idx];
81
+ }
82
+ let el: Element | null = current instanceof Element ? current : null;
83
+
84
+ // Validation: check if resolved element matches expected selector
85
+ if (config.expect) {
86
+ const didNotMatch = !el || !el.matches(config.expect);
87
+ if (didNotMatch) {
88
+ if (config.fallback) {
89
+ el = target.querySelector?.(config.expect) ?? el;
90
+ }
91
+ // Fire-and-forget: log correction suggestion
92
+ const capturedConfig = config;
93
+ const capturedVarName = varName;
94
+ import('./withIdsCorrector.js').then(module => {
95
+ module.logConfigCorrection(target, capturedVarName, capturedConfig);
96
+ }).catch(() => {});
97
+ }
98
+ }
99
+
100
+ if (!el) return undefined;
101
+
102
+ // Assign ID for stability against future DOM mutations
103
+ let id = el.id;
104
+ if (!id) {
105
+ id = generateUniqueId(rootNode);
106
+ el.id = id;
107
+ }
108
+ return el;
109
+ }
110
+
59
111
  const rootNode = target.getRootNode?.() ?? target;
60
112
 
61
113
  // Get or create cache for this rootNode
@@ -65,29 +117,27 @@ export function resolveIdVariable(
65
117
  idCacheMap.set(rootNode, cache);
66
118
  }
67
119
 
68
- // Check cache first
69
- const cached = cache.get(varName);
70
- if (cached) {
71
- const el = cached.ref.deref();
72
- if (el) return el;
73
-
74
- // WeakRef was collected — try getElementById fallback
75
- const el2 = rootNode.getElementById?.(cached.id);
76
- if (el2) {
77
- cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
78
- return el2;
79
- }
80
- // Element no longer exists — fall through to re-query
81
- }
82
-
83
120
  // First time or cache miss — resolve the element
84
121
  let el: Element | null = null;
85
122
 
86
123
  if (typeof config === 'string') {
87
124
  // String form: existing ID — use getElementById directly
125
+ // Check cache first (getElementById lookups are cacheable — global to rootNode)
126
+ const cached = cache.get(varName);
127
+ if (cached) {
128
+ const cachedEl = cached.ref.deref();
129
+ if (cachedEl) return cachedEl;
130
+
131
+ // WeakRef was collected — try getElementById fallback
132
+ const el2 = rootNode.getElementById?.(cached.id);
133
+ if (el2) {
134
+ cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
135
+ return el2;
136
+ }
137
+ }
88
138
  el = rootNode.getElementById?.(config) ?? null;
89
139
  } else {
90
- // Object form: { qry } — run querySelector against target
140
+ // Object form: { qry } — run querySelector against target (target-relative, no cache)
91
141
  el = target.querySelector?.(config.qry) ?? null;
92
142
  }
93
143
 
@@ -100,8 +150,10 @@ export function resolveIdVariable(
100
150
  el.id = id;
101
151
  }
102
152
 
103
- // Cache it
104
- cache.set(varName, { id, ref: new WeakRef(el) });
153
+ // Cache only for string-form (getElementById) — not for qry form (target-relative)
154
+ if (typeof config === 'string') {
155
+ cache.set(varName, { id, ref: new WeakRef(el) });
156
+ }
105
157
  return el;
106
158
  }
107
159
 
package/resolveValues.js CHANGED
@@ -1,6 +1,22 @@
1
+ /**
2
+ * resolveValues.ts — Async value resolution for path strings.
3
+ *
4
+ * Thin async wrapper around getValues that adds support for async protocol handlers.
5
+ * For synchronous-only use cases, import getValues/getValue directly for better performance.
6
+ *
7
+ * Re-exports ResolveValuesOptions for backward compatibility.
8
+ */
9
+ import { getValue } from './getValues.js';
10
+ // Re-export getValue as resolveValue for backward compatibility
11
+ export { getValue as resolveValue };
12
+ /**
13
+ * Checks if a string value looks like a protocol reference.
14
+ */
15
+ function hasProtocol(value) {
16
+ return value.includes('://');
17
+ }
1
18
  /**
2
19
  * Apply alias substitutions to a path string.
3
- * Replaces complete tokens between `?.` delimiters with their aliased values.
4
20
  */
5
21
  function applyAliases(path, aliasMap) {
6
22
  if (aliasMap.size === 0)
@@ -11,12 +27,8 @@ function applyAliases(path, aliasMap) {
11
27
  }
12
28
  /**
13
29
  * Path cache for parsed path strings.
14
- * Avoids re-splitting the same path on repeated calls.
15
30
  */
16
31
  const pathCache = new Map();
17
- /**
18
- * Parse a `?.`-delimited path string into segments, with caching.
19
- */
20
32
  function parseCachedPath(path) {
21
33
  let parts = pathCache.get(path);
22
34
  if (!parts) {
@@ -25,44 +37,8 @@ function parseCachedPath(path) {
25
37
  }
26
38
  return parts;
27
39
  }
28
- /**
29
- * Resolves a protocol-prefixed value (e.g., 'globalThis://key?.path').
30
- *
31
- * 1. Extracts the protocol name (before '://')
32
- * 2. If the protocol isn't in the protocols map, returns the value unchanged (false positive)
33
- * 3. Extracts the key (between '://' and first '?.' or end of string)
34
- * 4. Calls the protocol handler with the key
35
- * 5. If there's a remaining '?.' path, resolves it against the handler's result
36
- */
37
- async function resolveProtocolValue(value, protocols, options) {
38
- // Extract protocol name (before ://)
39
- const protoEnd = value.indexOf('://');
40
- const protocol = value.substring(0, protoEnd);
41
- // Resolve via protocol handler
42
- const handler = protocols[protocol];
43
- if (!handler)
44
- return value; // false flag — coincidentally looks like a protocol
45
- const rest = value.substring(protoEnd + 3);
46
- // Split at first ?. to separate key from path
47
- const pathStart = rest.indexOf('?.');
48
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
49
- const path = pathStart === -1 ? null : rest.substring(pathStart);
50
- const resolved = await handler(key);
51
- // If there's a remaining path, resolve it against the result
52
- if (path) {
53
- return resolveValue(path, resolved, options);
54
- }
55
- return resolved;
56
- }
57
- /**
58
- * Checks if a string value looks like a protocol reference.
59
- */
60
- function hasProtocol(value) {
61
- return value.includes('://');
62
- }
63
40
  /**
64
41
  * Navigate a path against a source object, optionally calling methods.
65
- * Returns the resolved value at the end of the path.
66
42
  */
67
43
  function navigatePath(source, parts, withMethods) {
68
44
  let current = source;
@@ -76,12 +52,10 @@ function navigatePath(source, parts, withMethods) {
76
52
  if (typeof method === 'function') {
77
53
  const nextPart = parts[i + 1];
78
54
  if (nextPart !== undefined && !(withMethods.has(nextPart))) {
79
- // Call method with next segment as argument, consume it
80
55
  current = method.call(current, nextPart);
81
56
  i += 2;
82
57
  }
83
58
  else {
84
- // Consecutive methods or last segment — call with no args
85
59
  current = method.call(current);
86
60
  i++;
87
61
  }
@@ -99,9 +73,26 @@ function navigatePath(source, parts, withMethods) {
99
73
  return current;
100
74
  }
101
75
  /**
102
- * Resolve path strings and protocol references within an array.
103
- * Recurses into nested arrays and plain objects. Non-string elements,
104
- * class instances, and other non-plain objects pass through unchanged.
76
+ * Resolves a protocol-prefixed value asynchronously.
77
+ */
78
+ async function resolveProtocolValue(value, protocols, options) {
79
+ const protoEnd = value.indexOf('://');
80
+ const protocol = value.substring(0, protoEnd);
81
+ const handler = protocols[protocol];
82
+ if (!handler)
83
+ return value;
84
+ const rest = value.substring(protoEnd + 3);
85
+ const pathStart = rest.indexOf('?.');
86
+ const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
87
+ const path = pathStart === -1 ? null : rest.substring(pathStart);
88
+ const resolved = await handler(key);
89
+ if (path) {
90
+ return getValue(path, resolved, options);
91
+ }
92
+ return resolved;
93
+ }
94
+ /**
95
+ * Resolve path strings and protocol references within an array (async).
105
96
  */
106
97
  async function resolveArray(arr, source, aliasMap, withMethods, protocols, options) {
107
98
  const result = [];
@@ -133,47 +124,23 @@ async function resolveArray(arr, source, aliasMap, withMethods, protocols, optio
133
124
  return result;
134
125
  }
135
126
  /**
136
- * Resolve RHS path strings in a pattern object against a source object.
127
+ * Async resolve RHS path strings in a pattern object against a source object.
137
128
  *
138
- * Any value that is a string starting with `?.` is treated as a path
139
- * and resolved against the source object using optional chaining semantics.
140
- * Non-string values and strings not starting with `?.` pass through unchanged.
141
- *
142
- * Supports `withMethods` for calling methods during resolution and `aka` for
143
- * alias substitution, consistent with assignGingerly's LHS path handling.
144
- *
145
- * Special case: `'?.'` (empty path) resolves to the source object itself.
129
+ * Supports async protocol handlers (e.g., fetch, IndexedDB).
130
+ * For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
146
131
  *
147
132
  * @param pattern - Object whose RHS values may contain `?.` path strings
148
133
  * @param source - Object to resolve paths against
149
- * @param options - Optional withMethods and aka for method calls and aliases
134
+ * @param options - Optional withMethods, aka, and protocol handlers
150
135
  * @returns New object with path strings replaced by resolved values
151
- *
152
- * @example
153
- * const result = resolveValues({
154
- * hello: '?.myPropContainer?.stringProp',
155
- * foo: '?.myFooString',
156
- * literal: 42
157
- * }, source);
158
- *
159
- * @example
160
- * // With methods and aliases
161
- * const result = resolveValues({
162
- * text: '?.q?..username?.textContent'
163
- * }, source, {
164
- * withMethods: ['querySelector'],
165
- * aka: { 'q': 'querySelector' }
166
- * });
167
136
  */
168
137
  export async function resolveValues(pattern, source, options) {
169
- // Build alias map
170
138
  const aliasMap = new Map();
171
139
  if (options?.aka) {
172
140
  for (const [alias, target] of Object.entries(options.aka)) {
173
141
  aliasMap.set(alias, target);
174
142
  }
175
143
  }
176
- // Build methods set
177
144
  const withMethods = options?.withMethods
178
145
  ? options.withMethods instanceof Set
179
146
  ? options.withMethods
@@ -183,24 +150,17 @@ export async function resolveValues(pattern, source, options) {
183
150
  const result = {};
184
151
  for (const [key, value] of Object.entries(pattern)) {
185
152
  if (typeof value === 'string' && value.startsWith('?.')) {
186
- // Apply aliases to the RHS path
187
153
  const aliased = applyAliases(value, aliasMap);
188
- // Parse path with caching
189
154
  const parts = parseCachedPath(aliased);
190
- // Navigate with method support
191
155
  result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
192
156
  }
193
157
  else if (typeof value === 'string' && protocols && hasProtocol(value)) {
194
- // Protocol-prefixed value — resolve asynchronously
195
158
  result[key] = await resolveProtocolValue(value, protocols, options);
196
159
  }
197
160
  else if (Array.isArray(value)) {
198
- // Resolve path strings and protocols within arrays (recursing into nested arrays)
199
161
  result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
200
162
  }
201
163
  else if (typeof value === 'object' && value !== null) {
202
- // Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
203
- // Only recurse into plain objects — skip DOM elements, class instances, etc.
204
164
  const proto = Object.getPrototypeOf(value);
205
165
  if (proto === Object.prototype || proto === null) {
206
166
  result[key] = await resolveValues(value, source, options);
@@ -215,47 +175,3 @@ export async function resolveValues(pattern, source, options) {
215
175
  }
216
176
  return result;
217
177
  }
218
- /**
219
- * Resolve a single `?.`-delimited path string against a source object.
220
- *
221
- * This is a lighter-weight alternative to `resolveValues` when you only need
222
- * to resolve one path and don't want the overhead of creating wrapper objects.
223
- *
224
- * @param path - A `?.`-delimited path string (e.g., '?.behaviors?.command')
225
- * @param source - Object to resolve the path against
226
- * @param options - Optional withMethods and aka for method calls and aliases
227
- * @returns The resolved value, or undefined if any segment is nullish
228
- *
229
- * @example
230
- * const value = resolveValue('?.behaviors?.commandBehavior?.command', el);
231
- *
232
- * @example
233
- * const value = resolveValue('?.q?.myEl?.textContent', el, {
234
- * withMethods: ['querySelector'],
235
- * aka: { 'q': 'querySelector' }
236
- * });
237
- */
238
- export function resolveValue(path, source, options) {
239
- if (!path.startsWith('?.'))
240
- return path;
241
- // Build alias map
242
- let aliased = path;
243
- if (options?.aka) {
244
- const aliasMap = new Map();
245
- for (const [alias, target] of Object.entries(options.aka)) {
246
- aliasMap.set(alias, target);
247
- }
248
- aliased = applyAliases(path, aliasMap);
249
- }
250
- // Parse path with caching
251
- const parts = parseCachedPath(aliased);
252
- if (parts.length === 0)
253
- return source;
254
- // Build methods set
255
- const withMethods = options?.withMethods
256
- ? options.withMethods instanceof Set
257
- ? options.withMethods
258
- : new Set(options.withMethods)
259
- : undefined;
260
- return navigatePath(source, parts, withMethods);
261
- }