genome 0.1.1 → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Valtio Universe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,153 +1,286 @@
1
+ ![NPM Version](https://img.shields.io/npm/v/genome?style=flat-square&color=%23e8b339)
2
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/overthemike/genome/test.yml?style=flat-square&color=%23e8b339)
3
+ ![npm bundle size](https://img.shields.io/bundlephobia/minzip/genome?style=flat-square&color=%23e8b339)
4
+ ![NPM License](https://img.shields.io/npm/l/genome?style=flat-square&color=%23e8b339)
5
+
6
+
1
7
  # genome
2
8
 
3
- Simple build system using ES6 classes, generators, and promises
9
+ A lightweight, robust library for generating unique identifiers for JavaScript/TypeScript objects based on their structure rather than requiring explicit string keys.
10
+
11
+ ## Purpose
12
+
13
+ This library provides a solution for scenarios where you need to:
14
+
15
+ - Persist and rehydrate state without requiring explicit string keys
16
+ - Identify structurally identical objects across different instances
17
+ - Match objects by their shape rather than by identity or manual keys
18
+ - Detect circular references safely
4
19
 
5
20
  ## Installation
6
- genome requires io.js or node.js with harmony flags.
7
21
 
8
- ```
9
- npm i -g genome
10
- npm i -save-dev genome
22
+ ```bash
23
+ pnpm add genome
11
24
  ```
12
25
 
13
- ## Usage
14
- Create a `genomefile.js` in your project's root directory.
26
+ ## Basic Usage
27
+ ```typescript
28
+ import { generateStructureId, getCompactId } from 'genome';
29
+
30
+ // Example object
31
+ const user = {
32
+ name: 'John',
33
+ age: 30,
34
+ preferences: {
35
+ theme: 'dark',
36
+ notifications: true
37
+ }
38
+ }
39
+
40
+ // Generate a unique ID based on the object structure and it's properties' types
41
+ const id = generateStructureId(user) // L0:3713-L1:5761-L2:13827
42
+
43
+ // Generate a unique ID and then hash that value
44
+ const hashed = getCompactId(user) // cd76ea96
15
45
 
16
- ```javascript
17
- var genome = require('genome');
46
+ // Get info on what a structure would be without generating
47
+ const {
48
+ id, // L0:541598767187353870402585606-L1:1547425049106725343623905933-L2:10
49
+ levels, // 3
50
+ collisions // 0 ID collisions - same object structure already ran through generator
51
+ } = getStructureInfo(user)
18
52
 
19
- genome.tasks = {
20
- // Tasks go here
21
- };
53
+ // Get info for a compact id
54
+ const {
55
+ id, // cd76ea96
56
+ levels, // 3
57
+ collisions // 0 ID collisions - same object structure already ran through generator
58
+ } = getCompactInfo(user)
22
59
 
23
- // Run tasks passed in from command line
24
- genome.run();
60
+ // get all of the data being stored about the state - debugging info
61
+ const allStructureStateData = exportStructureState()
25
62
  ```
26
63
 
27
- ### Create tasks
28
- Tasks in genome are generator functions. Task names may include colons (:) and/or hyphens (-).
64
+ ## API Reference
29
65
 
30
- ```javascript
31
- * sayhi() {
32
- console.log('Hello, world');
33
- },
66
+ ### `generateStructureId(obj: Record<string, any>, config?: StructureIdConfig): string`
34
67
 
35
- * 'say-something-else'() {
36
- console.log('Something else');
37
- },
68
+ Generates a unique ID string based on the structure of the provided object.
38
69
 
39
- * 'say:goodbye'() {
40
- console.log('See ya later');
41
- }
42
- ```
70
+ - **Parameters**:
71
+ - `obj`: The object to generate an ID for.
72
+ - `config` (optional): Configuration options for ID generation.
73
+ - **Returns**: A string representing the structure ID.
43
74
 
44
- ### Run tasks
45
- In your command line, run:
75
+ ### `getStructureInfo(obj: Record<string, any>, config?: StructureIdConfig): { id: string; levels: number; collisionCount: number; }`
46
76
 
47
- ```
48
- genome sayhi
49
- ```
77
+ Provides additional information about the object's structure.
50
78
 
51
- genome commands may accept multiple tasks to run asyncronously:
79
+ - **Parameters**:
80
+ - `obj`: The object to analyze.
81
+ - `config` (optional): Configuration options for ID generation.
82
+ - **Returns**: An object containing:
83
+ - `id`: The structure ID.
84
+ - `levels`: The number of nesting levels in the object.
85
+ - `collisionCount`: The number of times this structure has been encountered.
52
86
 
53
- ```
54
- genome sayhi say-something-else say:goodbye
55
- ```
87
+ ### `setStructureIdConfig(config: StructureIdConfig): void`
56
88
 
57
- Run a task from within another task using `genome.spawn()`.
89
+ Sets global configuration options for structure ID generation.
58
90
 
59
- ```javascript
60
- * speak() {
61
- genome.spawn('sayhi');
91
+ - **Parameters**:
92
+ - `config`: The configuration object.
62
93
 
63
- // Or use genome's shorthand methods:
64
- genome.sayhi();
65
- }
66
- ```
94
+ ### `getStructureIdConfig(): StructureIdConfig`
95
+
96
+ Gets the current global configuration.
67
97
 
68
- `genome.spawn()` accepts strings and arrays. Arrays of tasks will be run asyncronously.
98
+ - **Returns**: A copy of the current global configuration object.
69
99
 
70
- ```javascript
71
- * speak1() {
72
- genome.spawn(['sayhi', 'say-something-else', 'say:goodbye']);
73
- },
100
+ ### `resetState(): void`
74
101
 
75
- // Is the same as:
102
+ Resets the internal state of the library, clearing all cached property mappings.
76
103
 
77
- * speak2() {
78
- genome.spawn('sayhi');
79
- genome.spawn('say-something-else');
80
- genome.spawn('say:goodbye');
104
+ **Note**: You typically don't need to call this unless you want to start fresh with property-to-bit mappings.
105
+
106
+ ### Configuration Options
107
+
108
+ The `StructureIdConfig` object supports the following options:
109
+
110
+ ```typescript
111
+ interface StructureIdConfig {
112
+ newIdOnCollision?: boolean;
81
113
  }
82
114
  ```
83
115
 
84
- If you need tasks to run in a certain order, add the yield statement before calling `genome.spawn()`.
116
+ #### `newIdOnCollision` (default: `false`)
85
117
 
86
- ```javascript
87
- * speak2() {
88
- yield genome.spawn('sayhi');
89
- yield genome.spawn('say-something-else');
90
- genome.spawn('say:goodbye');
91
- }
118
+ When set to `true`, each object with the same structure will receive a unique ID. This is useful when you need to distinguish between different object instances that share the same structure.
119
+
120
+ ```typescript
121
+ // Generate a unique ID for each object, even with the same structure
122
+ const config = { newIdOnCollision: true };
123
+
124
+ const obj1 = { name: "John", age: 30 };
125
+ const obj2 = { name: "Alice", age: 25 };
126
+
127
+ const id1 = generateStructureId(obj1, config);
128
+ const id2 = generateStructureId(obj2, config);
129
+
130
+ console.log(id1); // "L0:0-L1:5"
131
+ console.log(id2); // "L0:1-L1:5"
132
+ console.log(id1 === id2); // false (even though structure is identical)
92
133
  ```
93
134
 
94
- ### Read/write files
95
- Genome adds `read()` and `write()` methods to strings to make reading and writing files as easy as:
135
+ You can set this option globally or per call:
136
+
137
+ ```typescript
138
+ // Set globally
139
+ setStructureIdConfig({ newIdOnCollision: true });
96
140
 
97
- ```javascript
98
- return 'dist/html.index'.write(yield 'app/html.index'.read());
141
+ // Will use global config
142
+ const id1 = generateStructureId(obj1);
143
+ const id2 = generateStructureId(obj2);
144
+
145
+ // Override for a specific call
146
+ const id3 = generateStructureId(obj3, { newIdOnCollision: false });
99
147
  ```
100
148
 
101
- Genome also adds a `.contents` property as a read/write shorthand, so the same code can be written as:
149
+ ## When to use `genome`
150
+
151
+ The `genome` library shines in scenarios where you need to identify and match objects based on their structure rather than explicit keys or instance identity. Here are some ideal use cases:
152
+
153
+ ### State Management Without Explicit Keys
102
154
 
103
- ```javascript
104
- 'dist/html.index'.contents = yield 'app/html.index'.contents;
155
+ When persisting and rehydrating application state, you often need a way to match stored state with the corresponding objects in your application. Instead of manually maintaining string keys for every object, `genome` automatically generates consistent identifiers based on object structure.
156
+
157
+ ```ts
158
+ // Instead of this:
159
+ const componentKey = "user-preferences-panel";
160
+ storeState(componentKey, preferences);
161
+ // Later:
162
+ const savedState = getState(componentKey);
163
+
164
+ // You can do this:
165
+ const structureId = generateStructureId(preferences);
166
+ storeState(structureId, preferences);
167
+ // Later:
168
+ const savedState = getState(generateStructureId(preferences));
105
169
  ```
106
170
 
107
- Not that `read()`, `write()` and the `.contents` *getter* all return promises, but the `.contents` *setter*
108
- does not return anything. So if you need the file to be written *before* something else happens, use `write()`.
171
+ ### Memoization and Caching
109
172
 
110
- `.write()` accepts strings, promises, streams and arrays of file objects.
173
+ When implementing memoization patterns, you can use structure-based IDs to cache results based on input structure rather than identity:
111
174
 
112
- ### Processing files
113
- Genome does not require plugins like gulp or grunt. Simply install standard node packages and use their
114
- build-in api.
175
+ ```ts
176
+ const memoizedResults = new Map<string, any>();
115
177
 
116
- ```javascript
117
- * html() {
118
- // Process one file and output it
119
- var slm = require('slm');
178
+ function expensiveCalculation(data: SomeComplexObject) {
179
+ const structureId = generateStructureId(data);
180
+
181
+ if (memoizedResults.has(structureId)) {
182
+ return memoizedResults.get(structureId);
183
+ }
184
+
185
+ const result = /* complex calculation */;
186
+ memoizedResults.set(structureId, result);
187
+ return result;
188
+ }
189
+ ```
120
190
 
121
- return 'dist/index.html'.write(slm.render(yield 'app/index.slm'.contents));
122
- },
191
+ ### Normalizing Data for Storage
123
192
 
124
- * scripts() {
125
- // Output stream to file
126
- var browserify = require('browserify');
193
+ When storing objects in databases or state management systems, you can use structural IDs to create consistent references:
127
194
 
128
- return 'dist/scripts/app.js'.write(browserify('app/scripts/app.js', { transform: 'babelify' }).bundle());
129
- },
195
+ ```ts
196
+ function normalizeForStorage(entities: Record<string, unknown>[]) {
197
+ const normalizedEntities: Record<string, any> = {};
198
+
199
+ for (const entity of entities) {
200
+ const id = generateStructureId(entity);
201
+ normalizedEntities[id] = entity;
202
+ }
203
+
204
+ return normalizedEntities;
205
+ }
206
+ ```
130
207
 
131
- * styles() {
132
- // Output multiple files to directory with the String.prototype.use method
133
- var stylus = require('stylus');
208
+ ### Change detection
134
209
 
135
- return 'dist/styles/'.write(yield 'app/styles/*.styl'.use(stylus.render, '.css'));
210
+ Detect changes in object structure without relying on reference equality:
211
+
212
+ ```ts
213
+ function hasStructuralChanges(oldObj: object, newObj: object): boolean {
214
+ return generateStructureId(oldObj) !== generateStructureId(newObj);
136
215
  }
137
216
  ```
138
217
 
139
- ### Watch files
140
- Watch files for changes with String.prototype.onChange, passing in a function or a task name or array of task names.
218
+ ### Object Deduplication
219
+
220
+ Efficiently identify and remove duplicate objects with identical structures:
221
+
222
+ ```ts
223
+ function deduplicateByStructure<T>(objects: T[]): T[] {
224
+ const uniqueStructures = new Map<string, T>();
225
+
226
+ for (const obj of objects) {
227
+ const id = generateStructureId(obj as Record<string, unknown>);
228
+ if (!uniqueStructures.has(id)) {
229
+ uniqueStructures.set(id, obj);
230
+ }
231
+ }
232
+
233
+ return Array.from(uniqueStructures.values());
234
+ }
235
+ ```
141
236
 
142
- ```javascript
143
- * watch() {
144
- 'app/**/*.slm'.onChange('html');
145
- 'app/scripts/**/*.js'.onChange('scripts');
146
- 'app/styles/**/*.styl'.onChange('styles');
147
- 'dist/**/*'.onChange(browserSync.reload);
237
+ ### Unique Per-Instance IDs
238
+
239
+ When you need to uniquely identify each object instance, even if they share the same structure, you can use the `newIdOnCollision` option:
240
+
241
+ ```ts
242
+ function assignUniqueIds<T>(objects: T[]): Map<T, string> {
243
+ const idMap = new Map<T, string>();
244
+ const config = { newIdOnCollision: true };
245
+
246
+ for (const obj of objects) {
247
+ const id = generateStructureId(obj as Record<string, unknown>, config);
248
+ idMap.set(obj, id);
249
+ }
250
+
251
+ return idMap;
148
252
  }
149
253
  ```
254
+ ### Benefits Over Manual Key Management
255
+
256
+ - Automatic: No need to manually specify and maintain string keys
257
+ - Consistent: Same structure always generates the same ID
258
+ - Structural: Changes to object structure are automatically reflected in the ID
259
+ - Safe: Handles circular references without issues
260
+ - Deterministic: Property order doesn't affect the generated ID
261
+
262
+ ## How It Works
263
+
264
+ The library uses a bit-wise approach to generate structure IDs:
265
+
266
+ 1. Each JavaScript type gets a unique bit value (`number`, `string`, `object`, etc.)
267
+ 2. Each property name gets a unique bit value the first time it's encountered
268
+ 3. These bit values are consistently used for the same types and property names
269
+ 4. The object is traversed, and hash values are calculated for each level of nesting
270
+ 5. The final ID is formed by combining these level hashes
271
+
272
+ This approach ensures:
273
+ - Identical structures get identical IDs
274
+ - Different structures get different IDs
275
+ - The algorithm works correctly with circular references
276
+ - Property order doesn't affect the generated ID
277
+
278
+ ## Performance Considerations
279
+
280
+ - The library maintains a global mapping of property names to bit values, which grows as more unique property names are encountered
281
+ - For very large or complex objects, the bit values might become quite large (using BigInt internally)
282
+ - Circular references are handled efficiently without stack overflows
283
+
284
+ ## License
150
285
 
151
- ## Project Goals
152
- - Never require speciallized plugins like Gulp and Grunt
153
- - Keep code as simple and natural as possible
286
+ MIT
package/dist/hash.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @fileoverview Fast, non-cryptographic hash functions for structure ID generation
3
+ * @module @synpatico/genome/hash
4
+ */
5
+ /**
6
+ * Available hash function implementations
7
+ */
8
+ export declare const hashFunction: {
9
+ readonly MURMUR: typeof murmurHash3;
10
+ readonly XXHASH: typeof xxHash32;
11
+ };
12
+ /**
13
+ * Type representing available hash functions
14
+ */
15
+ export type HashFunction = (typeof hashFunction)[keyof typeof hashFunction];
16
+ /**
17
+ * Options for hash function selection
18
+ */
19
+ type HashFunctionProps = {
20
+ /** Built-in hash function to use */
21
+ type?: HashFunction;
22
+ /** Custom hash function implementation */
23
+ custom?: (str: string) => string;
24
+ };
25
+ /**
26
+ * Generates a hash from a string using the specified algorithm.
27
+ * Defaults to xxHash32 for optimal performance.
28
+ *
29
+ * @param str - The string to hash
30
+ * @param options - Optional hash function configuration
31
+ * @returns A hexadecimal hash string
32
+ *
33
+ * @example
34
+ * ```javascript
35
+ * // Using default xxHash32
36
+ * const h1 = hash("hello"); // "884863d4"
37
+ *
38
+ * // Using MurmurHash3
39
+ * const h2 = hash("hello", { type: hashFunction.MURMUR }); // "613cae7d"
40
+ *
41
+ * // Using custom function
42
+ * const h3 = hash("hello", { custom: (s) => s.length.toString() }); // "5"
43
+ * ```
44
+ */
45
+ export declare const hash: (str: string, options?: HashFunctionProps) => string;
46
+ /**
47
+ * MurmurHash3 implementation - fast non-cryptographic hash function.
48
+ * Provides good distribution and collision resistance for hash tables.
49
+ *
50
+ * @param str - The string to hash
51
+ * @returns A hexadecimal hash string
52
+ *
53
+ * @example
54
+ * ```javascript
55
+ * const hash = murmurHash3("hello world");
56
+ * console.log(hash); // "5e928f0f"
57
+ * ```
58
+ */
59
+ export declare function murmurHash3(str: string): string;
60
+ /**
61
+ *
62
+ * @param input - byte array or string
63
+ * @param seed - optional seed (32-bit unsigned);
64
+ */
65
+ export declare function xxHash32(input: Uint8Array | string, seed?: number): string;
66
+ export {};
67
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;;;CAGf,CAAA;AAEV;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAA;AAE3E;;GAEG;AACH,KAAK,iBAAiB,GAAG;IACxB,oCAAoC;IACpC,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CAChC,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,UAAU,iBAAiB,KAAG,MAU/D,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAyD/C;AAsBD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE,IAAI,SAAI,GAAG,MAAM,CA2KrE"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Order-preserving structure ID generator that avoids GLOBAL_KEY_MAP growth
3
+ * from array indices and lengths by hashing numeric values directly.
4
+ *
5
+ * This mirrors the current generateStructureId algorithm with one change:
6
+ * - For arrays, we compute index/length contributions via numeric hashing
7
+ * (fixed seeds) instead of storing strings like "[i]" and "length:n"
8
+ * in GLOBAL_KEY_MAP. Property names still use GLOBAL_KEY_MAP.
9
+ */
10
+ import { type StructureIdConfig } from "./index";
11
+ /**
12
+ * Generate a structure ID while avoiding GLOBAL_KEY_MAP growth from array indices/lengths.
13
+ * Property names still use GLOBAL_KEY_MAP like the current implementation.
14
+ */
15
+ export declare const generateStructureIdIndexHash: (obj: unknown, config?: StructureIdConfig) => string;
16
+ export declare function getStructureInfoIndexHash(obj: unknown, config?: StructureIdConfig): {
17
+ id: string;
18
+ levels: number;
19
+ collisionCount: number;
20
+ };
21
+ //# sourceMappingURL=id-indexhash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-indexhash.d.ts","sourceRoot":"","sources":["../src/id-indexhash.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,SAAS,CAAA;AA8DhB;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACvC,KAAK,OAAO,EACZ,SAAS,iBAAiB,KACzB,MAuHF,CAAA;AAED,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,OAAO,EACZ,MAAM,CAAC,EAAE,iBAAiB,GACzB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAUxD"}
@@ -0,0 +1,6 @@
1
+ export declare function generateStructureIdMultiset(obj: unknown): string;
2
+ export declare function getStructureInfoMultiset(obj: unknown): {
3
+ id: string;
4
+ levels: number;
5
+ };
6
+ //# sourceMappingURL=id-multiset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-multiset.d.ts","sourceRoot":"","sources":["../src/id-multiset.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAyEhE;AAGD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAGrF"}
@@ -0,0 +1,2 @@
1
+ export declare function generateStructureIdMultiset32(obj: unknown): string;
2
+ //# sourceMappingURL=id-multiset32.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-multiset32.d.ts","sourceRoot":"","sources":["../src/id-multiset32.ts"],"names":[],"mappings":"AAwCA,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAiElE"}