@humanspeak/svelte-keyed
Version:
A powerful writable derived store for Svelte that enables deep object and array manipulation with TypeScript support
127 lines (126 loc) • 4.1 kB
JavaScript
import { derived } from 'svelte/store';
/**
* Converts a string path with array notation into an array of tokens.
* Optimized version that avoids unnecessary string operations and uses a single pass.
*
* @param key - The path string to tokenize (e.g., "users[0].name" or "deeply.nested.property")
* @returns An array of string tokens representing each path segment
*
* @example
* ```ts
* getTokens('users[0].name') // returns ['users', '0', 'name']
* getTokens('deeply.nested.property') // returns ['deeply', 'nested', 'property']
* ```
*/
export const getTokens = (key) => {
const tokens = [];
let currentToken = '';
let i = 0;
const len = key.length;
while (i < len) {
const char = key[i];
if (char === '[') {
// If we have accumulated characters, push them as a token
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
// Extract the array index
i++;
let index = '';
while (i < len && key[i] !== ']') {
index += key[i];
i++;
}
tokens.push(index);
i++; // Skip the closing bracket
}
else if (char === '.') {
// Push accumulated token if exists
if (currentToken) {
tokens.push(currentToken);
currentToken = '';
}
i++;
}
else {
currentToken += char;
i++;
}
}
// Push any remaining token
if (currentToken) {
tokens.push(currentToken);
}
return tokens;
};
/**
* Safely retrieves a nested value from an object using an array of key tokens.
* Returns undefined if any intermediate value in the path is null or undefined.
*
* @param root - The root object to traverse
* @param keyTokens - Array of string tokens representing the path to the desired value
* @returns The value at the specified path, or undefined if the path is invalid
*
* @internal
*/
/* trunk-ignore(eslint/@typescript-eslint/no-explicit-any) */
const getNested = (root, keyTokens) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current = root;
for (const key of keyTokens) {
if (current == null) {
return undefined;
}
current = current[key];
}
return current;
};
/**
* Creates a shallow clone of an object while preserving its prototype chain.
*
* @param source - The source object to clone
* @returns A new object with the same properties and prototype as the source
*
* @internal
*/
const clonedWithPrototype = (source) => {
const clone = Object.create(source);
Object.assign(clone, source);
return clone;
};
export function keyed(parent, path) {
const keyTokens = getTokens(path);
if (keyTokens.some((token) => token === '__proto__')) {
throw new Error('key cannot include "__proto__"');
}
const branchTokens = keyTokens.slice(0, keyTokens.length - 1);
const leafToken = keyTokens[keyTokens.length - 1];
const keyedValue = derived(parent, ($parent) => getNested($parent, keyTokens));
const set = (value) => {
parent.update(($parent) => {
if ($parent == null) {
return $parent;
}
const newParent = Array.isArray($parent) ? [...$parent] : clonedWithPrototype($parent);
getNested(newParent, branchTokens)[leafToken] = value;
return newParent;
});
};
const update = (fn) => {
parent.update(($parent) => {
if ($parent == null) {
return $parent;
}
const newValue = fn(getNested($parent, keyTokens));
const newParent = Array.isArray($parent) ? [...$parent] : clonedWithPrototype($parent);
getNested(newParent, branchTokens)[leafToken] = newValue;
return newParent;
});
};
return {
subscribe: keyedValue.subscribe,
set,
update
};
}