Utilizing WeakMap to effectively store Metadata
Microsoft Co-pilot. What a lovely AI tool

Utilizing WeakMap to effectively store Metadata

Metadata is simply data that holds information about some other piece of data. Seems repetitive but if you were to think of networking and mainly HTTP. If you perform an HTTP request, data is sent or received (HTTP response) together with some HTTP headers. The HTTP headers are considered metadata and specifically descriptive metadata. So as much as you as the recipient of the request might want to read the data sent, you will need the HTTP headers to be able to understand the data you have been sent.

This caught my interest because most of the time, when I am creating a web application. I would want my, let say DOM elements, to hold some information (metadata). I don't want to mutate them by adding attributes so the most obvious way I thought of, is to use a cache object. An example of a cache object would be

const cache = {};

cache["user1"] = {
	object: new User("admin"),
	meta: {
		syncedOn: new Date("2009-11-27T16:45:30"),
	}
};

cache["user2"] = {
	object: new User("student"),
	meta: {
		syncedOn: new Date("2009-11-27T09:45:30"),
	}
};        

Some problems with the example above are:

  • Iteration can be a pain

for (const key in cache) {
	// check all users whose last sync were 20 minutes ago
}

// OR
Object.keys(cache).map(someComputationBasedOnMetadata);        

  • If user2 is no longer used it won't be garbage collected since there is still a lingering reference to it. This causes memory leaks.

Solving iteration in JavaScript

In JavaScript, we can make an object to behave like an Array by adding Symbol.iterator generator function which yields the values required

const cache = {
	*[Symbol.iterator]() {
	  for (const k of Object.keys(this)) {
             // you could do more computation
	      yield this[k];
	  }
	}
};

cache["john_doe"] = {
	object: new User("admin"),
	meta: {
		syncedOn: new Date("2024-05-07T00:45:30")
	}
};

cache["jane_doe"] = {
	object: new User("root"),
	meta: {
		syncedOn: new Date("2024-05-07T03:15:30")
	}
};

for (const key of Object.keys(cache)) {
        const metadata = cache[key];
	// some computation done here!
}        

Managing garbage collection

Here is where a tradeoff comes in. We are going to introduce a JavaScript data structure WeakMap.

A WeakMap is a collection of key/value pairs where the key is an object or non-registered symbol and the values are normal (or arbitrary) JavaScript types e.g. Boolean or Number or JavaScript objects ({ }).

Although it has a similar API to a Map it has one main advantage which is the keys provided are never restricted from being garbage collected. This means:

const wm = new WeakMap();
const signupForm = document.querySelector("form[data-purpose='signup']");

wm.set(
	signupForm, 
	{ 
		lastFilledOn: new Date("2024-05-09T13:05:00") 
	}
);        
In an event where signupForm is no longer referenced, it will be garbage collected. This means later on wm.has(signupForm) is returns false.

Now the tradeoff is we cannot iterate over a WeakMap. The reason is because unlike Maps, WeakMaps do not keep an array or list of keys since its keys are non-deterministic or garbage collectable.

WeakMap use case

Going back to our cache example, we can expand it by assuming a real-world use case.

Let's say we have a table of company employees. We are tasked to improve the table's functionality by enabling in-memory editing. We can cache the state of our form per row using WeakMap following the pattern.

type EditFormCache = WeakMap<HTMLFormElement, "editing" | "stalled">;        

This way when we toggle back and forth between a normal table row and a form row, we can only get forms which have not yet been garbage collected stored in our cache.

const wm = new WeakMap();
const form = document.querySelectorAll("form.table-row-edit-form");

for (const f of form) {
	wm.set(f, "stalled");
}        

Then when the user is typing in an input you can capture the parent (HTMLFormElement) and update the state

const form = document.querySelector("input.my-input").parentElement;

if (wm.has(form)) {
	const state = wm.get(form);
	switch (state) {
		case "editing": {
			wm.set(form, "stalled");
			break;
		}

		case "stalled": {
			wm.set(form, "editing");
			break;
		} 
	}
}        
Then maybe after updating the state we can use WebSocket API to save the data entered if state is "stalled" or even inform other clients of the current state of the table row i.e. "editing".

Closing remarks

The example above may resemble a real-world scenario but the truth of the matter is there is more work to it than meets the eye. That being said, you will notice that,

  • If your table is paginated then your cache will always stay fresh since it will remove the garbage collected keys.
  • The operations used by WeakMap are fast since it doesn't encourage loops hence most operations are of O(1).
  • It is extensible allowing you to create robust collections like ClearableWeakMap and you can even use them in OOP factories when managing produced objects.

All in all, I found it to be an interesting JavaScript collection. I am planning to see how I can create an Inversion of Control (IoC) library with it. I would love to hear the cool ways you have or would want to use WeakMap.

Thank you and Happy coding!

Brian Okello

Software Developer, Student.

6 个月

Nice job!!

回复

要查看或添加评论,请登录

社区洞察

其他会员也浏览了