Skip to content

Commit 58c9dc7

Browse files
updates to jscad and manifold cache helpers, and workers + unit tests
1 parent 8eb1ae4 commit 58c9dc7

8 files changed

Lines changed: 2347 additions & 60 deletions

File tree

packages/dev/jscad-worker/lib/jscad-worker/cache-helper.test.ts

Lines changed: 700 additions & 0 deletions
Large diffs are not rendered by default.

packages/dev/jscad-worker/lib/jscad-worker/cache-helper.ts

Lines changed: 204 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,247 @@ export class CacheHelper {
44
hashesFromPreviousRun = {};
55
usedHashes = {};
66
argCache = {};
7-
8-
constructor() { }
7+
jscadObjectHashes = new Set<string | number>(); // Track which hashes contain JSCAD objects
98

109
cleanAllCache(): void {
11-
const usedHashKeys = Object.keys(this.usedHashes);
10+
// Clean all entries in argCache, not just usedHashes
11+
const allCacheKeys = Object.keys(this.argCache);
1212

13-
usedHashKeys.forEach(hash => {
13+
allCacheKeys.forEach(hash => {
1414
if (this.argCache[hash]) {
1515
try {
16-
this.argCache[hash].delete();
16+
const cachedItem = this.argCache[hash];
17+
// Only attempt to delete JSCAD objects
18+
if (this.isJSCADObject(cachedItem)) {
19+
// Handle arrays of JSCAD objects
20+
if (Array.isArray(cachedItem)) {
21+
cachedItem.forEach(obj => {
22+
try {
23+
if (obj.delete) {
24+
obj.delete();
25+
}
26+
} catch (error) {
27+
// Ignore errors for already deleted objects
28+
}
29+
});
30+
} else {
31+
if (cachedItem.delete) {
32+
cachedItem.delete();
33+
}
34+
}
35+
}
1736
}
18-
// eslint-disable-next-line no-empty
19-
catch {
37+
catch (error) {
38+
// Ignore errors when cleaning objects that may already be deleted
2039
}
2140
}
2241
});
2342

2443
this.argCache = {};
2544
this.usedHashes = {};
2645
this.hashesFromPreviousRun = {};
46+
this.jscadObjectHashes.clear();
47+
}
48+
49+
cleanCacheForHash(hash: string): void {
50+
if (this.argCache[hash]) {
51+
try {
52+
const cachedItem = this.argCache[hash];
53+
// Only attempt to delete JSCAD objects
54+
if (this.isJSCADObject(cachedItem)) {
55+
// Handle arrays of JSCAD objects
56+
if (Array.isArray(cachedItem)) {
57+
cachedItem.forEach(obj => {
58+
try {
59+
if (obj.delete) {
60+
obj.delete();
61+
}
62+
} catch (error) {
63+
// Ignore errors for already deleted objects
64+
}
65+
});
66+
} else {
67+
if (cachedItem.delete) {
68+
cachedItem.delete();
69+
}
70+
}
71+
}
72+
}
73+
catch (error) {
74+
// Ignore errors when cleaning objects that may already be deleted
75+
}
76+
}
77+
delete this.argCache[hash];
78+
delete this.usedHashes[hash];
79+
delete this.hashesFromPreviousRun[hash];
80+
this.jscadObjectHashes.delete(hash);
81+
}
82+
83+
cleanUpCache(): void {
84+
// Clean up cache entries that were used in previous run but not in current run
85+
// This helps manage memory by removing unused cached objects
86+
87+
const usedHashKeys = Object.keys(this.usedHashes);
88+
const hashesFromPreviousRunKeys = Object.keys(this.hashesFromPreviousRun);
89+
90+
// Find hashes that exist in previous run but not in current run
91+
// These are the ones we should clean up
92+
let hashesToDelete: string[] = [];
93+
if (hashesFromPreviousRunKeys.length > 0) {
94+
hashesToDelete = hashesFromPreviousRunKeys.filter(hash => !usedHashKeys.includes(hash));
95+
}
96+
97+
// Delete unused objects and clean them from cache
98+
if (hashesToDelete.length > 0) {
99+
hashesToDelete.forEach(hash => {
100+
if (this.argCache[hash]) {
101+
try {
102+
const obj = this.argCache[hash];
103+
// Only try to delete if it's a JSCAD object
104+
if (this.isJSCADObject(obj)) {
105+
// Handle arrays of JSCAD objects
106+
if (Array.isArray(obj)) {
107+
obj.forEach(o => {
108+
try {
109+
if (o.delete) {
110+
o.delete();
111+
}
112+
} catch {
113+
// Ignore errors for already deleted objects
114+
}
115+
});
116+
} else {
117+
if (obj.delete) {
118+
obj.delete();
119+
}
120+
}
121+
}
122+
} catch {
123+
// Ignore errors for already deleted or invalid objects
124+
}
125+
delete this.argCache[hash];
126+
}
127+
delete this.usedHashes[hash];
128+
this.jscadObjectHashes.delete(hash);
129+
});
130+
}
131+
132+
// Update hashesFromPreviousRun to be current usedHashes for next cleanup cycle
133+
this.hashesFromPreviousRun = { ...this.usedHashes };
27134
}
28135

136+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
137+
isJSCADObject(obj: any): boolean {
138+
// JSCAD objects typically have specific properties or methods
139+
// Check for common JSCAD object characteristics
140+
return obj !== undefined && obj !== null && (
141+
(!Array.isArray(obj) && typeof obj === "object" && obj.delete !== undefined) ||
142+
(Array.isArray(obj) && obj.length > 0 && typeof obj[0] === "object" && obj[0].delete !== undefined)
143+
);
144+
}
29145

30-
cacheOp(args, cacheMiss): any {
146+
/** Hashes input arguments and checks the cache for that hash.
147+
* It returns a copy of the cached object if it exists, but will
148+
* call the `cacheMiss()` callback otherwise. The result will be
149+
* added to the cache.
150+
*/
151+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
152+
cacheOp(args: any, cacheMiss: () => any): any {
31153
let toReturn = null;
32154
const curHash = this.computeHash(args);
33155
this.usedHashes[curHash] = curHash;
34156
this.hashesFromPreviousRun[curHash] = curHash;
35157
const check = this.checkCache(curHash);
36158
if (check) {
37-
// TODO I need to check if and why cloning is required.
38-
// toReturn = new this.occ.TopoDS_Shape(check);
39-
toReturn = check;
40-
toReturn.hash = check.hash;
159+
if (this.isJSCADObject(check)) {
160+
toReturn = check;
161+
toReturn.hash = check.hash;
162+
} else if (check.value) {
163+
toReturn = check.value;
164+
}
41165
} else {
42166
toReturn = cacheMiss();
43-
toReturn.hash = curHash;
44-
this.addToCache(curHash, toReturn);
167+
if (Array.isArray(toReturn) && this.isJSCADObject(toReturn)) {
168+
toReturn.forEach((r, index) => {
169+
const itemHash = this.computeHash({ ...args, index });
170+
r.hash = itemHash;
171+
this.addToCache(itemHash, r);
172+
// Track individual element hashes so they can be cleaned up
173+
this.usedHashes[itemHash] = itemHash;
174+
this.hashesFromPreviousRun[itemHash] = itemHash;
175+
});
176+
} else {
177+
if (this.isJSCADObject(toReturn)) {
178+
toReturn.hash = curHash;
179+
this.addToCache(curHash, toReturn);
180+
}
181+
else {
182+
this.addToCache(curHash, { value: toReturn });
183+
}
184+
}
45185
}
46186
return toReturn;
47187
}
48-
/** Returns the cached object if it exists, or null otherwise. */
49-
checkCache(hash): any {
50-
return this.argCache[hash] || null;
188+
/** Returns the cached object if it exists and is valid, or null otherwise. */
189+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
190+
checkCache(hash: string | number): any {
191+
const cachedObject = this.argCache[hash];
192+
if (!cachedObject) {
193+
return null;
194+
}
195+
196+
// For wrapped values (non-JSCAD objects stored as { value: ... })
197+
if (cachedObject.value !== undefined && !this.isJSCADObject(cachedObject)) {
198+
return cachedObject;
199+
}
200+
201+
// If this hash was tracked as a JSCAD object, verify it's still valid
202+
if (this.jscadObjectHashes.has(hash)) {
203+
const isStillValid = this.isJSCADObject(cachedObject);
204+
if (!isStillValid) {
205+
// Object was a JSCAD object but is no longer valid
206+
delete this.argCache[hash];
207+
this.jscadObjectHashes.delete(hash);
208+
return null;
209+
}
210+
}
211+
212+
return cachedObject;
51213
}
52-
/** Adds this `shape` to the cache, indexable by `hash`. */
53-
addToCache(hash, shape): any {
54-
const cacheShape = shape;
55-
cacheShape.hash = hash;
56-
this.argCache[hash] = cacheShape;
214+
/** Adds this `object` to the cache, indexable by `hash`. */
215+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
216+
addToCache(hash: string | number, object: any): string | number {
217+
const cacheObject = object;
218+
// Only set hash property on objects, not primitives
219+
if (cacheObject !== null && typeof cacheObject === "object") {
220+
cacheObject.hash = hash;
221+
}
222+
this.argCache[hash] = cacheObject;
223+
224+
// Track if this is a JSCAD object
225+
if (this.isJSCADObject(cacheObject)) {
226+
this.jscadObjectHashes.add(hash);
227+
}
228+
57229
return hash;
58230
}
59231

60232
/** This function computes a 32-bit integer hash given a set of `arguments`.
61-
* If `raw` is true, the raw set of sanitized arguments will be returned instead.
233+
* If `raw` is true, the raw set of sanitized arguments will be returned instead.
62234
*/
63-
computeHash(args, raw?: any): any {
235+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
236+
computeHash(args: any, raw?: boolean): number | string {
64237
let argsString = JSON.stringify(args);
65-
argsString = argsString.replace(/(\"ptr\"\:(-?[0-9]*?)\,)/g, "");
66-
argsString = argsString.replace(/(\"ptr\"\:(-?[0-9]*))/g, "");
238+
argsString = argsString.replace(/("ptr":(-?[0-9]*?),)/g, "");
239+
argsString = argsString.replace(/("ptr":(-?[0-9]*))/g, "");
67240
if (argsString.includes("ptr")) { console.error("YOU DONE MESSED UP YOUR REGEX."); }
68241
const hashString = Math.random.toString() + argsString;
69242
if (raw) { return hashString; }
70243
return this.stringToHash(hashString);
71244
}
72245

73246
/** This function converts a string to a 32bit integer. */
74-
stringToHash(str: string): any {
247+
stringToHash(str: string): number {
75248
let hash = 0;
76249
if (str.length === 0) { return hash; }
77250
for (let i = 0; i < str.length; i++) {
@@ -85,8 +258,11 @@ export class CacheHelper {
85258
}
86259

87260
/** This function returns a version of the `inputArray` without the `objectToRemove`. */
88-
remove(inputArray, objectToRemove): any {
261+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
262+
remove(inputArray: any[], objectToRemove: any): any[] {
89263
return inputArray.filter((el) => {
264+
// Keep elements where hash is different OR ptr is different
265+
// (remove only when BOTH hash AND ptr match)
90266
return el.hash !== objectToRemove.hash ||
91267
el.ptr !== objectToRemove.ptr;
92268
});

0 commit comments

Comments
 (0)