assign-gingerly 0.0.95 → 0.0.96

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/README.md CHANGED
@@ -82,7 +82,7 @@ The third fundamental utility function is:
82
82
  **`assignFrom` is synchronous** — it resolves paths, expands substitutions, handles spreads, processes `#[x]` refs, and runs inferred assignments all without yielding to the event loop. Handler commands (` =>`), `beVigilant`, and `enhance` are fire-and-forget (kicked off asynchronously in the background).
83
83
 
84
84
  **`assignFromAsync`** is the awaitable variant for when you need to:
85
- - Use async protocol handlers (e.g., `fetch`-based resolution)
85
+ - Use async protocol handlers (e.g., `fetch`-based resolution — protocols are introduced just below, and covered in full in [Protocol Resolution](#protocol-resolution-in-getvalues-and-assignfrom) later in this doc)
86
86
  - `await` handler completion before proceeding
87
87
  - Wait for `enhance` (EMC JSON imports) to finish
88
88
 
@@ -97,7 +97,7 @@ assignFrom adds support for:
97
97
 
98
98
  1. Resolving RHS path strings against a source object (`from`).
99
99
  2. Target-relative root references via `$0` so paths can resolve from the first argument passed to `assignFrom`/`assignFromAsync` (the target object) instead of the `from` object.
100
- 3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
100
+ 3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols) — don't worry if this is unfamiliar yet, it's explained in full in [Protocol Resolution in `getValues` and `assignFrom`](#protocol-resolution-in-getvalues-and-assignfrom) later in this doc.
101
101
  4. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
102
102
  5. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
103
103
  6. Dynamic substitutions via the `substitutions` option for injecting runtime string values into path segments. See [docs/substitutions.md](docs/substitutions.md).
package/index.js CHANGED
@@ -8,6 +8,7 @@ export { buildCSSQuery } from './buildCSSQuery.js';
8
8
  export { resolveTemplate } from './resolve/resolveTemplate.js';
9
9
  export { getHost } from './getHost.js';
10
10
  export { resolveValues, resolveValue } from './resolve/resolveValues.js';
11
+ export { getValues, getValue, hasProtocol, parseProtocolRef, isPlainObject } from './resolve/getValues.js';
11
12
  export { assignFromAsync } from './assignFromAsync.js';
12
13
  export { assignFrom } from './assignFrom.js';
13
14
  export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
package/index.ts CHANGED
@@ -8,6 +8,7 @@ export {buildCSSQuery} from './buildCSSQuery.js';
8
8
  export {resolveTemplate} from './resolve/resolveTemplate.js';
9
9
  export {getHost} from './getHost.js';
10
10
  export {resolveValues, resolveValue} from './resolve/resolveValues.js';
11
+ export {getValues, getValue, hasProtocol, parseProtocolRef, isPlainObject} from './resolve/getValues.js';
11
12
  export {assignFromAsync} from './assignFromAsync.js';
12
13
  export {assignFrom} from './assignFrom.js';
13
14
  export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.95",
3
+ "version": "0.0.96",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -183,11 +183,47 @@ function navigatePath(source, parts, withMethods, permissionProcessor) {
183
183
  return current;
184
184
  }
185
185
  /**
186
- * Checks if a string value looks like a protocol reference.
186
+ * Whether a string carries a protocol prefix (contains `://`).
187
+ *
188
+ * Exported as the single source of truth for the outer USL grammar, so
189
+ * downstream packages (and resolveValues) don't re-implement the check.
187
190
  */
188
- function hasProtocol(value) {
191
+ export function hasProtocol(value) {
189
192
  return value.includes('://');
190
193
  }
194
+ /**
195
+ * Split a protocol-prefixed value string into its outer-grammar parts:
196
+ * `‹protocol›://‹key›?.‹path›`.
197
+ *
198
+ * - `protocol` is the text before `://` (`''` when there is no `://`).
199
+ * - `key` is the text between `://` and the first `?.` (or end of string).
200
+ * - `path` is the `?.`-onward remainder (starting with `?.`), or `null`.
201
+ *
202
+ * Pure string parsing — no handler lookup, no resolution. Shared by the sync
203
+ * (`getProtocolValue`) and async (`resolveProtocolValue`) resolvers.
204
+ */
205
+ export function parseProtocolRef(value) {
206
+ const protoEnd = value.indexOf('://');
207
+ if (protoEnd === -1)
208
+ return { protocol: '', key: value, path: null };
209
+ const protocol = value.substring(0, protoEnd);
210
+ const rest = value.substring(protoEnd + 3);
211
+ const pathStart = rest.indexOf('?.');
212
+ const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
213
+ const path = pathStart === -1 ? null : rest.substring(pathStart);
214
+ return { protocol, key, path };
215
+ }
216
+ /**
217
+ * Whether a value is a plain object (prototype is `Object.prototype` or `null`),
218
+ * as opposed to an array, a class instance, or a primitive. Used to decide
219
+ * whether a nested value should be recursed into as a pattern.
220
+ */
221
+ export function isPlainObject(value) {
222
+ if (!value || typeof value !== 'object')
223
+ return false;
224
+ const proto = Object.getPrototypeOf(value);
225
+ return proto === Object.prototype || proto === null;
226
+ }
191
227
  /**
192
228
  * Detect a leading run of `!` characters used as a boolean coercion / negation
193
229
  * marker (`!` = negate, `!!` = coerce to boolean, `!!!` = negate, …). Returns the
@@ -266,15 +302,10 @@ function resolveStringValue(value, source, aliasMap, withMethods, protocols, opt
266
302
  * Resolve a protocol-prefixed value synchronously.
267
303
  */
268
304
  function getProtocolValue(value, protocols, options) {
269
- const protoEnd = value.indexOf('://');
270
- const protocol = value.substring(0, protoEnd);
305
+ const { protocol, key, path } = parseProtocolRef(value);
271
306
  const handler = protocols[protocol];
272
307
  if (!handler)
273
308
  return value; // not a recognized protocol
274
- const rest = value.substring(protoEnd + 3);
275
- const pathStart = rest.indexOf('?.');
276
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
277
- const path = pathStart === -1 ? null : rest.substring(pathStart);
278
309
  const resolved = handler(key);
279
310
  if (path) {
280
311
  return getValue(path, resolved, options);
@@ -295,13 +326,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options, substi
295
326
  result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
296
327
  }
297
328
  else if (item && typeof item === 'object') {
298
- const proto = Object.getPrototypeOf(item);
299
- if (proto === Object.prototype || proto === null) {
300
- result.push(getValues(item, source, options));
301
- }
302
- else {
303
- result.push(item);
304
- }
329
+ result.push(isPlainObject(item) ? getValues(item, source, options) : item);
305
330
  }
306
331
  else {
307
332
  result.push(item);
@@ -334,13 +359,7 @@ export function getValues(pattern, source, options) {
334
359
  result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
335
360
  }
336
361
  else if (typeof value === 'object' && value !== null) {
337
- const proto = Object.getPrototypeOf(value);
338
- if (proto === Object.prototype || proto === null) {
339
- result[key] = getValues(value, source, options);
340
- }
341
- else {
342
- result[key] = value;
343
- }
362
+ result[key] = isPlainObject(value) ? getValues(value, source, options) : value;
344
363
  }
345
364
  else {
346
365
  result[key] = value;
@@ -17,7 +17,7 @@
17
17
  * }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
18
18
  */
19
19
 
20
- import type { GetValuesOptions, PermissionProcessor } from '../types/assign-gingerly/types.js';
20
+ import type { GetValuesOptions, ParsedProtocolRef, PermissionProcessor, SyncProtocolHandlers } from '../types/assign-gingerly/types.js';
21
21
 
22
22
  export function normalizeAliasOptions(options?: {
23
23
  aka?: Record<string, string>;
@@ -221,12 +221,48 @@ function navigatePath(
221
221
  }
222
222
 
223
223
  /**
224
- * Checks if a string value looks like a protocol reference.
224
+ * Whether a string carries a protocol prefix (contains `://`).
225
+ *
226
+ * Exported as the single source of truth for the outer USL grammar, so
227
+ * downstream packages (and resolveValues) don't re-implement the check.
225
228
  */
226
- function hasProtocol(value: string): boolean {
229
+ export function hasProtocol(value: string): boolean {
227
230
  return value.includes('://');
228
231
  }
229
232
 
233
+ /**
234
+ * Split a protocol-prefixed value string into its outer-grammar parts:
235
+ * `‹protocol›://‹key›?.‹path›`.
236
+ *
237
+ * - `protocol` is the text before `://` (`''` when there is no `://`).
238
+ * - `key` is the text between `://` and the first `?.` (or end of string).
239
+ * - `path` is the `?.`-onward remainder (starting with `?.`), or `null`.
240
+ *
241
+ * Pure string parsing — no handler lookup, no resolution. Shared by the sync
242
+ * (`getProtocolValue`) and async (`resolveProtocolValue`) resolvers.
243
+ */
244
+ export function parseProtocolRef(value: string): ParsedProtocolRef {
245
+ const protoEnd = value.indexOf('://');
246
+ if (protoEnd === -1) return { protocol: '', key: value, path: null };
247
+ const protocol = value.substring(0, protoEnd);
248
+ const rest = value.substring(protoEnd + 3);
249
+ const pathStart = rest.indexOf('?.');
250
+ const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
251
+ const path = pathStart === -1 ? null : rest.substring(pathStart);
252
+ return { protocol, key, path };
253
+ }
254
+
255
+ /**
256
+ * Whether a value is a plain object (prototype is `Object.prototype` or `null`),
257
+ * as opposed to an array, a class instance, or a primitive. Used to decide
258
+ * whether a nested value should be recursed into as a pattern.
259
+ */
260
+ export function isPlainObject(value: any): boolean {
261
+ if (!value || typeof value !== 'object') return false;
262
+ const proto = Object.getPrototypeOf(value);
263
+ return proto === Object.prototype || proto === null;
264
+ }
265
+
230
266
  /**
231
267
  * Detect a leading run of `!` characters used as a boolean coercion / negation
232
268
  * marker (`!` = negate, `!!` = coerce to boolean, `!!!` = negate, …). Returns the
@@ -249,7 +285,7 @@ function parseNegationMarker(value: string): { count: number; rest: string } | n
249
285
  */
250
286
  function looksLikeReference(
251
287
  value: string,
252
- protocols: Record<string, (key: string) => any> | undefined
288
+ protocols: SyncProtocolHandlers | undefined
253
289
  ): boolean {
254
290
  return value.startsWith('?.')
255
291
  || value.startsWith('$0')
@@ -265,7 +301,7 @@ function resolveReferenceString(
265
301
  source: any,
266
302
  aliasMap: Map<string, string>,
267
303
  withMethods: Set<string> | undefined,
268
- protocols: Record<string, (key: string) => any> | undefined,
304
+ protocols: SyncProtocolHandlers | undefined,
269
305
  options: GetValuesOptions | undefined,
270
306
  substitutionMap: Map<string, string> | undefined
271
307
  ): any {
@@ -298,7 +334,7 @@ function resolveStringValue(
298
334
  source: any,
299
335
  aliasMap: Map<string, string>,
300
336
  withMethods: Set<string> | undefined,
301
- protocols: Record<string, (key: string) => any> | undefined,
337
+ protocols: SyncProtocolHandlers | undefined,
302
338
  options: GetValuesOptions | undefined,
303
339
  substitutionMap: Map<string, string> | undefined
304
340
  ): any {
@@ -325,21 +361,14 @@ function resolveStringValue(
325
361
  */
326
362
  function getProtocolValue(
327
363
  value: string,
328
- protocols: Record<string, (key: string) => any>,
364
+ protocols: SyncProtocolHandlers,
329
365
  options?: GetValuesOptions
330
366
  ): any {
331
- const protoEnd = value.indexOf('://');
332
- const protocol = value.substring(0, protoEnd);
367
+ const { protocol, key, path } = parseProtocolRef(value);
333
368
 
334
369
  const handler = protocols[protocol];
335
370
  if (!handler) return value; // not a recognized protocol
336
371
 
337
- const rest = value.substring(protoEnd + 3);
338
-
339
- const pathStart = rest.indexOf('?.');
340
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
341
- const path = pathStart === -1 ? null : rest.substring(pathStart);
342
-
343
372
  const resolved = handler(key);
344
373
 
345
374
  if (path) {
@@ -357,7 +386,7 @@ function getArray(
357
386
  source: any,
358
387
  aliasMap: Map<string, string>,
359
388
  withMethods: Set<string> | undefined,
360
- protocols: Record<string, (key: string) => any> | undefined,
389
+ protocols: SyncProtocolHandlers | undefined,
361
390
  options?: GetValuesOptions,
362
391
  substitutionMap?: Map<string, string>
363
392
  ): any[] {
@@ -368,12 +397,7 @@ function getArray(
368
397
  } else if (Array.isArray(item)) {
369
398
  result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
370
399
  } else if (item && typeof item === 'object') {
371
- const proto = Object.getPrototypeOf(item);
372
- if (proto === Object.prototype || proto === null) {
373
- result.push(getValues(item, source, options));
374
- } else {
375
- result.push(item);
376
- }
400
+ result.push(isPlainObject(item) ? getValues(item, source, options) : item);
377
401
  } else {
378
402
  result.push(item);
379
403
  }
@@ -410,12 +434,7 @@ export function getValues(
410
434
  } else if (Array.isArray(value)) {
411
435
  result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
412
436
  } else if (typeof value === 'object' && value !== null) {
413
- const proto = Object.getPrototypeOf(value);
414
- if (proto === Object.prototype || proto === null) {
415
- result[key] = getValues(value, source, options);
416
- } else {
417
- result[key] = value;
418
- }
437
+ result[key] = isPlainObject(value) ? getValues(value, source, options) : value;
419
438
  } else {
420
439
  result[key] = value;
421
440
  }
@@ -4,30 +4,24 @@
4
4
  * Thin async wrapper around getValues that adds support for async protocol handlers.
5
5
  * For synchronous-only use cases, import getValues/getValue directly for better performance.
6
6
  *
7
+ * The outer-grammar primitives (`hasProtocol`, `parseProtocolRef`, `isPlainObject`)
8
+ * live in getValues.js and are re-exported here for convenience.
9
+ *
7
10
  * Re-exports ResolveValuesOptions for backward compatibility.
8
11
  */
9
- import { getValue, getValues } from './getValues.js';
12
+ import { getValue, getValues, hasProtocol, isPlainObject, parseProtocolRef } from './getValues.js';
10
13
  // Re-export getValue as resolveValue for backward compatibility
11
14
  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
- }
15
+ // Re-export the shared outer-grammar primitives (defined in getValues.js)
16
+ export { hasProtocol, parseProtocolRef, isPlainObject };
18
17
  /**
19
18
  * Resolves a protocol-prefixed value asynchronously.
20
19
  */
21
20
  async function resolveProtocolValue(value, protocols, options) {
22
- const protoEnd = value.indexOf('://');
23
- const protocol = value.substring(0, protoEnd);
21
+ const { protocol, key, path } = parseProtocolRef(value);
24
22
  const handler = protocols[protocol];
25
23
  if (!handler)
26
24
  return value;
27
- const rest = value.substring(protoEnd + 3);
28
- const pathStart = rest.indexOf('?.');
29
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
30
- const path = pathStart === -1 ? null : rest.substring(pathStart);
31
25
  const resolved = await handler(key);
32
26
  if (path) {
33
27
  return getValue(path, resolved, options);
@@ -52,8 +46,7 @@ async function resolveArray(arr, source, protocols, options) {
52
46
  result.push(await resolveArray(item, source, protocols, options));
53
47
  }
54
48
  else if (item && typeof item === 'object') {
55
- const proto = Object.getPrototypeOf(item);
56
- if (proto === Object.prototype || proto === null) {
49
+ if (isPlainObject(item)) {
57
50
  result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
58
51
  }
59
52
  else {
@@ -93,8 +86,7 @@ export async function resolveValues(pattern, source, options) {
93
86
  result[key] = await resolveArray(value, source, protocols, options);
94
87
  }
95
88
  else if (typeof value === 'object' && value !== null) {
96
- const proto = Object.getPrototypeOf(value);
97
- if (proto === Object.prototype || proto === null) {
89
+ if (isPlainObject(value)) {
98
90
  result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
99
91
  }
100
92
  else {
@@ -1,126 +1,116 @@
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
-
10
- import { getValue, getValues } from './getValues.js';
11
- import type { ResolveValuesOptions } from '../types/assign-gingerly/types.js';
12
-
13
- export type { ResolveValuesOptions };
14
-
15
- // Re-export getValue as resolveValue for backward compatibility
16
- export { getValue as resolveValue };
17
-
18
- /**
19
- * Checks if a string value looks like a protocol reference.
20
- */
21
- function hasProtocol(value: string): boolean {
22
- return value.includes('://');
23
- }
24
-
25
-
26
- /**
27
- * Resolves a protocol-prefixed value asynchronously.
28
- */
29
- async function resolveProtocolValue(
30
- value: string,
31
- protocols: Record<string, (key: string) => any | Promise<any>>,
32
- options?: ResolveValuesOptions
33
- ): Promise<any> {
34
- const protoEnd = value.indexOf('://');
35
- const protocol = value.substring(0, protoEnd);
36
-
37
- const handler = protocols[protocol];
38
- if (!handler) return value;
39
-
40
- const rest = value.substring(protoEnd + 3);
41
- const pathStart = rest.indexOf('?.');
42
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
43
- const path = pathStart === -1 ? null : rest.substring(pathStart);
44
-
45
- const resolved = await handler(key);
46
-
47
- if (path) {
48
- return getValue(path, resolved, options);
49
- }
50
- return resolved;
51
- }
52
-
53
- /**
54
- * Resolve path strings and protocol references within an array (async).
55
- */
56
- async function resolveArray(
57
- arr: any[],
58
- source: any,
59
- protocols: Record<string, (key: string) => any | Promise<any>> | undefined,
60
- options?: ResolveValuesOptions
61
- ): Promise<any[]> {
62
- const result: any[] = [];
63
- for (const item of arr) {
64
- if (typeof item === 'string') {
65
- if (protocols && hasProtocol(item)) {
66
- result.push(await resolveProtocolValue(item, protocols, options));
67
- } else {
68
- result.push(getValue(item, source, options));
69
- }
70
- } else if (Array.isArray(item)) {
71
- result.push(await resolveArray(item, source, protocols, options));
72
- } else if (item && typeof item === 'object') {
73
- const proto = Object.getPrototypeOf(item);
74
- if (proto === Object.prototype || proto === null) {
75
- result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
76
- } else {
77
- result.push(item);
78
- }
79
- } else {
80
- result.push(item);
81
- }
82
- }
83
- return result;
84
- }
85
-
86
- /**
87
- * Async resolve RHS path strings in a pattern object against a source object.
88
- *
89
- * Supports async protocol handlers (e.g., fetch, IndexedDB).
90
- * For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
91
- *
92
- * @param pattern - Object whose RHS values may contain `?.` path strings
93
- * @param source - Object to resolve paths against
94
- * @param options - Optional withMethods, aka, and protocol handlers
95
- * @returns New object with path strings replaced by resolved values
96
- */
97
- export async function resolveValues(
98
- pattern: Record<string, any>,
99
- source: any,
100
- options?: ResolveValuesOptions
101
- ): Promise<Record<string, any>> {
102
- const protocols = options?.protocols;
103
-
104
- const result: Record<string, any> = {};
105
- for (const [key, value] of Object.entries(pattern)) {
106
- if (typeof value === 'string') {
107
- if (protocols && hasProtocol(value)) {
108
- result[key] = await resolveProtocolValue(value, protocols, options);
109
- } else {
110
- result[key] = getValue(value, source, options);
111
- }
112
- } else if (Array.isArray(value)) {
113
- result[key] = await resolveArray(value, source, protocols, options);
114
- } else if (typeof value === 'object' && value !== null) {
115
- const proto = Object.getPrototypeOf(value);
116
- if (proto === Object.prototype || proto === null) {
117
- result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
118
- } else {
119
- result[key] = value;
120
- }
121
- } else {
122
- result[key] = value;
123
- }
124
- }
125
- return result;
126
- }
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
+ * The outer-grammar primitives (`hasProtocol`, `parseProtocolRef`, `isPlainObject`)
8
+ * live in getValues.js and are re-exported here for convenience.
9
+ *
10
+ * Re-exports ResolveValuesOptions for backward compatibility.
11
+ */
12
+
13
+ import { getValue, getValues, hasProtocol, isPlainObject, parseProtocolRef } from './getValues.js';
14
+ import type { ProtocolHandlers, ResolveValuesOptions } from '../types/assign-gingerly/types.js';
15
+
16
+ export type { ResolveValuesOptions };
17
+
18
+ // Re-export getValue as resolveValue for backward compatibility
19
+ export { getValue as resolveValue };
20
+
21
+ // Re-export the shared outer-grammar primitives (defined in getValues.js)
22
+ export { hasProtocol, parseProtocolRef, isPlainObject };
23
+
24
+ /**
25
+ * Resolves a protocol-prefixed value asynchronously.
26
+ */
27
+ async function resolveProtocolValue(
28
+ value: string,
29
+ protocols: ProtocolHandlers,
30
+ options?: ResolveValuesOptions
31
+ ): Promise<any> {
32
+ const { protocol, key, path } = parseProtocolRef(value);
33
+
34
+ const handler = protocols[protocol];
35
+ if (!handler) return value;
36
+
37
+ const resolved = await handler(key);
38
+
39
+ if (path) {
40
+ return getValue(path, resolved, options);
41
+ }
42
+ return resolved;
43
+ }
44
+
45
+ /**
46
+ * Resolve path strings and protocol references within an array (async).
47
+ */
48
+ async function resolveArray(
49
+ arr: any[],
50
+ source: any,
51
+ protocols: ProtocolHandlers | undefined,
52
+ options?: ResolveValuesOptions
53
+ ): Promise<any[]> {
54
+ const result: any[] = [];
55
+ for (const item of arr) {
56
+ if (typeof item === 'string') {
57
+ if (protocols && hasProtocol(item)) {
58
+ result.push(await resolveProtocolValue(item, protocols, options));
59
+ } else {
60
+ result.push(getValue(item, source, options));
61
+ }
62
+ } else if (Array.isArray(item)) {
63
+ result.push(await resolveArray(item, source, protocols, options));
64
+ } else if (item && typeof item === 'object') {
65
+ if (isPlainObject(item)) {
66
+ result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
67
+ } else {
68
+ result.push(item);
69
+ }
70
+ } else {
71
+ result.push(item);
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Async resolve RHS path strings in a pattern object against a source object.
79
+ *
80
+ * Supports async protocol handlers (e.g., fetch, IndexedDB).
81
+ * For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
82
+ *
83
+ * @param pattern - Object whose RHS values may contain `?.` path strings
84
+ * @param source - Object to resolve paths against
85
+ * @param options - Optional withMethods, aka, and protocol handlers
86
+ * @returns New object with path strings replaced by resolved values
87
+ */
88
+ export async function resolveValues(
89
+ pattern: Record<string, any>,
90
+ source: any,
91
+ options?: ResolveValuesOptions
92
+ ): Promise<Record<string, any>> {
93
+ const protocols = options?.protocols;
94
+
95
+ const result: Record<string, any> = {};
96
+ for (const [key, value] of Object.entries(pattern)) {
97
+ if (typeof value === 'string') {
98
+ if (protocols && hasProtocol(value)) {
99
+ result[key] = await resolveProtocolValue(value, protocols, options);
100
+ } else {
101
+ result[key] = getValue(value, source, options);
102
+ }
103
+ } else if (Array.isArray(value)) {
104
+ result[key] = await resolveArray(value, source, protocols, options);
105
+ } else if (typeof value === 'object' && value !== null) {
106
+ if (isPlainObject(value)) {
107
+ result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
108
+ } else {
109
+ result[key] = value;
110
+ }
111
+ } else {
112
+ result[key] = value;
113
+ }
114
+ }
115
+ return result;
116
+ }
@@ -406,7 +406,7 @@ export interface AssignFromOptions {
406
406
  from: any;
407
407
 
408
408
  /** Protocol handlers (sync or async) */
409
- protocols?: Record<string, (key: string) => any | Promise<any>>;
409
+ protocols?: ProtocolHandlers;
410
410
 
411
411
  /** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
412
412
  withMethods?: string[] | Set<string>;
@@ -480,6 +480,37 @@ export interface AssignFromOptions {
480
480
  [key: string]: any;
481
481
  }
482
482
 
483
+ /**
484
+ * A protocol handler resolves the key portion of a protocol-prefixed value
485
+ * (the text between `://` and the first `?.`) to a value. May be sync or async.
486
+ */
487
+ export type ProtocolHandler = (key: string) => any | Promise<any>;
488
+
489
+ /**
490
+ * A synchronous-only protocol handler, as accepted by getValues / getValue /
491
+ * assignFrom. For handlers that may return a Promise, use ProtocolHandler.
492
+ */
493
+ export type SyncProtocolHandler = (key: string) => any;
494
+
495
+ /** Map of protocol name (the text before `://`) to a handler (sync or async). */
496
+ export type ProtocolHandlers = Record<string, ProtocolHandler>;
497
+
498
+ /** Map of protocol name to a synchronous handler. */
499
+ export type SyncProtocolHandlers = Record<string, SyncProtocolHandler>;
500
+
501
+ /**
502
+ * The outer grammar of a protocol-prefixed value string,
503
+ * `‹protocol›://‹key›?.‹path›`, as produced by `parseProtocolRef`.
504
+ */
505
+ export interface ParsedProtocolRef {
506
+ /** Text before `://`; `''` when the value contains no `://`. */
507
+ protocol: string;
508
+ /** Text between `://` and the first `?.` (or the end of the string). */
509
+ key: string;
510
+ /** Text from the first `?.` onward (always starts with `?.`), or `null`. */
511
+ path: string | null;
512
+ }
513
+
483
514
  /**
484
515
  * Options for synchronous value resolution (getValues / getValue).
485
516
  * Extends IAssignGingerlyOptions with synchronous protocol handlers.
@@ -507,7 +538,7 @@ export interface GetValuesOptions extends IAssignGingerlyOptions {
507
538
  * localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
508
539
  * }
509
540
  */
510
- protocols?: Record<string, (key: string) => any>;
541
+ protocols?: SyncProtocolHandlers;
511
542
  }
512
543
 
513
544
  /**
@@ -530,7 +561,7 @@ export interface ResolveValuesOptions extends IAssignGingerlyOptions {
530
561
  * Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
531
562
  * Each handler receives the key portion and returns the resolved value (sync or async).
532
563
  */
533
- protocols?: Record<string, (key: string) => any | Promise<any>>;
564
+ protocols?: ProtocolHandlers;
534
565
  }
535
566
 
536
567
  /**