diff --git a/src/Emulator.spec.ts b/src/Emulator.spec.ts index d300235..dd9c4b6 100644 --- a/src/Emulator.spec.ts +++ b/src/Emulator.spec.ts @@ -92,6 +92,42 @@ describe("Emulator", () => { }); }); + describe("parent", () => { + it("Access a proxy parent", () => { + const parent = $.proxy(); + const child = parent.child; + expect($.parent(child)).toBe(parent); + expect($.parent(parent)).toBe(undefined); + }); + }); + + describe("children", () => { + it("Can access all the direct children of a proxy", () => { + const parent = $.proxy(); + parent.girl = 10; + parent.boy = 5; + parent.alien = NaN; + parent.alien.notDirectChild = true; + + expect($.children(parent).length).toBe(3); + }); + + it("Can use [Symbol.iterator] to access all children as well", () => { + const parent = $.proxy(); + + parent.girl = 1; + parent.boy = 2; + + const children = [...parent]; + + for (const proxy of parent) { + expect($.parent(proxy)).toBe(parent); + } + + expect(children.length).toBe(2); + }); + }); + describe("revoke", () => { it("Turns a proxy unusable", () => { const proxy = $.proxy(); @@ -106,7 +142,7 @@ describe("Emulator", () => { }); describe("destroy", () => { - it("Destroys the $ and turns it unusable", () => { + it("Destroys the emulator and turns it unusable", () => { const $ = new Emulator(); $.destroy(); expect($.proxy).toThrow(); @@ -115,7 +151,7 @@ describe("Emulator", () => { }); describe("count", () => { - it("Returns the number of proxies in the $", () => { + it("Returns the number of proxies in the emulator", () => { const current = $.count(); $.proxy(); expect($.count()).toBe(current + 1); diff --git a/src/Emulator.ts b/src/Emulator.ts index f4024f1..d72313a 100755 --- a/src/Emulator.ts +++ b/src/Emulator.ts @@ -3,9 +3,11 @@ import { findProxy, map, createProxy } from "./utils"; import { EventEmitter } from "events"; export default class Emulator extends EventEmitter implements Exotic.Emulator { + static iterator = Symbol(90); + get refs(): Exotic.namespace[] { const { bindings }: Exotic.emulator.data = map.emulators.get(this); - return Object.keys(bindings); + return Reflect.ownKeys(bindings); } constructor(options: Exotic.emulator.options = {}) { @@ -21,19 +23,18 @@ export default class Emulator extends EventEmitter implements Exotic.Emulator { map.emulators.set(this, data); } - bind(selector: Exotic.namespace): Exotic.Proxy { - const data: Exotic.emulator.data = map.emulators.get(this); - const { bindings } = data; - const group = bindings[selector]; + bind(namespace: Exotic.namespace): Exotic.Proxy { + const { bindings }: Exotic.emulator.data = map.emulators.get(this); + const group = bindings[namespace]; // return the first proxy in the existing group - if (group) return group.first; + if (group) return group.root; // create the first proxy for a new group - return createProxy(this, undefined, selector); + return createProxy(this, undefined, namespace); } - proxy(value?: unknown): Exotic.Proxy { + proxy(value?: any): Exotic.Proxy { return createProxy(this, value); } @@ -44,13 +45,21 @@ export default class Emulator extends EventEmitter implements Exotic.Emulator { return target; } - parent(value?: any): undefined | Exotic.Proxy { + parent(value?: Exotic.traceable): undefined | Exotic.Proxy { const proxy = findProxy(value); if (!proxy) return; const { origin } = map.proxies.get(proxy); return origin && origin.proxy; } + children(value?: Exotic.traceable): Exotic.Proxy[] { + const results = []; + const proxy = findProxy(value); + if (!proxy) return results; + const { sandbox } = map.proxies.get(proxy); + return Reflect.ownKeys(sandbox).map((key) => sandbox[key]); + } + count(): number { const { activeItems }: Exotic.emulator.data = map.emulators.get(this); return activeItems; diff --git a/src/types/Exotic.ts b/src/types/Exotic.ts index e57caf9..8740207 100644 --- a/src/types/Exotic.ts +++ b/src/types/Exotic.ts @@ -7,26 +7,27 @@ declare namespace Exotic { interface Emulator extends EventEmitter { refs: namespace[]; - bind(selelctor: namespace): Proxy; + bind(x: namespace): Proxy; proxy(value?: any): Proxy; target(value?: any): any; } - // eslint-disable-next-line @typescript-eslint/ban-types - interface FunctionLike extends Function { + interface FunctionLike { (...args: any[]): void; + [x: namespace]: any; + [Symbol.iterator](): Iterator; } interface Proxy extends FunctionLike { [x: namespace]: any; + [Symbol.iterator](): Iterator; } // eslint-disable-next-line @typescript-eslint/no-namespace namespace proxy { interface group { length: number; - first: Exotic.Proxy; - last: Exotic.Proxy; + root: Exotic.Proxy; } interface origin { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 504e14e..5f7b76a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1 +1 @@ -export const globalNamespace: symbol = Symbol("global"); +export const globalNamespace: symbol = Symbol("GLOBAL"); diff --git a/src/utils/createProxy.ts b/src/utils/createProxy.ts index 5dd7770..48f098a 100644 --- a/src/utils/createProxy.ts +++ b/src/utils/createProxy.ts @@ -5,6 +5,18 @@ import isTraceable from "./isTraceable"; import { globalNamespace } from "./constants"; import traps from "./traps"; +const dummyPrototype = Object.assign(Object.create(null), { + [Symbol.iterator]() { + const proxy = findProxy(this); + const { sandbox } = map.proxies.get(proxy); + return function* () { + for (const key of Reflect.ownKeys(sandbox)) { + yield sandbox[key]; + } + }; + }, +}); + const createProxy = ( scope: Exotic.Emulator, target: any, @@ -19,9 +31,13 @@ const createProxy = ( const { bindings } = data; const id = ++data.itemCount; - const dummy: Exotic.FunctionLike = function () {}; + const dummy = function () {} as Exotic.FunctionLike; + const traceable = isTraceable(target); - const { proxy, revoke } = Proxy.revocable(dummy, traps); + const { proxy, revoke } = Proxy.revocable( + Object.setPrototypeOf(dummy, dummyPrototype), + traps, + ); let group: Exotic.proxy.group = bindings[namespace]; @@ -29,16 +45,13 @@ const createProxy = ( // create the new group group = { length: 0, - first: proxy, - last: proxy, + root: proxy, }; bindings[namespace] = group; scope.emit("bind", namespace); } - group.last = proxy; - // set the proxy information const proxyData: Exotic.proxy.data = { id, diff --git a/src/utils/traps/get.ts b/src/utils/traps/get.ts index ccef392..0e98ae7 100644 --- a/src/utils/traps/get.ts +++ b/src/utils/traps/get.ts @@ -3,7 +3,7 @@ import createProxy from "../createProxy"; import findProxy from "../findProxy"; import map from "../map"; -const get = (dummy: Exotic.FunctionLike, key: string): unknown => { +const get = (dummy: Exotic.FunctionLike, key: Exotic.namespace): unknown => { const proxy = findProxy(dummy); const { scope, namespace, target, sandbox } = map.proxies.get(proxy); @@ -13,6 +13,10 @@ const get = (dummy: Exotic.FunctionLike, key: string): unknown => { proxy, }; + if (key === Symbol.iterator) { + return dummy[key](); + } + let value: any = sandbox[key]; // get new target from sandbox