assign-gingerly 0.0.1 → 0.0.3
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 +189 -10
- package/assignGingerly.js +352 -0
- package/package.json +9 -9
- package/index.d.ts +0 -35
- package/index.js +0 -187
package/README.md
CHANGED
|
@@ -61,6 +61,176 @@ console.log(obj);
|
|
|
61
61
|
|
|
62
62
|
When the right hand side of an expression is an object, assignGingerly is recursively applied (passing the third argument in if applicable, which will be discussed below)
|
|
63
63
|
|
|
64
|
+
## Example 4 - Incrementing values with !inc command
|
|
65
|
+
|
|
66
|
+
The `!inc` command allows you to increment numeric values:
|
|
67
|
+
|
|
68
|
+
```TypeScript
|
|
69
|
+
const obj = {
|
|
70
|
+
a: {
|
|
71
|
+
b: {
|
|
72
|
+
c: 2
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
assignGingerly(obj, {
|
|
77
|
+
'!inc ?.a?.b?.c': 3,
|
|
78
|
+
'!inc ?.a?.d?.e': -2
|
|
79
|
+
});
|
|
80
|
+
console.log(obj);
|
|
81
|
+
// {
|
|
82
|
+
// a: {
|
|
83
|
+
// b: { c: 5 }, // 2 + 3 = 5
|
|
84
|
+
// d: { e: -2 } // non-existent path created with value -2
|
|
85
|
+
// }
|
|
86
|
+
// }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The `!inc` command syntax is `!inc <path>` where the path can use the `?.` nested notation. The right-hand side value is added to the existing value using `+=`. If the path doesn't exist, it's created and set directly to the value. Non-numeric increments will allow JavaScript to throw its natural error.
|
|
90
|
+
|
|
91
|
+
## Example 5 - Toggling boolean values with !toggle command
|
|
92
|
+
|
|
93
|
+
The `!toggle` command allows you to toggle boolean values either immediately or after a delay:
|
|
94
|
+
|
|
95
|
+
```TypeScript
|
|
96
|
+
const obj = {
|
|
97
|
+
a: {
|
|
98
|
+
b: {
|
|
99
|
+
c: true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
assignGingerly(obj, {
|
|
104
|
+
'!toggle ?.a?.b?.c': 0, // Toggle immediately
|
|
105
|
+
'!toggle ?.a?.d?.e': 20 // Toggle after 20ms
|
|
106
|
+
});
|
|
107
|
+
console.log(obj);
|
|
108
|
+
// {
|
|
109
|
+
// a: {
|
|
110
|
+
// b: { c: false } // Toggled immediately
|
|
111
|
+
// // d doesn't exist yet
|
|
112
|
+
// }
|
|
113
|
+
// }
|
|
114
|
+
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
console.log(obj);
|
|
117
|
+
// {
|
|
118
|
+
// a: {
|
|
119
|
+
// b: { c: false },
|
|
120
|
+
// d: { e: true } // Created and toggled after 20ms
|
|
121
|
+
// }
|
|
122
|
+
// }
|
|
123
|
+
}, 40);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `!toggle` command syntax is `!toggle <path>` where the path can use the `?.` nested notation. The right-hand side value determines the behavior:
|
|
127
|
+
- **RHS = 0**: Toggle the existing value immediately (non-existent paths are not created)
|
|
128
|
+
- **RHS > 0**: Schedule the toggle to happen after N milliseconds (non-existent paths are created and initialized to `true`)
|
|
129
|
+
|
|
130
|
+
For existing values, the toggle is performed using JavaScript's logical NOT operator (`!value`). Non-numeric delay values will be passed to `setTimeout` and may throw an error.
|
|
131
|
+
|
|
132
|
+
## Example 6 - Deleting properties with !delete command
|
|
133
|
+
|
|
134
|
+
The `!delete` command allows you to delete properties either immediately or after a delay:
|
|
135
|
+
|
|
136
|
+
```TypeScript
|
|
137
|
+
const obj = {
|
|
138
|
+
a: {
|
|
139
|
+
b: {
|
|
140
|
+
c: true,
|
|
141
|
+
d: 'hello'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
assignGingerly(obj, {
|
|
146
|
+
'!delete ?.a?.b?.c': 0, // Delete immediately
|
|
147
|
+
'!delete ?.a?.b': 20 // Delete after 20ms
|
|
148
|
+
});
|
|
149
|
+
console.log(obj);
|
|
150
|
+
// {
|
|
151
|
+
// a: {
|
|
152
|
+
// b: { d: 'hello' } // c deleted immediately
|
|
153
|
+
// }
|
|
154
|
+
// }
|
|
155
|
+
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
console.log(obj);
|
|
158
|
+
// {
|
|
159
|
+
// a: {} // b deleted after 20ms
|
|
160
|
+
// }
|
|
161
|
+
}, 40);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The `!delete` command syntax is `!delete <path>` where the path can use the `?.` nested notation. The right-hand side value determines the behavior:
|
|
165
|
+
- **RHS = 0**: Delete the final property immediately (non-existent paths are silently skipped)
|
|
166
|
+
- **RHS > 0**: Schedule the deletion to happen after N milliseconds (non-existent paths are silently skipped)
|
|
167
|
+
|
|
168
|
+
**Important**: The `!delete` command only deletes the **final property** in the path. The entire nested chain is not deleted. For example, `'!delete ?.a?.b?.c': 0` only deletes property `c`, leaving the structure `a.b` intact. If any intermediate path doesn't exist, the command is silently skipped without error.
|
|
169
|
+
|
|
170
|
+
## Example 7 - Reversible assignments with assignTentatively
|
|
171
|
+
|
|
172
|
+
The `assignTentatively` function works like `assignGingerly` but with a powerful addition: **reversibility**. It tracks changes and generates a reversal object that can undo all modifications:
|
|
173
|
+
|
|
174
|
+
```TypeScript
|
|
175
|
+
import assignTentatively from 'assign-gingerly/assignTentatively';
|
|
176
|
+
|
|
177
|
+
const obj = { f: { g: 'hello' } };
|
|
178
|
+
const reversal = {};
|
|
179
|
+
|
|
180
|
+
assignTentatively(obj, {
|
|
181
|
+
'?.style?.height': '15px',
|
|
182
|
+
'?.a?.b?.c': {
|
|
183
|
+
d: 'hello',
|
|
184
|
+
e: 'world'
|
|
185
|
+
},
|
|
186
|
+
'?.f?.g': 'bye'
|
|
187
|
+
}, { reversal });
|
|
188
|
+
|
|
189
|
+
console.log(obj);
|
|
190
|
+
// {
|
|
191
|
+
// f: { g: 'bye' },
|
|
192
|
+
// style: { height: '15px' },
|
|
193
|
+
// a: { b: { c: { d: 'hello', e: 'world' } } }
|
|
194
|
+
// }
|
|
195
|
+
|
|
196
|
+
console.log(reversal);
|
|
197
|
+
// {
|
|
198
|
+
// '!delete ?.a': 0,
|
|
199
|
+
// '!delete ?.style': 0,
|
|
200
|
+
// '?.f?.g': 'hello'
|
|
201
|
+
// }
|
|
202
|
+
|
|
203
|
+
// Later, restore to original state:
|
|
204
|
+
assignTentatively(obj, reversal);
|
|
205
|
+
console.log(obj);
|
|
206
|
+
// {
|
|
207
|
+
// f: { g: 'hello' }
|
|
208
|
+
// }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Key differences from assignGingerly:**
|
|
212
|
+
- **No setTimeout support**: All `!toggle`, `!inc`, and `!delete` commands execute immediately, regardless of the RHS value
|
|
213
|
+
- **No registry/DI support**: Dependency injection features are not available (pass it in and it will be ignored)
|
|
214
|
+
- **Reversal tracking**: Maintains a reversal object that records:
|
|
215
|
+
- **Original values** of modified existing properties
|
|
216
|
+
- **!delete commands** for newly created top-level paths (e.g., `!delete ?.a` for paths created under `a`)
|
|
217
|
+
- **Original values** for deleted properties
|
|
218
|
+
|
|
219
|
+
**Reversal guarantee:**
|
|
220
|
+
```JavaScript
|
|
221
|
+
const reversal = {};
|
|
222
|
+
const obj = {...originalObj};
|
|
223
|
+
const string1 = JSON.stringify(obj);
|
|
224
|
+
|
|
225
|
+
assignTentatively(obj, sourceChanges, { reversal });
|
|
226
|
+
assignTentatively(obj, reversal);
|
|
227
|
+
|
|
228
|
+
const string2 = JSON.stringify(obj);
|
|
229
|
+
console.log(string1 === string2); // true
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This guarantees that applying the reversal object restores the object to its exact original state.
|
|
233
|
+
|
|
64
234
|
## Dependency injection based on a registry object and a Symbolic reference
|
|
65
235
|
|
|
66
236
|
```Typescript
|
|
@@ -84,32 +254,30 @@ class YourEnhancement extends ElementEnhancement(EventTarget){
|
|
|
84
254
|
}
|
|
85
255
|
|
|
86
256
|
class BaseRegistry{
|
|
87
|
-
|
|
257
|
+
push(IBaseRegistryItem | IBaseRegistryItem[]){
|
|
88
258
|
...
|
|
89
259
|
}
|
|
90
260
|
}
|
|
91
261
|
|
|
92
262
|
//Here's where the dependency injection mapping takes place
|
|
93
263
|
const baseRegistry = new BaseRegistry;
|
|
94
|
-
baseRegistry.
|
|
264
|
+
baseRegistry.push([
|
|
95
265
|
{
|
|
96
266
|
map: {
|
|
97
267
|
[isHappy]: 'isHappy'
|
|
98
268
|
},
|
|
99
|
-
spawn: MyEnhancement
|
|
269
|
+
spawn: MyEnhancement,
|
|
100
270
|
},{
|
|
101
271
|
|
|
102
272
|
map: {
|
|
103
273
|
[isMellow]: 'isMellow'
|
|
104
274
|
},
|
|
105
|
-
spawn:
|
|
106
|
-
return YourEnhancement;
|
|
107
|
-
}
|
|
275
|
+
spawn: YourEnhancement,
|
|
108
276
|
}
|
|
109
277
|
]);
|
|
110
278
|
//end of dependency injection
|
|
111
279
|
|
|
112
|
-
const
|
|
280
|
+
const result = assignGingerly({}, {
|
|
113
281
|
[isHappy]: true,
|
|
114
282
|
[isMellow]: true,
|
|
115
283
|
'?.style.height': '40px',
|
|
@@ -117,15 +285,26 @@ const asyncResult = await assignGingerly({}, {
|
|
|
117
285
|
}, {
|
|
118
286
|
registry: BaseRegistry
|
|
119
287
|
});
|
|
120
|
-
|
|
288
|
+
result.set[isMellow] = false;
|
|
121
289
|
```
|
|
122
290
|
|
|
123
|
-
The assignGingerly searches the registry for any items that has a mapping with a matching symbol of isHappy and isMellow, and if found, sees it already has an instance of the spawn class associated with the first passed in parameter. If no such instance is found, it instantiates one, associates the instance with the first parameter, then sets the property value.
|
|
291
|
+
The assignGingerly searches the registry for any items that has a mapping with a matching symbol of isHappy and isMellow, and if found, sees if it already has an instance of the spawn class associated with the first passed in parameter. If no such instance is found, it instantiates one, associates the instance with the first parameter, then sets the property value.
|
|
124
292
|
|
|
125
293
|
It also adds a lazy property to the first passed in parameter, "set", which returns a proxy, and that proxy watches for symbol references passed in a value, and sets the value from that spawned instance. Again, if the spawned instance is not found, it respawns it.
|
|
126
294
|
|
|
127
295
|
The suggestion to use Symbol.for with a guid, as opposed to just Symbol(), is based on some negative experiences I've had with multiple versions of the same library being referenced, but is not required. Regular symbols could also be used when that risk can be avoided.
|
|
128
296
|
|
|
129
|
-
|
|
297
|
+
## Support for JSON assignment with Symbol.for symbols
|
|
298
|
+
|
|
299
|
+
```JavaScript
|
|
300
|
+
const result = assignGingerly({}, {
|
|
301
|
+
"[Symbol.for('TFWsx0YH5E6eSfhE7zfLxA')]": true,
|
|
302
|
+
"[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
|
|
303
|
+
'?.style.height': '40px',
|
|
304
|
+
'?.enhancements?.mellowYellow?.madAboutFourteen': true
|
|
305
|
+
}, {
|
|
306
|
+
registry: BaseRegistry
|
|
307
|
+
});
|
|
308
|
+
```
|
|
130
309
|
|
|
131
310
|
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map to store spawned instances associated with objects
|
|
3
|
+
*/
|
|
4
|
+
const instanceMap = new WeakMap();
|
|
5
|
+
/**
|
|
6
|
+
* Base registry class for managing dependency injection
|
|
7
|
+
*/
|
|
8
|
+
export class BaseRegistry {
|
|
9
|
+
items = [];
|
|
10
|
+
push(items) {
|
|
11
|
+
if (Array.isArray(items)) {
|
|
12
|
+
this.items.push(...items);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.items.push(items);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
getItems() {
|
|
19
|
+
return this.items;
|
|
20
|
+
}
|
|
21
|
+
findBySymbol(symbol) {
|
|
22
|
+
return this.items.find(item => {
|
|
23
|
+
const map = item.map;
|
|
24
|
+
return Object.keys(map).some(key => {
|
|
25
|
+
if (typeof key === 'symbol' || (typeof map[key] === 'symbol')) {
|
|
26
|
+
return key === symbol || map[key] === symbol;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}) || Object.getOwnPropertySymbols(map).some(sym => sym === symbol);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Helper function to check if a string key represents a Symbol.for expression
|
|
35
|
+
*/
|
|
36
|
+
function isSymbolForKey(key) {
|
|
37
|
+
return key.startsWith('[Symbol.for(') && key.endsWith(')]');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Helper function to extract the symbol key from a Symbol.for string
|
|
41
|
+
*/
|
|
42
|
+
function parseSymbolForKey(key) {
|
|
43
|
+
const match = key.match(/^\[Symbol\.for\(['"](.+)['"]\)\]$/);
|
|
44
|
+
if (match && match[1]) {
|
|
45
|
+
return Symbol.for(match[1]);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Helper function to check if a key represents an !inc command
|
|
51
|
+
*/
|
|
52
|
+
function isIncCommand(key) {
|
|
53
|
+
return key.startsWith('!inc ');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Helper function to parse an !inc command and extract the path
|
|
57
|
+
*/
|
|
58
|
+
function parseIncCommand(key) {
|
|
59
|
+
if (!isIncCommand(key)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return key.substring(5); // Remove '!inc ' prefix
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Helper function to check if a key represents a !toggle command
|
|
66
|
+
*/
|
|
67
|
+
function isToggleCommand(key) {
|
|
68
|
+
return key.startsWith('!toggle ');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Helper function to parse a !toggle command and extract the path
|
|
72
|
+
*/
|
|
73
|
+
function parseToggleCommand(key) {
|
|
74
|
+
if (!isToggleCommand(key)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return key.substring(8); // Remove '!toggle ' prefix
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Helper function to check if a key represents a !delete command
|
|
81
|
+
*/
|
|
82
|
+
function isDeleteCommand(key) {
|
|
83
|
+
return key.startsWith('!delete ');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Helper function to parse a !delete command and extract the path
|
|
87
|
+
*/
|
|
88
|
+
function parseDeleteCommand(key) {
|
|
89
|
+
if (!isDeleteCommand(key)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return key.substring(8); // Remove '!delete ' prefix
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Helper function to parse a path string with ?. notation
|
|
96
|
+
*/
|
|
97
|
+
function parsePath(path) {
|
|
98
|
+
return path
|
|
99
|
+
.split('.')
|
|
100
|
+
.map(part => part.replace(/\?/g, ''))
|
|
101
|
+
.filter(part => part.length > 0);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Helper function to check if a path starts with ?. notation
|
|
105
|
+
*/
|
|
106
|
+
function isNestedPath(path) {
|
|
107
|
+
return path.startsWith('?.');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Helper function to get or create a nested object
|
|
111
|
+
*/
|
|
112
|
+
function ensureNestedPath(obj, pathParts) {
|
|
113
|
+
let current = obj;
|
|
114
|
+
for (const part of pathParts.slice(0, -1)) {
|
|
115
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
116
|
+
current[part] = {};
|
|
117
|
+
}
|
|
118
|
+
current = current[part];
|
|
119
|
+
}
|
|
120
|
+
return current;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Main assignGingerly function
|
|
124
|
+
*/
|
|
125
|
+
export function assignGingerly(target, source, options) {
|
|
126
|
+
if (!target || typeof target !== 'object') {
|
|
127
|
+
return target;
|
|
128
|
+
}
|
|
129
|
+
const registry = options?.registry instanceof BaseRegistry
|
|
130
|
+
? options.registry
|
|
131
|
+
: options?.registry
|
|
132
|
+
? new options.registry()
|
|
133
|
+
: undefined;
|
|
134
|
+
// Convert Symbol.for string keys to actual symbols
|
|
135
|
+
const processedSource = {};
|
|
136
|
+
for (const key of Object.keys(source)) {
|
|
137
|
+
if (isSymbolForKey(key)) {
|
|
138
|
+
const symbol = parseSymbolForKey(key);
|
|
139
|
+
if (symbol) {
|
|
140
|
+
processedSource[symbol] = source[key];
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Invalid Symbol.for format - treat as regular string key
|
|
144
|
+
processedSource[key] = source[key];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
processedSource[key] = source[key];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Copy over actual symbol keys
|
|
152
|
+
for (const sym of Object.getOwnPropertySymbols(source)) {
|
|
153
|
+
processedSource[sym] = source[sym];
|
|
154
|
+
}
|
|
155
|
+
// First pass: handle all non-symbol keys and sync operations
|
|
156
|
+
for (const key of Object.keys(processedSource)) {
|
|
157
|
+
const value = processedSource[key];
|
|
158
|
+
// Handle !inc commands
|
|
159
|
+
if (isIncCommand(key)) {
|
|
160
|
+
const path = parseIncCommand(key);
|
|
161
|
+
if (path) {
|
|
162
|
+
const pathParts = parsePath(path);
|
|
163
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
164
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
165
|
+
// If the path doesn't exist, set it directly to the value
|
|
166
|
+
if (!(lastKey in parent)) {
|
|
167
|
+
parent[lastKey] = value;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Path exists, apply increment: oldValue += newValue
|
|
171
|
+
parent[lastKey] += value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Handle !toggle commands
|
|
177
|
+
if (isToggleCommand(key)) {
|
|
178
|
+
const path = parseToggleCommand(key);
|
|
179
|
+
if (path) {
|
|
180
|
+
const delay = value;
|
|
181
|
+
if (delay === 0) {
|
|
182
|
+
// Immediate toggle
|
|
183
|
+
const pathParts = parsePath(path);
|
|
184
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
185
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
186
|
+
if (lastKey in parent) {
|
|
187
|
+
// Path exists, toggle it
|
|
188
|
+
parent[lastKey] = !parent[lastKey];
|
|
189
|
+
}
|
|
190
|
+
// If path doesn't exist, don't create it for immediate toggle
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Delayed toggle using setTimeout
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
const pathParts = parsePath(path);
|
|
196
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
197
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
198
|
+
if (lastKey in parent) {
|
|
199
|
+
// Path exists, toggle it
|
|
200
|
+
parent[lastKey] = !parent[lastKey];
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Path doesn't exist, initialize to true
|
|
204
|
+
parent[lastKey] = true;
|
|
205
|
+
}
|
|
206
|
+
}, delay);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Handle !delete commands
|
|
212
|
+
if (isDeleteCommand(key)) {
|
|
213
|
+
const path = parseDeleteCommand(key);
|
|
214
|
+
if (path) {
|
|
215
|
+
const delay = value;
|
|
216
|
+
if (delay === 0) {
|
|
217
|
+
// Immediate delete
|
|
218
|
+
const pathParts = parsePath(path);
|
|
219
|
+
if (pathParts.length > 0) {
|
|
220
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
221
|
+
const parentPathParts = pathParts.slice(0, -1);
|
|
222
|
+
// Navigate to parent without creating intermediate paths
|
|
223
|
+
let parent = target;
|
|
224
|
+
let canDelete = true;
|
|
225
|
+
for (const part of parentPathParts) {
|
|
226
|
+
if (!(part in parent) || typeof parent[part] !== 'object' || parent[part] === null) {
|
|
227
|
+
canDelete = false;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
parent = parent[part];
|
|
231
|
+
}
|
|
232
|
+
if (canDelete && lastKey in parent) {
|
|
233
|
+
delete parent[lastKey];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Delayed delete using setTimeout
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
const pathParts = parsePath(path);
|
|
241
|
+
if (pathParts.length > 0) {
|
|
242
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
243
|
+
const parentPathParts = pathParts.slice(0, -1);
|
|
244
|
+
// Navigate to parent without creating intermediate paths
|
|
245
|
+
let parent = target;
|
|
246
|
+
let canDelete = true;
|
|
247
|
+
for (const part of parentPathParts) {
|
|
248
|
+
if (!(part in parent) || typeof parent[part] !== 'object' || parent[part] === null) {
|
|
249
|
+
canDelete = false;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
parent = parent[part];
|
|
253
|
+
}
|
|
254
|
+
if (canDelete && lastKey in parent) {
|
|
255
|
+
delete parent[lastKey];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}, delay);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (isNestedPath(key)) {
|
|
264
|
+
const pathParts = parsePath(key);
|
|
265
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
266
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
267
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
268
|
+
// Recursively apply assignGingerly for nested objects
|
|
269
|
+
if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
|
|
270
|
+
parent[lastKey] = {};
|
|
271
|
+
}
|
|
272
|
+
assignGingerly(parent[lastKey], value, options);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
parent[lastKey] = value;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
280
|
+
// Recursively apply assignGingerly for nested objects
|
|
281
|
+
if (!(key in target) || typeof target[key] !== 'object') {
|
|
282
|
+
target[key] = {};
|
|
283
|
+
}
|
|
284
|
+
assignGingerly(target[key], value, options);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
target[key] = value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Second pass: handle symbol keys for dependency injection
|
|
292
|
+
const symbols = Object.getOwnPropertySymbols(processedSource);
|
|
293
|
+
for (const sym of symbols) {
|
|
294
|
+
const value = processedSource[sym];
|
|
295
|
+
if (registry) {
|
|
296
|
+
const registryItem = registry.findBySymbol(sym);
|
|
297
|
+
if (registryItem) {
|
|
298
|
+
// Get or initialize the instances map for this target
|
|
299
|
+
if (!instanceMap.has(target)) {
|
|
300
|
+
instanceMap.set(target, new Map());
|
|
301
|
+
}
|
|
302
|
+
const instances = instanceMap.get(target);
|
|
303
|
+
// Check if instance already exists
|
|
304
|
+
let instance = instances.get(sym);
|
|
305
|
+
if (!instance) {
|
|
306
|
+
const SpawnClass = registryItem.spawn;
|
|
307
|
+
instance = new SpawnClass();
|
|
308
|
+
instances.set(sym, instance);
|
|
309
|
+
}
|
|
310
|
+
// Find the mapped property name
|
|
311
|
+
const mappedKey = registryItem.map[sym];
|
|
312
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
313
|
+
instance[mappedKey] = value;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Add lazy 'set' property that returns a proxy
|
|
319
|
+
if (registry && !('set' in target)) {
|
|
320
|
+
Object.defineProperty(target, 'set', {
|
|
321
|
+
get() {
|
|
322
|
+
return new Proxy({}, {
|
|
323
|
+
set: (_, prop, value) => {
|
|
324
|
+
if (typeof prop === 'symbol') {
|
|
325
|
+
const registryItem = registry.findBySymbol(prop);
|
|
326
|
+
if (registryItem) {
|
|
327
|
+
if (!instanceMap.has(target)) {
|
|
328
|
+
instanceMap.set(target, new Map());
|
|
329
|
+
}
|
|
330
|
+
const instances = instanceMap.get(target);
|
|
331
|
+
let instance = instances.get(prop);
|
|
332
|
+
if (!instance) {
|
|
333
|
+
const SpawnClass = registryItem.spawn;
|
|
334
|
+
instance = new SpawnClass();
|
|
335
|
+
instances.set(prop, instance);
|
|
336
|
+
}
|
|
337
|
+
const mappedKey = registryItem.map[prop];
|
|
338
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
339
|
+
instance[mappedKey] = value;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
configurable: true,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return target;
|
|
351
|
+
}
|
|
352
|
+
export default assignGingerly;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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": {
|
|
@@ -13,29 +13,29 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
|
|
15
15
|
"type": "module",
|
|
16
|
-
"types": "
|
|
16
|
+
"types": "types.d.ts",
|
|
17
17
|
"files": [
|
|
18
|
-
"
|
|
18
|
+
"assignGingerly.js",
|
|
19
19
|
"index.d.ts",
|
|
20
20
|
"README.md",
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|
|
25
|
-
"import": "./
|
|
26
|
-
"types": "./
|
|
25
|
+
"import": "./assignGingerly.js",
|
|
26
|
+
"types": "./types.d.ts"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
|
-
"main": "
|
|
29
|
+
"main": "assignGingerly.js",
|
|
30
30
|
"scripts": {
|
|
31
31
|
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
32
32
|
"test": "playwright test",
|
|
33
33
|
"update": "ncu -u && npm install"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@playwright/test": "^1.
|
|
37
|
-
"spa-ssi": "0.0.
|
|
38
|
-
"@types/node": "^
|
|
36
|
+
"@playwright/test": "^1.58.0",
|
|
37
|
+
"spa-ssi": "0.0.26",
|
|
38
|
+
"@types/node": "^25.0.10",
|
|
39
39
|
"typescript": "^5.9.3"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/index.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interface for registry items that define dependency injection mappings
|
|
3
|
-
*/
|
|
4
|
-
export interface IBaseRegistryItem<T = any> {
|
|
5
|
-
spawn: { new (): T } | Promise<{ new (): T }>;
|
|
6
|
-
map: { [key: string | symbol]: keyof T };
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Interface for the options passed to assignGingerly
|
|
11
|
-
*/
|
|
12
|
-
export interface IAssignGingerlyOptions {
|
|
13
|
-
registry?: typeof BaseRegistry | BaseRegistry;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Base registry class for managing dependency injection
|
|
18
|
-
*/
|
|
19
|
-
export declare class BaseRegistry {
|
|
20
|
-
private items;
|
|
21
|
-
define(items: IBaseRegistryItem | IBaseRegistryItem[]): void;
|
|
22
|
-
getItems(): IBaseRegistryItem[];
|
|
23
|
-
findBySymbol(symbol: symbol | string): IBaseRegistryItem | undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Main assignGingerly function
|
|
28
|
-
*/
|
|
29
|
-
export declare function assignGingerly(
|
|
30
|
-
target: any,
|
|
31
|
-
source: Record<string | symbol, any>,
|
|
32
|
-
options?: IAssignGingerlyOptions
|
|
33
|
-
): Promise<any>;
|
|
34
|
-
|
|
35
|
-
export default assignGingerly;
|
package/index.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map to store spawned instances associated with objects
|
|
3
|
-
*/
|
|
4
|
-
const instanceMap = new WeakMap();
|
|
5
|
-
/**
|
|
6
|
-
* Base registry class for managing dependency injection
|
|
7
|
-
*/
|
|
8
|
-
export class BaseRegistry {
|
|
9
|
-
items = [];
|
|
10
|
-
define(items) {
|
|
11
|
-
if (Array.isArray(items)) {
|
|
12
|
-
this.items.push(...items);
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
this.items.push(items);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
getItems() {
|
|
19
|
-
return this.items;
|
|
20
|
-
}
|
|
21
|
-
findBySymbol(symbol) {
|
|
22
|
-
return this.items.find(item => {
|
|
23
|
-
const map = item.map;
|
|
24
|
-
return Object.keys(map).some(key => {
|
|
25
|
-
if (typeof key === 'symbol' || (typeof map[key] === 'symbol')) {
|
|
26
|
-
return key === symbol || map[key] === symbol;
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
}) || Object.getOwnPropertySymbols(map).some(sym => sym === symbol);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Helper function to parse a path string with ?. notation
|
|
35
|
-
*/
|
|
36
|
-
function parsePath(path) {
|
|
37
|
-
return path
|
|
38
|
-
.split('.')
|
|
39
|
-
.map(part => part.replace(/\?/g, ''))
|
|
40
|
-
.filter(part => part.length > 0);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Helper function to check if a path starts with ?. notation
|
|
44
|
-
*/
|
|
45
|
-
function isNestedPath(path) {
|
|
46
|
-
return path.startsWith('?.');
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Helper function to get or create a nested object
|
|
50
|
-
*/
|
|
51
|
-
function ensureNestedPath(obj, pathParts) {
|
|
52
|
-
let current = obj;
|
|
53
|
-
for (const part of pathParts.slice(0, -1)) {
|
|
54
|
-
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
55
|
-
current[part] = {};
|
|
56
|
-
}
|
|
57
|
-
current = current[part];
|
|
58
|
-
}
|
|
59
|
-
return current;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Main assignGingerly function
|
|
63
|
-
*/
|
|
64
|
-
export async function assignGingerly(target, source, options) {
|
|
65
|
-
if (!target || typeof target !== 'object') {
|
|
66
|
-
return target;
|
|
67
|
-
}
|
|
68
|
-
const registry = options?.registry instanceof BaseRegistry
|
|
69
|
-
? options.registry
|
|
70
|
-
: options?.registry
|
|
71
|
-
? new options.registry()
|
|
72
|
-
: undefined;
|
|
73
|
-
// Track promises for async spawning
|
|
74
|
-
const asyncSpawns = [];
|
|
75
|
-
// First pass: handle all non-symbol keys and sync operations
|
|
76
|
-
for (const key of Object.keys(source)) {
|
|
77
|
-
const value = source[key];
|
|
78
|
-
if (isNestedPath(key)) {
|
|
79
|
-
const pathParts = parsePath(key);
|
|
80
|
-
const lastKey = pathParts[pathParts.length - 1];
|
|
81
|
-
const parent = ensureNestedPath(target, pathParts);
|
|
82
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
83
|
-
// Recursively apply assignGingerly for nested objects
|
|
84
|
-
if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
|
|
85
|
-
parent[lastKey] = {};
|
|
86
|
-
}
|
|
87
|
-
await assignGingerly(parent[lastKey], value, options);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
parent[lastKey] = value;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
95
|
-
// Recursively apply assignGingerly for nested objects
|
|
96
|
-
if (!(key in target) || typeof target[key] !== 'object') {
|
|
97
|
-
target[key] = {};
|
|
98
|
-
}
|
|
99
|
-
await assignGingerly(target[key], value, options);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
target[key] = value;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Second pass: handle symbol keys for dependency injection
|
|
107
|
-
const symbols = Object.getOwnPropertySymbols(source);
|
|
108
|
-
for (const sym of symbols) {
|
|
109
|
-
const value = source[sym];
|
|
110
|
-
if (registry) {
|
|
111
|
-
const registryItem = registry.findBySymbol(sym);
|
|
112
|
-
if (registryItem) {
|
|
113
|
-
// Get or initialize the instances map for this target
|
|
114
|
-
if (!instanceMap.has(target)) {
|
|
115
|
-
instanceMap.set(target, new Map());
|
|
116
|
-
}
|
|
117
|
-
const instances = instanceMap.get(target);
|
|
118
|
-
// Check if instance already exists
|
|
119
|
-
let instance = instances.get(sym);
|
|
120
|
-
if (!instance) {
|
|
121
|
-
// Check if spawn is a constructor or a promise
|
|
122
|
-
const SpawnClass = await Promise.resolve(registryItem.spawn);
|
|
123
|
-
instance = new SpawnClass();
|
|
124
|
-
instances.set(sym, instance);
|
|
125
|
-
}
|
|
126
|
-
// Find the mapped property name
|
|
127
|
-
const mappedKey = registryItem.map[sym];
|
|
128
|
-
if (mappedKey && instance && typeof instance === 'object') {
|
|
129
|
-
instance[mappedKey] = value;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// Add lazy 'set' property that returns a proxy
|
|
135
|
-
if (registry && !('set' in target)) {
|
|
136
|
-
Object.defineProperty(target, 'set', {
|
|
137
|
-
get() {
|
|
138
|
-
return new Proxy({}, {
|
|
139
|
-
set: (_, prop, value) => {
|
|
140
|
-
if (typeof prop === 'symbol') {
|
|
141
|
-
const registryItem = registry.findBySymbol(prop);
|
|
142
|
-
if (registryItem) {
|
|
143
|
-
if (!instanceMap.has(target)) {
|
|
144
|
-
instanceMap.set(target, new Map());
|
|
145
|
-
}
|
|
146
|
-
const instances = instanceMap.get(target);
|
|
147
|
-
let instance = instances.get(prop);
|
|
148
|
-
if (!instance) {
|
|
149
|
-
const SpawnClass = registryItem.spawn;
|
|
150
|
-
if (SpawnClass instanceof Promise) {
|
|
151
|
-
// Handle async case - would need to be awaited externally
|
|
152
|
-
SpawnClass.then((SC) => {
|
|
153
|
-
instance = new SC();
|
|
154
|
-
instances.set(prop, instance);
|
|
155
|
-
const mappedKey = registryItem.map[prop];
|
|
156
|
-
if (mappedKey && instance && typeof instance === 'object') {
|
|
157
|
-
instance[mappedKey] = value;
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
instance = new SpawnClass();
|
|
163
|
-
instances.set(prop, instance);
|
|
164
|
-
const mappedKey = registryItem.map[prop];
|
|
165
|
-
if (mappedKey && instance && typeof instance === 'object') {
|
|
166
|
-
instance[mappedKey] = value;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
const mappedKey = registryItem.map[prop];
|
|
172
|
-
if (mappedKey && instance && typeof instance === 'object') {
|
|
173
|
-
instance[mappedKey] = value;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return true;
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
},
|
|
182
|
-
configurable: true,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
return target;
|
|
186
|
-
}
|
|
187
|
-
export default assignGingerly;
|