assign-gingerly 0.0.59 → 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.
package/assignFrom.js CHANGED
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
10
10
  */
11
- import { getValues } from './getValues.js';
11
+ import { getValues, getValue } from './getValues.js';
12
12
  import assignGingerly from './assignGingerly.js';
13
13
  import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
14
14
  import { processInferredAssignments } from './inferredAssignments.js';
@@ -26,6 +26,120 @@ export const SUBSTITUTION_VARS = [
26
26
  export function isHandlerCommand(key) {
27
27
  return key.endsWith(' =>');
28
28
  }
29
+ /**
30
+ * Check if a key ends with the ternary operator ' ?='.
31
+ */
32
+ export function isTernaryCommand(key) {
33
+ return key.endsWith(' ?=');
34
+ }
35
+ /**
36
+ * Parse a ?= ternary command and extract the LHS path.
37
+ */
38
+ export function parseTernaryCommand(key) {
39
+ if (!isTernaryCommand(key))
40
+ return null;
41
+ return key.substring(0, key.length - 3); // Remove ' ?=' suffix
42
+ }
43
+ /**
44
+ * Resolve a single value — if it's a `?.` path string, resolve against source.
45
+ * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
46
+ */
47
+ function resolveTernaryValue(value, source, options) {
48
+ if (typeof value === 'string' && value.startsWith('?.')) {
49
+ return getValue(value, source, {
50
+ withMethods: options.withMethods,
51
+ aka: options.aka,
52
+ protocols: options.protocols
53
+ });
54
+ }
55
+ if (typeof value === 'string' && value.includes('://') && options.protocols) {
56
+ // Check if it matches a known protocol
57
+ const protoEnd = value.indexOf('://');
58
+ const protocol = value.substring(0, protoEnd);
59
+ if (options.protocols[protocol]) {
60
+ return getValue(value, source, {
61
+ withMethods: options.withMethods,
62
+ aka: options.aka,
63
+ protocols: options.protocols
64
+ });
65
+ }
66
+ }
67
+ return value;
68
+ }
69
+ /**
70
+ * Evaluate a ?= ternary expression.
71
+ *
72
+ * Supported forms:
73
+ * - [ifTruthy, thenResult] — guard (skip if falsy)
74
+ * - [ifTruthy, thenResult, elseResult] — ternary
75
+ * - [ifTrue, trueResult, falseResult, neither] — three-state (true/false/nullish)
76
+ * - [[lhs, rhs], ifEqual, ifNotEqual?] — equality comparison
77
+ * - [[lhs, rhs], ifEqual] — equality guard
78
+ *
79
+ * Returns undefined to signal "skip assignment" (guard forms when condition not met).
80
+ */
81
+ const TERNARY_SKIP = Symbol('ternary-skip');
82
+ function evaluateTernary(arr, source, options) {
83
+ const condition = arr[0];
84
+ if (Array.isArray(condition)) {
85
+ // Comparison mode: [[lhs, rhs], ...] or [[lhs, op, rhs], ...]
86
+ const lhs = resolveTernaryValue(condition[0], source, options);
87
+ if (condition.length === 2) {
88
+ // Equality: [[lhs, rhs], result, elseResult?]
89
+ const rhs = resolveTernaryValue(condition[1], source, options);
90
+ if (lhs === rhs) {
91
+ return resolveTernaryValue(arr[1], source, options);
92
+ }
93
+ else {
94
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
95
+ }
96
+ }
97
+ else {
98
+ // Operator: [[lhs, op, rhs], result, elseResult?]
99
+ const op = condition[1];
100
+ const rhs = resolveTernaryValue(condition[2], source, options);
101
+ const satisfied = compareWithOp(lhs, op, rhs);
102
+ if (satisfied) {
103
+ return resolveTernaryValue(arr[1], source, options);
104
+ }
105
+ else {
106
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ // Truthiness mode
112
+ const resolved = resolveTernaryValue(condition, source, options);
113
+ if (arr.length === 4) {
114
+ // [ifTrue, trueResult, falseResult, neitherResult]
115
+ if (resolved == null)
116
+ return resolveTernaryValue(arr[3], source, options);
117
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
118
+ }
119
+ else if (arr.length === 3) {
120
+ // [ifTruthy, thenResult, elseResult]
121
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
122
+ }
123
+ else {
124
+ // [ifTruthy, thenResult] — guard, skip if falsy
125
+ return resolved ? resolveTernaryValue(arr[1], source, options) : TERNARY_SKIP;
126
+ }
127
+ }
128
+ }
129
+ /**
130
+ * Compare two values with a given operator.
131
+ */
132
+ function compareWithOp(lhs, op, rhs) {
133
+ switch (op) {
134
+ case '===': return lhs === rhs;
135
+ case '!==': return lhs !== rhs;
136
+ case '>': return lhs > rhs;
137
+ case '>=': return lhs >= rhs;
138
+ case '<': return lhs < rhs;
139
+ case '<=': return lhs <= rhs;
140
+ default: return lhs === rhs; // fallback to equality
141
+ }
142
+ }
29
143
  /**
30
144
  * Recursively substitute a placeholder in all string values of an object.
31
145
  */
@@ -150,6 +264,7 @@ export function categorizeKeys(expandedPattern) {
150
264
  const normalPattern = {};
151
265
  const idRefNormalKeys = [];
152
266
  const idRefHandlerKeys = [];
267
+ const ternaryKeys = [];
153
268
  for (const key of Object.keys(expandedPattern)) {
154
269
  if (isHandlerCommand(key)) {
155
270
  if (key.startsWith('#[')) {
@@ -159,6 +274,9 @@ export function categorizeKeys(expandedPattern) {
159
274
  handlerKeys.push(key);
160
275
  }
161
276
  }
277
+ else if (isTernaryCommand(key)) {
278
+ ternaryKeys.push(key);
279
+ }
162
280
  else if (key.startsWith('#[')) {
163
281
  idRefNormalKeys.push(key);
164
282
  }
@@ -166,19 +284,32 @@ export function categorizeKeys(expandedPattern) {
166
284
  normalPattern[key] = expandedPattern[key];
167
285
  }
168
286
  }
169
- return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys };
287
+ return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
288
+ }
289
+ /**
290
+ * Merge withIds and at into a single lookup map for resolveIdVariable.
291
+ */
292
+ function getEffectiveIds(options) {
293
+ if (!options.withIds && !options.at)
294
+ return undefined;
295
+ if (options.withIds && !options.at)
296
+ return options.withIds;
297
+ if (!options.withIds && options.at)
298
+ return options.at;
299
+ return { ...options.withIds, ...options.at };
170
300
  }
171
301
  /**
172
302
  * Process #[x] normal keys synchronously.
173
303
  */
174
304
  function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, options) {
175
- if (!options.withIds)
305
+ const ids = getEffectiveIds(options);
306
+ if (!ids)
176
307
  return;
177
308
  for (const key of idRefNormalKeys) {
178
309
  const parsed = parseIdRef(key);
179
310
  if (!parsed)
180
311
  continue;
181
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
312
+ const el = resolveIdVariable(parsed.varName, target, ids);
182
313
  if (!el)
183
314
  continue;
184
315
  const value = expandedPattern[key];
@@ -210,9 +341,55 @@ export function assignFrom(target, pattern, options, permissions) {
210
341
  // Expand looped substitution variables
211
342
  const expandedPattern = expandSubstitutions(pattern, options);
212
343
  // Categorize keys
213
- const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
344
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
345
+ // Process ?= ternary keys (sync)
346
+ if (ternaryKeys.length > 0) {
347
+ const ternaryResolved = {};
348
+ for (const key of ternaryKeys) {
349
+ const lhsPath = parseTernaryCommand(key);
350
+ if (!lhsPath)
351
+ continue;
352
+ const arr = expandedPattern[key];
353
+ if (!Array.isArray(arr) || arr.length < 2)
354
+ continue;
355
+ const result = evaluateTernary(arr, options.from, options);
356
+ if (result !== TERNARY_SKIP) {
357
+ ternaryResolved[lhsPath] = result;
358
+ }
359
+ }
360
+ if (Object.keys(ternaryResolved).length > 0) {
361
+ assignGingerly(target, ternaryResolved, options);
362
+ }
363
+ }
214
364
  // Process normal keys via getValues (sync) + assignGingerly
215
365
  if (Object.keys(normalPattern).length > 0) {
366
+ // Resolve #[x] references on RHS values before getValues
367
+ if (options.withIds || options.at) {
368
+ const ids = getEffectiveIds(options);
369
+ for (const key of Object.keys(normalPattern)) {
370
+ const value = normalPattern[key];
371
+ if (typeof value === 'string' && value.startsWith('#[')) {
372
+ const closeIdx = value.indexOf(']');
373
+ if (closeIdx !== -1) {
374
+ const varName = value.substring(2, closeIdx);
375
+ const el = resolveIdVariable(varName, target, ids);
376
+ if (el) {
377
+ const remainingPath = value.substring(closeIdx + 1);
378
+ if (remainingPath) {
379
+ normalPattern[key] = getValue(remainingPath, el, {
380
+ withMethods: options.withMethods,
381
+ aka: options.aka,
382
+ protocols: options.protocols
383
+ });
384
+ }
385
+ else {
386
+ normalPattern[key] = el.id; // bare #[x] → ID string
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ }
216
393
  const resolved = getValues(normalPattern, options.from, {
217
394
  withMethods: options.withMethods,
218
395
  aka: options.aka,
@@ -232,13 +409,14 @@ export function assignFrom(target, pattern, options, permissions) {
232
409
  });
233
410
  }
234
411
  // Process #[x] handler keys — fire-and-forget (async)
235
- if (idRefHandlerKeys.length > 0 && options.withIds) {
412
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
413
+ const ids = getEffectiveIds(options);
236
414
  import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
237
415
  for (const key of idRefHandlerKeys) {
238
416
  const parsed = parseIdRef(key);
239
417
  if (!parsed)
240
418
  continue;
241
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
419
+ const el = resolveIdVariable(parsed.varName, target, ids);
242
420
  if (!el)
243
421
  continue;
244
422
  const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
@@ -248,15 +426,15 @@ export function assignFrom(target, pattern, options, permissions) {
248
426
  });
249
427
  }
250
428
  // Process inferred assignments (sync)
251
- if (options.inferredAssignments) {
252
- processInferredAssignments(target, options.from, options.inferredAssignments);
429
+ if (options.infer) {
430
+ processInferredAssignments(target, options.from, options.infer);
253
431
  // beVigilant — fire-and-forget (async)
254
- if (options.inferredAssignments.beVigilant) {
432
+ if (options.infer.beVigilant) {
255
433
  if (!options.signal) {
256
- throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
434
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
257
435
  }
258
436
  import('./beVigilant.js').then(({ setupVigilantObserver }) => {
259
- setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
437
+ setupVigilantObserver(target, options.from, options.infer, options.signal);
260
438
  });
261
439
  }
262
440
  }
package/assignFrom.ts CHANGED
@@ -35,6 +35,119 @@ export function isHandlerCommand(key: string): boolean {
35
35
  return key.endsWith(' =>');
36
36
  }
37
37
 
38
+ /**
39
+ * Check if a key ends with the ternary operator ' ?='.
40
+ */
41
+ export function isTernaryCommand(key: string): boolean {
42
+ return key.endsWith(' ?=');
43
+ }
44
+
45
+ /**
46
+ * Parse a ?= ternary command and extract the LHS path.
47
+ */
48
+ export function parseTernaryCommand(key: string): string | null {
49
+ if (!isTernaryCommand(key)) return null;
50
+ return key.substring(0, key.length - 3); // Remove ' ?=' suffix
51
+ }
52
+
53
+ /**
54
+ * Resolve a single value — if it's a `?.` path string, resolve against source.
55
+ * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
56
+ */
57
+ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions): any {
58
+ if (typeof value === 'string' && value.startsWith('?.')) {
59
+ return getValue(value, source, {
60
+ withMethods: options.withMethods,
61
+ aka: options.aka,
62
+ protocols: options.protocols
63
+ });
64
+ }
65
+ if (typeof value === 'string' && value.includes('://') && options.protocols) {
66
+ // Check if it matches a known protocol
67
+ const protoEnd = value.indexOf('://');
68
+ const protocol = value.substring(0, protoEnd);
69
+ if (options.protocols[protocol]) {
70
+ return getValue(value, source, {
71
+ withMethods: options.withMethods,
72
+ aka: options.aka,
73
+ protocols: options.protocols
74
+ });
75
+ }
76
+ }
77
+ return value;
78
+ }
79
+
80
+ /**
81
+ * Evaluate a ?= ternary expression.
82
+ *
83
+ * Supported forms:
84
+ * - [ifTruthy, thenResult] — guard (skip if falsy)
85
+ * - [ifTruthy, thenResult, elseResult] — ternary
86
+ * - [ifTrue, trueResult, falseResult, neither] — three-state (true/false/nullish)
87
+ * - [[lhs, rhs], ifEqual, ifNotEqual?] — equality comparison
88
+ * - [[lhs, rhs], ifEqual] — equality guard
89
+ *
90
+ * Returns undefined to signal "skip assignment" (guard forms when condition not met).
91
+ */
92
+ const TERNARY_SKIP = Symbol('ternary-skip');
93
+
94
+ function evaluateTernary(arr: any[], source: any, options: AssignFromOptions): any {
95
+ const condition = arr[0];
96
+
97
+ if (Array.isArray(condition)) {
98
+ // Comparison mode: [[lhs, rhs], ...] or [[lhs, op, rhs], ...]
99
+ const lhs = resolveTernaryValue(condition[0], source, options);
100
+ if (condition.length === 2) {
101
+ // Equality: [[lhs, rhs], result, elseResult?]
102
+ const rhs = resolveTernaryValue(condition[1], source, options);
103
+ if (lhs === rhs) {
104
+ return resolveTernaryValue(arr[1], source, options);
105
+ } else {
106
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
107
+ }
108
+ } else {
109
+ // Operator: [[lhs, op, rhs], result, elseResult?]
110
+ const op = condition[1] as string;
111
+ const rhs = resolveTernaryValue(condition[2], source, options);
112
+ const satisfied = compareWithOp(lhs, op, rhs);
113
+ if (satisfied) {
114
+ return resolveTernaryValue(arr[1], source, options);
115
+ } else {
116
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
117
+ }
118
+ }
119
+ } else {
120
+ // Truthiness mode
121
+ const resolved = resolveTernaryValue(condition, source, options);
122
+ if (arr.length === 4) {
123
+ // [ifTrue, trueResult, falseResult, neitherResult]
124
+ if (resolved == null) return resolveTernaryValue(arr[3], source, options);
125
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
126
+ } else if (arr.length === 3) {
127
+ // [ifTruthy, thenResult, elseResult]
128
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
129
+ } else {
130
+ // [ifTruthy, thenResult] — guard, skip if falsy
131
+ return resolved ? resolveTernaryValue(arr[1], source, options) : TERNARY_SKIP;
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Compare two values with a given operator.
138
+ */
139
+ function compareWithOp(lhs: any, op: string, rhs: any): boolean {
140
+ switch (op) {
141
+ case '===': return lhs === rhs;
142
+ case '!==': return lhs !== rhs;
143
+ case '>': return lhs > rhs;
144
+ case '>=': return lhs >= rhs;
145
+ case '<': return lhs < rhs;
146
+ case '<=': return lhs <= rhs;
147
+ default: return lhs === rhs; // fallback to equality
148
+ }
149
+ }
150
+
38
151
  /**
39
152
  * Recursively substitute a placeholder in all string values of an object.
40
153
  */
@@ -164,6 +277,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
164
277
  const normalPattern: Record<string, any> = {};
165
278
  const idRefNormalKeys: string[] = [];
166
279
  const idRefHandlerKeys: string[] = [];
280
+ const ternaryKeys: string[] = [];
167
281
 
168
282
  for (const key of Object.keys(expandedPattern)) {
169
283
  if (isHandlerCommand(key)) {
@@ -172,6 +286,8 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
172
286
  } else {
173
287
  handlerKeys.push(key);
174
288
  }
289
+ } else if (isTernaryCommand(key)) {
290
+ ternaryKeys.push(key);
175
291
  } else if (key.startsWith('#[')) {
176
292
  idRefNormalKeys.push(key);
177
293
  } else {
@@ -179,7 +295,17 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
179
295
  }
180
296
  }
181
297
 
182
- return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys };
298
+ return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
299
+ }
300
+
301
+ /**
302
+ * Merge withIds and at into a single lookup map for resolveIdVariable.
303
+ */
304
+ function getEffectiveIds(options: AssignFromOptions): Record<string, any> | undefined {
305
+ if (!options.withIds && !options.at) return undefined;
306
+ if (options.withIds && !options.at) return options.withIds;
307
+ if (!options.withIds && options.at) return options.at;
308
+ return { ...options.withIds, ...options.at };
183
309
  }
184
310
 
185
311
  /**
@@ -191,13 +317,14 @@ function processIdRefNormalKeys(
191
317
  target: any,
192
318
  options: AssignFromOptions
193
319
  ): void {
194
- if (!options.withIds) return;
320
+ const ids = getEffectiveIds(options);
321
+ if (!ids) return;
195
322
 
196
323
  for (const key of idRefNormalKeys) {
197
324
  const parsed = parseIdRef(key);
198
325
  if (!parsed) continue;
199
326
 
200
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
327
+ const el = resolveIdVariable(parsed.varName, target, ids);
201
328
  if (!el) continue;
202
329
 
203
330
  const value = expandedPattern[key];
@@ -242,10 +369,55 @@ export function assignFrom(
242
369
  const expandedPattern = expandSubstitutions(pattern, options);
243
370
 
244
371
  // Categorize keys
245
- const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
372
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
373
+
374
+ // Process ?= ternary keys (sync)
375
+ if (ternaryKeys.length > 0) {
376
+ const ternaryResolved: Record<string, any> = {};
377
+ for (const key of ternaryKeys) {
378
+ const lhsPath = parseTernaryCommand(key);
379
+ if (!lhsPath) continue;
380
+ const arr = expandedPattern[key];
381
+ if (!Array.isArray(arr) || arr.length < 2) continue;
382
+ const result = evaluateTernary(arr, options.from, options);
383
+ if (result !== TERNARY_SKIP) {
384
+ ternaryResolved[lhsPath] = result;
385
+ }
386
+ }
387
+ if (Object.keys(ternaryResolved).length > 0) {
388
+ assignGingerly(target, ternaryResolved, options);
389
+ }
390
+ }
246
391
 
247
392
  // Process normal keys via getValues (sync) + assignGingerly
248
393
  if (Object.keys(normalPattern).length > 0) {
394
+ // Resolve #[x] references on RHS values before getValues
395
+ if (options.withIds || options.at) {
396
+ const ids = getEffectiveIds(options)!;
397
+ for (const key of Object.keys(normalPattern)) {
398
+ const value = normalPattern[key];
399
+ if (typeof value === 'string' && value.startsWith('#[')) {
400
+ const closeIdx = value.indexOf(']');
401
+ if (closeIdx !== -1) {
402
+ const varName = value.substring(2, closeIdx);
403
+ const el = resolveIdVariable(varName, target, ids);
404
+ if (el) {
405
+ const remainingPath = value.substring(closeIdx + 1);
406
+ if (remainingPath) {
407
+ normalPattern[key] = getValue(remainingPath, el, {
408
+ withMethods: options.withMethods,
409
+ aka: options.aka,
410
+ protocols: options.protocols
411
+ });
412
+ } else {
413
+ normalPattern[key] = el.id; // bare #[x] → ID string
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+
249
421
  const resolved = getValues(normalPattern, options.from, {
250
422
  withMethods: options.withMethods,
251
423
  aka: options.aka,
@@ -269,12 +441,13 @@ export function assignFrom(
269
441
  }
270
442
 
271
443
  // Process #[x] handler keys — fire-and-forget (async)
272
- if (idRefHandlerKeys.length > 0 && options.withIds) {
444
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
445
+ const ids = getEffectiveIds(options)!;
273
446
  import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
274
447
  for (const key of idRefHandlerKeys) {
275
448
  const parsed = parseIdRef(key);
276
449
  if (!parsed) continue;
277
- const el = resolveIdVariable(parsed.varName, target, options.withIds!);
450
+ const el = resolveIdVariable(parsed.varName, target, ids);
278
451
  if (!el) continue;
279
452
  const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
280
453
  const syntheticPattern = { [syntheticKey]: expandedPattern[key] };
@@ -284,16 +457,16 @@ export function assignFrom(
284
457
  }
285
458
 
286
459
  // Process inferred assignments (sync)
287
- if (options.inferredAssignments) {
288
- processInferredAssignments(target, options.from, options.inferredAssignments);
460
+ if (options.infer) {
461
+ processInferredAssignments(target, options.from, options.infer);
289
462
 
290
463
  // beVigilant — fire-and-forget (async)
291
- if (options.inferredAssignments.beVigilant) {
464
+ if (options.infer.beVigilant) {
292
465
  if (!options.signal) {
293
- throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
466
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
294
467
  }
295
468
  import('./beVigilant.js').then(({ setupVigilantObserver }) => {
296
- setupVigilantObserver(target, options.from, options.inferredAssignments!, options.signal!);
469
+ setupVigilantObserver(target, options.from, options.infer!, options.signal!);
297
470
  });
298
471
  }
299
472
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * assignFromAsync-extension.js — Adds assignFromAsync to Object.prototype.
3
+ *
4
+ * Import this module for the side effect of extending all objects with
5
+ * the assignFromAsync method, enabling awaitable handler execution:
6
+ *
7
+ * @example
8
+ * import 'assign-gingerly/assignFromAsync-extension.js';
9
+ *
10
+ * await oElement.assignFromAsync({
11
+ * '?.querySelector?..mainView =>': {
12
+ * do: 'builtIns.lazyLoad',
13
+ * get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
14
+ * }
15
+ * }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
16
+ */
17
+
18
+ import { assignFromAsync } from './assignFromAsync.js';
19
+
20
+ Object.defineProperty(Object.prototype, 'assignFromAsync', {
21
+ value: async function (pattern, options) {
22
+ await assignFromAsync(this, pattern, options);
23
+ return this;
24
+ },
25
+ writable: true,
26
+ enumerable: false,
27
+ configurable: true,
28
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * assignFromAsync-extension.ts — Adds assignFromAsync to Object.prototype.
3
+ *
4
+ * Import this module for the side effect of extending all objects with
5
+ * the assignFromAsync method, enabling awaitable handler execution:
6
+ *
7
+ * @example
8
+ * import 'assign-gingerly/assignFromAsync-extension.js';
9
+ *
10
+ * await oElement.assignFromAsync({
11
+ * '?.querySelector?..mainView =>': {
12
+ * do: 'builtIns.lazyLoad',
13
+ * get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
14
+ * }
15
+ * }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
16
+ */
17
+
18
+ import { assignFromAsync } from './assignFromAsync.js';
19
+ import type { AssignFromOptions } from './assignFromAsync.js';
20
+
21
+ declare global {
22
+ interface Object {
23
+ /**
24
+ * Resolve RHS path strings from a source object and assign into this object.
25
+ * Async — awaits handler execution and supports async protocol handlers.
26
+ *
27
+ * @param pattern - Object with LHS paths as keys and RHS path strings (or literals) as values
28
+ * @param options - Configuration including `from` (source object), protocols, withMethods, etc.
29
+ * @returns Promise resolving to this object after assignment
30
+ *
31
+ * @example
32
+ * await oElement.assignFromAsync({
33
+ * '?.querySelector?..outlet =>': {
34
+ * do: 'builtIns.lazyLoad',
35
+ * get: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
36
+ * }
37
+ * }, { from: viewModel, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
38
+ */
39
+ assignFromAsync(
40
+ pattern: Record<string, any>,
41
+ options: AssignFromOptions
42
+ ): Promise<this>;
43
+ }
44
+ }
45
+
46
+ Object.defineProperty(Object.prototype, 'assignFromAsync', {
47
+ value: async function <T extends object>(
48
+ this: T,
49
+ pattern: Record<string, any>,
50
+ options: AssignFromOptions
51
+ ): Promise<T> {
52
+ await assignFromAsync(this, pattern, options);
53
+ return this;
54
+ },
55
+ writable: true,
56
+ enumerable: false,
57
+ configurable: true,
58
+ });
@@ -41,13 +41,14 @@ export async function assignFromAsync(target, pattern, options, permissions) {
41
41
  assignGingerly(target, resolved, options);
42
42
  }
43
43
  // Process #[x] normal keys — resolve element, then apply remaining path + value
44
- if (idRefNormalKeys.length > 0 && options.withIds) {
44
+ if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
45
+ const ids = { ...options.withIds, ...options.at };
45
46
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
46
47
  for (const key of idRefNormalKeys) {
47
48
  const parsed = parseIdRef(key);
48
49
  if (!parsed)
49
50
  continue;
50
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
51
+ const el = resolveIdVariable(parsed.varName, target, ids);
51
52
  if (!el)
52
53
  continue;
53
54
  const value = expandedPattern[key];
@@ -75,14 +76,15 @@ export async function assignFromAsync(target, pattern, options, permissions) {
75
76
  await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
76
77
  }
77
78
  // Process #[x] handler keys — resolve element, then pass to handler processing
78
- if (idRefHandlerKeys.length > 0 && options.withIds) {
79
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
80
+ const ids = { ...options.withIds, ...options.at };
79
81
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
80
82
  _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
81
83
  for (const key of idRefHandlerKeys) {
82
84
  const parsed = parseIdRef(key);
83
85
  if (!parsed)
84
86
  continue;
85
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
87
+ const el = resolveIdVariable(parsed.varName, target, ids);
86
88
  if (!el)
87
89
  continue;
88
90
  // Build a synthetic key for processHandlerCommands:
@@ -97,16 +99,16 @@ export async function assignFromAsync(target, pattern, options, permissions) {
97
99
  }
98
100
  }
99
101
  // Process inferred assignments — dynamically imported only when option is present
100
- if (options.inferredAssignments) {
102
+ if (options.infer) {
101
103
  const { processInferredAssignments } = await import('./inferredAssignments.js');
102
- await processInferredAssignments(target, options.from, options.inferredAssignments);
104
+ await processInferredAssignments(target, options.from, options.infer);
103
105
  // Set up MutationObserver for new matching elements if beVigilant
104
- if (options.inferredAssignments.beVigilant) {
106
+ if (options.infer.beVigilant) {
105
107
  if (!options.signal) {
106
- throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
108
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
107
109
  }
108
110
  const { setupVigilantObserver } = await import('./beVigilant.js');
109
- setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
111
+ setupVigilantObserver(target, options.from, options.infer, options.signal);
110
112
  }
111
113
  }
112
114
  // Process bulk enhancements — dynamically imported only when option is present