Skip to content

Commit

Permalink
Add SignaledMap
Browse files Browse the repository at this point in the history
  • Loading branch information
Exelord committed May 16, 2022
1 parent 092f6f0 commit c7b0e02
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./primitives/object";
export * from "./primitives/array";
export * from "./primitives/map";
107 changes: 107 additions & 0 deletions src/primitives/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { createCache } from "../utls/cache";

export const OBJECT_KEYS = Symbol("objectKeys");

export class SignaledMap<K = unknown, V = unknown> implements Map<K, V> {
private signals = createCache();

private cache: Map<K, V>;

constructor();
constructor(entries: readonly (readonly [K, V])[] | null);
constructor(iterable: Iterable<readonly [K, V]>);
constructor(
existing?:
| readonly (readonly [K, V])[]
| Iterable<readonly [K, V]>
| null
| undefined
) {
// TypeScript doesn't correctly resolve the overloads for calling the `Map`
// constructor for the no-value constructor. This resolves that.
this.cache = existing ? new Map(existing) : new Map();
}

get size(): number {
this.signals.track(OBJECT_KEYS);
return this.cache.size;
}

get [Symbol.toStringTag](): string {
return this.cache[Symbol.toStringTag];
}

[Symbol.iterator](): IterableIterator<[K, V]> {
this.signals.track(OBJECT_KEYS);
return this.cache[Symbol.iterator]();
}

get(key: K): V | undefined {
this.signals.track(key);
return this.cache.get(key);
}

has(key: K): boolean {
this.signals.track(key);
return this.cache.has(key);
}

entries(): IterableIterator<[K, V]> {
this.signals.track(OBJECT_KEYS);
return this.cache.entries();
}

keys(): IterableIterator<K> {
this.signals.track(OBJECT_KEYS);
return this.cache.keys();
}

values(): IterableIterator<V> {
this.signals.track(OBJECT_KEYS);
return this.cache.values();
}

forEach(fn: (value: V, key: K, map: Map<K, V>) => void): void {
this.signals.track(OBJECT_KEYS);
this.cache.forEach(fn);
}

set(key: K, value: V): this {
const hasKey = this.cache.has(key);
const prevValue = this.cache.get(key);

this.cache.set(key, value);

if (!hasKey || value !== prevValue) this.signals.dirty(OBJECT_KEYS);
if (value !== prevValue) this.signals.dirty(key);

return this;
}

delete(key: K): boolean {
if (!this.cache.has(key)) return false;

const currentValue = this.cache.get(key);
const result = this.cache.delete(key);

this.signals.dirty(OBJECT_KEYS);

if (currentValue !== undefined) this.signals.dirty(key);

return result;
}

clear(): void {
this.signals.dirtyAll();
this.signals.dirty(OBJECT_KEYS);

this.cache.clear();
}
}

// So instanceof works
Object.setPrototypeOf(SignaledMap.prototype, Map.prototype);

export function createMap<K = unknown, V = unknown>(map: Map<K, V>): Map<K, V> {
return new SignaledMap<K, V>(map);
}
104 changes: 104 additions & 0 deletions tests/src/primitives/map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { createRenderEffect, createRoot } from "solid-js";
import { describe, it, expect, vi } from "vitest";
import { createMap, SignaledMap } from "../../../src/primitives/map";

describe("SignaledMap", () => {
it("works with SignaledMap syntax", () => {
const spy = vi.fn();

createRoot(() => {
const map = new Map([["track", "me"]]);
const signaledMap = new SignaledMap(map);

createRenderEffect(() => {
spy(signaledMap.get("track"));
});

expect(spy).toBeCalledTimes(1);

signaledMap.set("track", "yeah");
});

expect(spy).toBeCalledTimes(2);
});

describe("set", () => {
it("uses signal to track properties", () => {
const spy = vi.fn();

createRoot(() => {
const map = new Map([["track", "me"]]);
const signaledMap = createMap(map);

createRenderEffect(() => {
spy(signaledMap.get("track"));
});

expect(spy).toBeCalledTimes(1);

signaledMap.set("track", "yeah");
});

expect(spy).toBeCalledTimes(2);
});

it("tracks keys", () => {
const spy = vi.fn();

createRoot(() => {
const map = new Map();
const signaledMap = createMap(map);

createRenderEffect(() => {
spy(signaledMap.keys());
});

expect(spy).toBeCalledTimes(1);

signaledMap.set("track", "me");
});

expect(spy).toBeCalledTimes(2);
});
});

describe("delete", () => {
it("uses signal to track properties", () => {
const spy = vi.fn();

createRoot(() => {
const map = new Map([["track", "me"]]);
const signaledMap = createMap(map);

createRenderEffect(() => {
spy(signaledMap.get("track"));
});

expect(spy).toBeCalledTimes(1);

signaledMap.delete("track");
});

expect(spy).toBeCalledTimes(2);
});

it("tracks keys", () => {
const spy = vi.fn();

createRoot(() => {
const map = new Map([["track", "me"]]);
const signaledMap = createMap(map);

createRenderEffect(() => {
spy(signaledMap.keys());
});

expect(spy).toBeCalledTimes(1);

signaledMap.delete("track");
});

expect(spy).toBeCalledTimes(2);
});
});
});

0 comments on commit c7b0e02

Please sign in to comment.