From cf6509c12c49194dab0acaeb24c803df7b6df055 Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Mon, 7 Mar 2022 00:44:31 +0200 Subject: [PATCH 01/10] inital idea for plugable --- package.json | 1 + packages/plugable/LICENSE | 21 ++++ packages/plugable/README.md | 1 + packages/plugable/package.json | 23 ++++ packages/plugable/src/index.ts | 1 + packages/plugable/src/plugable-react.tsx | 16 +++ packages/plugable/src/plugable.ts | 79 ++++++++++++ packages/plugable/src/test/test.unit.ts | 89 +++++++++++++ packages/plugable/src/tsconfig.json | 8 ++ packages/plugable/src/types.ts | 18 +++ packages/wait-for-call/src/test/test.unit.ts | 2 + tsconfig.json | 3 +- yarn.lock | 126 +++++++++++-------- 13 files changed, 336 insertions(+), 52 deletions(-) create mode 100644 packages/plugable/LICENSE create mode 100644 packages/plugable/README.md create mode 100644 packages/plugable/package.json create mode 100644 packages/plugable/src/index.ts create mode 100644 packages/plugable/src/plugable-react.tsx create mode 100644 packages/plugable/src/plugable.ts create mode 100644 packages/plugable/src/test/test.unit.ts create mode 100644 packages/plugable/src/tsconfig.json create mode 100644 packages/plugable/src/types.ts diff --git a/package.json b/package.json index 85c565d6..37a5f6dc 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/chai-as-promised": "^7.1.5", "@types/mocha": "^9.1.0", "@types/node": "14", + "@types/react": "^17.0.39", "@types/sinon": "^10.0.11", "@typescript-eslint/eslint-plugin": "^5.12.1", "@typescript-eslint/parser": "^5.12.1", diff --git a/packages/plugable/LICENSE b/packages/plugable/LICENSE new file mode 100644 index 00000000..e2900d23 --- /dev/null +++ b/packages/plugable/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Wix.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/plugable/README.md b/packages/plugable/README.md new file mode 100644 index 00000000..416bccbd --- /dev/null +++ b/packages/plugable/README.md @@ -0,0 +1 @@ +# Plugable diff --git a/packages/plugable/package.json b/packages/plugable/package.json new file mode 100644 index 00000000..8d7502ee --- /dev/null +++ b/packages/plugable/package.json @@ -0,0 +1,23 @@ +{ + "name": "@wixc3/plugable-map", + "version": "1.0.1", + "main": "dist/index.js", + "scripts": { + "test": "mocha \"dist/test/**/*.unit.js\"" + }, + "peerDependencies": { + "react": "^17.0.2" + }, + "files": [ + "dist", + "src", + "!dist/test", + "!dist/tsconfig.tsbuildinfo" + ], + "license": "MIT", + "author": "Wix.com", + "repository": "git@github.com:wixplosives/core3-utils.git", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/plugable/src/index.ts b/packages/plugable/src/index.ts new file mode 100644 index 00000000..ae31b780 --- /dev/null +++ b/packages/plugable/src/index.ts @@ -0,0 +1 @@ +export * from './plugable'; diff --git a/packages/plugable/src/plugable-react.tsx b/packages/plugable/src/plugable-react.tsx new file mode 100644 index 00000000..481eb100 --- /dev/null +++ b/packages/plugable/src/plugable-react.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import type { Plugable, Key } from './types'; +import { createPlugable, on, get } from './plugable'; + +export const PlugableContext = createContext(createPlugable()); + +export function usePlugable() { + return useContext(PlugableContext); +} + +export function usePlugableValue(key: Key): T | undefined { + const [_, setState] = useState({}); + const map = usePlugable(); + useEffect(() => on(map, key, () => setState({})), [key]); + return get(map, key); +} diff --git a/packages/plugable/src/plugable.ts b/packages/plugable/src/plugable.ts new file mode 100644 index 00000000..2e4a775b --- /dev/null +++ b/packages/plugable/src/plugable.ts @@ -0,0 +1,79 @@ +import { type Plugable, type Key, internals, PlugableInternals, Val } from './types'; + +export function createPlugable(): Plugable { + const rec = Object.create(null) as Plugable; + rec[internals] = { + parent: undefined, + listeners: new Map(), + handlerToRec: new WeakMap(), + }; + return rec; +} + +export function inheritPlugable(rec: Plugable) { + const inherited = Object.create(rec) as Plugable; + inherited[internals] = Object.create(rec[internals]) as PlugableInternals; + inherited[internals].parent = rec; + return inherited; +} + +export function createKey(debugName?: string): Key { + return Symbol(debugName) as Key; +} + +export function getThrow(rec: Plugable, key: Key): Value { + const value = get(rec, key); + if (value === undefined || value === null) { + throw new Error(`missing value for key ${String(key)}`); + } + return value; +} + +export function get(rec: Plugable, key: Key): Value | undefined { + return (rec as Record, Value>)[key]; +} + +export function set( + rec: Plugable, + key: Key, + value: Value, + isEqual = (prevues: Value | undefined, value: Value) => prevues === value +) { + if (!isEqual(get(rec, key), value)) { + (rec as Record, Value>)[key] = value; + dispatch(rec, key, value); + } +} + +function hasOwn(obj: unknown, key: string | symbol) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +export function dispatch(rec: Plugable, key: Key, value: T) { + const { listeners, handlerToRec } = rec[internals]; + listeners.get(key)?.forEach((listener) => shouldDispatch(rec, handlerToRec.get(listener), key) && listener(value)); +} + +function shouldDispatch(dispatcherRec: Plugable, handlerRec: Plugable | undefined, key: string | symbol): boolean { + if (dispatcherRec === handlerRec) { + return true; + } else if (handlerRec && !hasOwn(handlerRec, key)) { + return shouldDispatch(dispatcherRec, handlerRec[internals].parent, key); + } else { + return false; + } +} + +export function on(rec: Plugable, key: K, listener: (value: Val) => void) { + const { listeners, handlerToRec } = rec[internals]; + let handlers = listeners.get(key); + if (!handlers) { + handlers = new Set(); + listeners.set(key, handlers); + } + handlers.add(listener); + handlerToRec.set(listener, rec); + return () => { + handlers?.delete(listener); + }; +} diff --git a/packages/plugable/src/test/test.unit.ts b/packages/plugable/src/test/test.unit.ts new file mode 100644 index 00000000..20220652 --- /dev/null +++ b/packages/plugable/src/test/test.unit.ts @@ -0,0 +1,89 @@ +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { createPlugable, createKey, inheritPlugable, set, get, on } from '../plugable'; +chai.use(chaiAsPromised); + +describe('Plugable', () => { + it('set and get', () => { + const rec = createPlugable(); + const key = createKey(); + + set(rec, key, 'hello'); + expect(get(rec, key)).to.equal('hello'); + }); + + it('emit on set', () => { + const rec = createPlugable(); + const key = createKey(); + const res = new Array(); + + on(rec, key, res.push.bind(res)); + set(rec, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); + + it('emit on child when set', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + on(child, key, res.push.bind(res)); + set(child, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); + + it('emit on child when set on parent (no child override)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + on(child, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); + + it('no emit on child when set on parent (when there is override)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + set(child, key, 'world'); + on(child, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res).to.be.empty; + }); + + it('no emit on grandChild when set on parent (when there is override on child)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const grandChild = inheritPlugable(child); + const key = createKey(); + const res = new Array(); + + set(child, key, 'world'); + on(grandChild, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res).to.be.empty; + }); + + it('emit on grandChild when set on parent (when there is no override on child)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const grandChild = inheritPlugable(child); + const key = createKey(); + const res = new Array(); + + on(grandChild, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); +}); diff --git a/packages/plugable/src/tsconfig.json b/packages/plugable/src/tsconfig.json new file mode 100644 index 00000000..f0b1afca --- /dev/null +++ b/packages/plugable/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../dist", + "types": ["mocha"] + }, + "references": [] +} diff --git a/packages/plugable/src/types.ts b/packages/plugable/src/types.ts new file mode 100644 index 00000000..61609d3f --- /dev/null +++ b/packages/plugable/src/types.ts @@ -0,0 +1,18 @@ +const secret = Symbol(); +export const internals = Symbol('internals'); + +export type Key = (string | symbol) & { [secret]: V | undefined }; +export type Val = T extends Key ? V : never; + +export type PlugableInternals = { + parent: Plugable | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly listeners: Map void>>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly handlerToRec: WeakMap<(value: any) => void, Plugable>; +}; + +export type Plugable = { + [key: string]: never; + [internals]: PlugableInternals; +}; diff --git a/packages/wait-for-call/src/test/test.unit.ts b/packages/wait-for-call/src/test/test.unit.ts index c9ff1493..9c6c97cb 100644 --- a/packages/wait-for-call/src/test/test.unit.ts +++ b/packages/wait-for-call/src/test/test.unit.ts @@ -3,6 +3,8 @@ import chaiAsPromised from 'chai-as-promised'; import { createWaitForCall } from '../wait-for-call'; chai.use(chaiAsPromised); describe('Wait For Call', () => { + // TODO: check eslint flip + // eslint-disable-next-line @typescript-eslint/no-misused-promises it('mocks a call', async () => { const myFunction = (arg: string) => arg; const { waitForCall } = createWaitForCall('name', myFunction); diff --git a/tsconfig.json b/tsconfig.json index 95256e82..cf272342 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ { "path": "./packages/wait-for-call/src" }, { "path": "./packages/create-disposables/src" }, { "path": "./packages/mostly-equal/src" }, - { "path": "./packages/mark-text/src" } + { "path": "./packages/mark-text/src" }, + { "path": "./packages/plugable/src" } ] } diff --git a/yarn.lock b/yarn.lock index fecdfc63..1be256da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,6 +1005,25 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/react@^17.0.39": + version "17.0.39" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.39.tgz#d0f4cde092502a6db00a1cded6e6bf2abb7633ce" + integrity sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/sinon@^10.0.11": version "10.0.11" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.11.tgz#8245827b05d3fc57a6601bd35aee1f7ad330fc42" @@ -1018,13 +1037,13 @@ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@typescript-eslint/eslint-plugin@^5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.1.tgz#b2cd3e288f250ce8332d5035a2ff65aba3374ac4" - integrity sha512-M499lqa8rnNK7mUv74lSFFttuUsubIRdAbHcVaP93oFcKkEmHmLqy2n7jM9C8DVmFMYK61ExrZU6dLYhQZmUpw== + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.13.0.tgz#2809052b85911ced9c54a60dac10e515e9114497" + integrity sha512-vLktb2Uec81fxm/cfz2Hd6QaWOs8qdmVAZXLdOBX6JFJDhf6oDZpMzZ4/LZ6SFM/5DgDcxIMIvy3F+O9yZBuiQ== dependencies: - "@typescript-eslint/scope-manager" "5.12.1" - "@typescript-eslint/type-utils" "5.12.1" - "@typescript-eslint/utils" "5.12.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/type-utils" "5.13.0" + "@typescript-eslint/utils" "5.13.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -1033,68 +1052,68 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.12.1.tgz#b090289b553b8aa0899740d799d0f96e6f49771b" - integrity sha512-6LuVUbe7oSdHxUWoX/m40Ni8gsZMKCi31rlawBHt7VtW15iHzjbpj2WLiToG2758KjtCCiLRKZqfrOdl3cNKuw== + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.13.0.tgz#0394ed8f2f849273c0bf4b811994d177112ced5c" + integrity sha512-GdrU4GvBE29tm2RqWOM0P5QfCtgCyN4hXICj/X9ibKED16136l9ZpoJvCL5pSKtmJzA+NRDzQ312wWMejCVVfg== dependencies: - "@typescript-eslint/scope-manager" "5.12.1" - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/typescript-estree" "5.12.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/typescript-estree" "5.13.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.12.1.tgz#58734fd45d2d1dec49641aacc075fba5f0968817" - integrity sha512-J0Wrh5xS6XNkd4TkOosxdpObzlYfXjAFIm9QxYLCPOcHVv1FyyFCPom66uIh8uBr0sZCrtS+n19tzufhwab8ZQ== +"@typescript-eslint/scope-manager@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.13.0.tgz#cf6aff61ca497cb19f0397eea8444a58f46156b6" + integrity sha512-T4N8UvKYDSfVYdmJq7g2IPJYCRzwtp74KyDZytkR4OL3NRupvswvmJQJ4CX5tDSurW2cvCc1Ia1qM7d0jpa7IA== dependencies: - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/visitor-keys" "5.12.1" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" -"@typescript-eslint/type-utils@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.12.1.tgz#8d58c6a0bb176b5e9a91581cda1a7f91a114d3f0" - integrity sha512-Gh8feEhsNLeCz6aYqynh61Vsdy+tiNNkQtc+bN3IvQvRqHkXGUhYkUi+ePKzP0Mb42se7FDb+y2SypTbpbR/Sg== +"@typescript-eslint/type-utils@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.13.0.tgz#b0efd45c85b7bab1125c97b752cab3a86c7b615d" + integrity sha512-/nz7qFizaBM1SuqAKb7GLkcNn2buRdDgZraXlkhz+vUGiN1NZ9LzkA595tHHeduAiS2MsHqMNhE2zNzGdw43Yg== dependencies: - "@typescript-eslint/utils" "5.12.1" + "@typescript-eslint/utils" "5.13.0" debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.12.1.tgz#46a36a28ff4d946821b58fe5a73c81dc2e12aa89" - integrity sha512-hfcbq4qVOHV1YRdhkDldhV9NpmmAu2vp6wuFODL71Y0Ixak+FLeEU4rnPxgmZMnGreGEghlEucs9UZn5KOfHJA== +"@typescript-eslint/types@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.13.0.tgz#da1de4ae905b1b9ff682cab0bed6b2e3be9c04e5" + integrity sha512-LmE/KO6DUy0nFY/OoQU0XelnmDt+V8lPQhh8MOVa7Y5k2gGRd6U9Kp3wAjhB4OHg57tUO0nOnwYQhRRyEAyOyg== -"@typescript-eslint/typescript-estree@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.1.tgz#6a9425b9c305bcbc38e2d1d9a24c08e15e02b722" - integrity sha512-ahOdkIY9Mgbza7L9sIi205Pe1inCkZWAHE1TV1bpxlU4RZNPtXaDZfiiFWcL9jdxvW1hDYZJXrFm+vlMkXRbBw== +"@typescript-eslint/typescript-estree@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.13.0.tgz#b37c07b748ff030a3e93d87c842714e020b78141" + integrity sha512-Q9cQow0DeLjnp5DuEDjLZ6JIkwGx3oYZe+BfcNuw/POhtpcxMTy18Icl6BJqTSd+3ftsrfuVb7mNHRZf7xiaNA== dependencies: - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/visitor-keys" "5.12.1" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.12.1.tgz#447c24a05d9c33f9c6c64cb48f251f2371eef920" - integrity sha512-Qq9FIuU0EVEsi8fS6pG+uurbhNTtoYr4fq8tKjBupsK5Bgbk2I32UGm0Sh+WOyjOPgo/5URbxxSNV6HYsxV4MQ== +"@typescript-eslint/utils@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.13.0.tgz#2328feca700eb02837298339a2e49c46b41bd0af" + integrity sha512-+9oHlPWYNl6AwwoEt5TQryEHwiKRVjz7Vk6kaBeD3/kwHE5YqTGHtm/JZY8Bo9ITOeKutFaXnBlMgSATMJALUQ== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.12.1" - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/typescript-estree" "5.12.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/typescript-estree" "5.13.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.1.tgz#f722da106c8f9695ae5640574225e45af3e52ec3" - integrity sha512-l1KSLfupuwrXx6wc0AuOmC7Ko5g14ZOQ86wJJqRbdLbXLK02pK/DPiDDqCc7BqqiiA04/eAA6ayL0bgOrAkH7A== +"@typescript-eslint/visitor-keys@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.13.0.tgz#f45ff55bcce16403b221ac9240fbeeae4764f0fd" + integrity sha512-HLKEAS/qA1V7d9EzcpLFykTePmOQqOFim8oCvhY3pZgQ8Hi38hYpHd9e5GN6nQBFQNecNhws5wkS9Y5XIO0s/g== dependencies: - "@typescript-eslint/types" "5.12.1" + "@typescript-eslint/types" "5.13.0" eslint-visitor-keys "^3.0.0" "@ungap/promise-all-settled@1.1.2": @@ -1737,6 +1756,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +csstype@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" + integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== + dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -1989,9 +2013,9 @@ escape-string-regexp@^1.0.5: integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= eslint-config-prettier@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.4.0.tgz#8e6d17c7436649e98c4c2189868562921ef563de" - integrity sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw== + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-plugin-no-only-tests@^2.6.0: version "2.6.0" @@ -2530,9 +2554,9 @@ has-flag@^4.0.0: integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== has-tostringtag@^1.0.0: version "1.0.0" From 7cee2ffb4808a01e1e4d046980c0c7845b424b88 Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Mon, 7 Mar 2022 01:03:51 +0200 Subject: [PATCH 02/10] fix typo --- packages/plugable/src/plugable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugable/src/plugable.ts b/packages/plugable/src/plugable.ts index 2e4a775b..61ca3c28 100644 --- a/packages/plugable/src/plugable.ts +++ b/packages/plugable/src/plugable.ts @@ -37,7 +37,7 @@ export function set( rec: Plugable, key: Key, value: Value, - isEqual = (prevues: Value | undefined, value: Value) => prevues === value + isEqual = (previous: Value | undefined, value: Value) => previous === value ) { if (!isEqual(get(rec, key), value)) { (rec as Record, Value>)[key] = value; From 68069785cafb5192826aefc142d37688da8dbcde Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Wed, 9 Mar 2022 12:20:22 +0200 Subject: [PATCH 03/10] add assert to test --- packages/plugable/src/test/test.unit.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/plugable/src/test/test.unit.ts b/packages/plugable/src/test/test.unit.ts index 20220652..5cb9250b 100644 --- a/packages/plugable/src/test/test.unit.ts +++ b/packages/plugable/src/test/test.unit.ts @@ -72,6 +72,9 @@ describe('Plugable', () => { set(parent, key, 'hello'); expect(res).to.be.empty; + + const value = get(grandChild, key); + expect(value).to.equal('world'); }); it('emit on grandChild when set on parent (when there is no override on child)', () => { From 54e57b79b6342f96522f205496af35a880b81c06 Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Wed, 3 May 2023 14:50:24 +0300 Subject: [PATCH 04/10] feat: added prototype to the record --- packages/plugable/src/plugable-react.tsx | 18 +++++--- packages/plugable/src/plugable.ts | 53 ++++++++++++++++-------- packages/plugable/src/types.ts | 7 +++- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/packages/plugable/src/plugable-react.tsx b/packages/plugable/src/plugable-react.tsx index 481eb100..0fb37fdb 100644 --- a/packages/plugable/src/plugable-react.tsx +++ b/packages/plugable/src/plugable-react.tsx @@ -1,16 +1,24 @@ import { createContext, useContext, useEffect, useState } from 'react'; import type { Plugable, Key } from './types'; -import { createPlugable, on, get } from './plugable'; +import { on, get } from './plugable'; -export const PlugableContext = createContext(createPlugable()); +export const PlugableContext = createContext(undefined); export function usePlugable() { - return useContext(PlugableContext); + const plugable = useContext(PlugableContext); + if (!plugable) { + throw new Error('PlugableContext is not initialized'); + } + return plugable; +} + +function toggle(v: boolean) { + return !v; } export function usePlugableValue(key: Key): T | undefined { - const [_, setState] = useState({}); + const [_, setState] = useState(true); const map = usePlugable(); - useEffect(() => on(map, key, () => setState({})), [key]); + useEffect(() => on(map, key, () => setState(toggle)), [key]); return get(map, key); } diff --git a/packages/plugable/src/plugable.ts b/packages/plugable/src/plugable.ts index 61ca3c28..50ce7ed4 100644 --- a/packages/plugable/src/plugable.ts +++ b/packages/plugable/src/plugable.ts @@ -1,7 +1,24 @@ import { type Plugable, type Key, internals, PlugableInternals, Val } from './types'; +const proto = { + get(this: Plugable, key: Key): Value | undefined { + return get(this, key); + }, + set( + this: Plugable, + key: Key, + value: Value, + isEqual?: (previous: Value | undefined, value: Value) => boolean + ) { + set(this, key, value, isEqual); + }, + on(this: Plugable, key: K, listener: (value: Val) => void) { + return on(this, key, listener); + }, +}; + export function createPlugable(): Plugable { - const rec = Object.create(null) as Plugable; + const rec = Object.create(proto) as Plugable; rec[internals] = { parent: undefined, listeners: new Map(), @@ -45,15 +62,29 @@ export function set( } } -function hasOwn(obj: unknown, key: string | symbol) { - return Object.prototype.hasOwnProperty.call(obj, key); +export function on(rec: Plugable, key: K, listener: (value: Val) => void) { + const { listeners, handlerToRec } = rec[internals]; + let handlers = listeners.get(key); + if (!handlers) { + handlers = new Set(); + listeners.set(key, handlers); + } + handlers.add(listener); + handlerToRec.set(listener, rec); + return () => { + handlers?.delete(listener); + }; } -export function dispatch(rec: Plugable, key: Key, value: T) { +function dispatch(rec: Plugable, key: Key, value: T) { const { listeners, handlerToRec } = rec[internals]; listeners.get(key)?.forEach((listener) => shouldDispatch(rec, handlerToRec.get(listener), key) && listener(value)); } +function hasOwn(obj: unknown, key: string | symbol) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + function shouldDispatch(dispatcherRec: Plugable, handlerRec: Plugable | undefined, key: string | symbol): boolean { if (dispatcherRec === handlerRec) { return true; @@ -63,17 +94,3 @@ function shouldDispatch(dispatcherRec: Plugable, handlerRec: Plugable | undefine return false; } } - -export function on(rec: Plugable, key: K, listener: (value: Val) => void) { - const { listeners, handlerToRec } = rec[internals]; - let handlers = listeners.get(key); - if (!handlers) { - handlers = new Set(); - listeners.set(key, handlers); - } - handlers.add(listener); - handlerToRec.set(listener, rec); - return () => { - handlers?.delete(listener); - }; -} diff --git a/packages/plugable/src/types.ts b/packages/plugable/src/types.ts index 61609d3f..bc16f206 100644 --- a/packages/plugable/src/types.ts +++ b/packages/plugable/src/types.ts @@ -1,7 +1,7 @@ const secret = Symbol(); export const internals = Symbol('internals'); -export type Key = (string | symbol) & { [secret]: V | undefined }; +export type Key = symbol & { [secret]: V | undefined }; export type Val = T extends Key ? V : never; export type PlugableInternals = { @@ -13,6 +13,9 @@ export type PlugableInternals = { }; export type Plugable = { - [key: string]: never; + get(key: Key): Value | undefined; + set(key: Key, value: Value, isEqual?: (previous: Value | undefined, value: Value) => boolean): void; + on(key: K, listener: (value: Val) => void): () => void; [internals]: PlugableInternals; }; + From 8ba7761d117f2e95eed468ce106cfb921ec2cf7d Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Wed, 3 May 2023 15:02:22 +0300 Subject: [PATCH 05/10] update package --- packages/plugable/package.json | 7 +- packages/plugable/src/test/test.unit.ts | 183 +++++++++++++----------- packages/plugable/src/tsconfig.esm.json | 9 ++ packages/plugable/src/tsconfig.json | 2 +- 4 files changed, 112 insertions(+), 89 deletions(-) create mode 100644 packages/plugable/src/tsconfig.esm.json diff --git a/packages/plugable/package.json b/packages/plugable/package.json index 8d7502ee..bdc7ffe9 100644 --- a/packages/plugable/package.json +++ b/packages/plugable/package.json @@ -1,9 +1,10 @@ { - "name": "@wixc3/plugable-map", + "name": "@wixc3/plugable", "version": "1.0.1", - "main": "dist/index.js", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", "scripts": { - "test": "mocha \"dist/test/**/*.unit.js\"" + "test": "mocha \"dist/cjs/test/**/*.unit.js\"" }, "peerDependencies": { "react": "^17.0.2" diff --git a/packages/plugable/src/test/test.unit.ts b/packages/plugable/src/test/test.unit.ts index 5cb9250b..c54f50b1 100644 --- a/packages/plugable/src/test/test.unit.ts +++ b/packages/plugable/src/test/test.unit.ts @@ -4,89 +4,102 @@ import { createPlugable, createKey, inheritPlugable, set, get, on } from '../plu chai.use(chaiAsPromised); describe('Plugable', () => { - it('set and get', () => { - const rec = createPlugable(); - const key = createKey(); - - set(rec, key, 'hello'); - expect(get(rec, key)).to.equal('hello'); - }); - - it('emit on set', () => { - const rec = createPlugable(); - const key = createKey(); - const res = new Array(); - - on(rec, key, res.push.bind(res)); - set(rec, key, 'hello'); - - expect(res[0]).to.equal('hello'); - }); - - it('emit on child when set', () => { - const parent = createPlugable(); - const child = inheritPlugable(parent); - const key = createKey(); - const res = new Array(); - - on(child, key, res.push.bind(res)); - set(child, key, 'hello'); - - expect(res[0]).to.equal('hello'); - }); - - it('emit on child when set on parent (no child override)', () => { - const parent = createPlugable(); - const child = inheritPlugable(parent); - const key = createKey(); - const res = new Array(); - - on(child, key, res.push.bind(res)); - set(parent, key, 'hello'); - - expect(res[0]).to.equal('hello'); - }); - - it('no emit on child when set on parent (when there is override)', () => { - const parent = createPlugable(); - const child = inheritPlugable(parent); - const key = createKey(); - const res = new Array(); - - set(child, key, 'world'); - on(child, key, res.push.bind(res)); - set(parent, key, 'hello'); - - expect(res).to.be.empty; - }); - - it('no emit on grandChild when set on parent (when there is override on child)', () => { - const parent = createPlugable(); - const child = inheritPlugable(parent); - const grandChild = inheritPlugable(child); - const key = createKey(); - const res = new Array(); - - set(child, key, 'world'); - on(grandChild, key, res.push.bind(res)); - set(parent, key, 'hello'); - - expect(res).to.be.empty; - - const value = get(grandChild, key); - expect(value).to.equal('world'); - }); - - it('emit on grandChild when set on parent (when there is no override on child)', () => { - const parent = createPlugable(); - const child = inheritPlugable(parent); - const grandChild = inheritPlugable(child); - const key = createKey(); - const res = new Array(); - - on(grandChild, key, res.push.bind(res)); - set(parent, key, 'hello'); - - expect(res[0]).to.equal('hello'); - }); + it('set and get', () => { + const rec = createPlugable(); + const key = createKey(); + + set(rec, key, 'hello'); + expect(get(rec, key)).to.equal('hello'); + }); + + it('emit on set', () => { + const rec = createPlugable(); + const key = createKey(); + const res = new Array(); + + on(rec, key, res.push.bind(res)); + set(rec, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); + + it('emit on child when set', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + on(child, key, res.push.bind(res)); + set(child, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); + + it('emit on child when set on parent (no child override)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + on(child, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); + + it('no emit on child when set on parent (when there is override)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + set(child, key, 'world'); + on(child, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res).to.be.empty; + }); + + it('no emit on grandChild when set on parent (when there is override on child)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const grandChild = inheritPlugable(child); + const key = createKey(); + const res = new Array(); + + set(child, key, 'world'); + on(grandChild, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res).to.be.empty; + + const value = get(grandChild, key); + expect(value).to.equal('world'); + }); + + it('emit on grandChild when set on parent (when there is no override on child)', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const grandChild = inheritPlugable(child); + const key = createKey(); + const res = new Array(); + + on(grandChild, key, res.push.bind(res)); + set(parent, key, 'hello'); + + expect(res[0]).to.equal('hello'); + }); +}); + +describe('Plugable (prototype api)', () => { + it('set and get and on', () => { + const rec = createPlugable(); + const key = createKey(); + const res = new Array(); + + rec.on(key, res.push.bind(res)); + rec.set(key, 'hello'); + expect(rec.get(key)).to.equal('hello'); + expect(res[0]).to.equal('hello'); + }); }); diff --git a/packages/plugable/src/tsconfig.esm.json b/packages/plugable/src/tsconfig.esm.json new file mode 100644 index 00000000..82e97afc --- /dev/null +++ b/packages/plugable/src/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/esm", + "module": "esnext", + "types": ["mocha"] + }, + "references": [] +} diff --git a/packages/plugable/src/tsconfig.json b/packages/plugable/src/tsconfig.json index f0b1afca..49b162f4 100644 --- a/packages/plugable/src/tsconfig.json +++ b/packages/plugable/src/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "../dist", + "outDir": "../dist/cjs", "types": ["mocha"] }, "references": [] From 6d87a174664ab2d95336f11331b16b8e45676d37 Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Thu, 4 May 2023 15:21:31 +0300 Subject: [PATCH 06/10] * split plugable type * use getThrow * improve set default value perf --- packages/plugable/src/plugable.ts | 140 +++++++++++++++--------------- packages/plugable/src/types.ts | 23 ++--- 2 files changed, 84 insertions(+), 79 deletions(-) diff --git a/packages/plugable/src/plugable.ts b/packages/plugable/src/plugable.ts index 50ce7ed4..3d43f1ca 100644 --- a/packages/plugable/src/plugable.ts +++ b/packages/plugable/src/plugable.ts @@ -1,96 +1,98 @@ -import { type Plugable, type Key, internals, PlugableInternals, Val } from './types'; +import { type Plugable, type Key, internals, PlugableInternals, Val, PlugableApi } from './types'; -const proto = { - get(this: Plugable, key: Key): Value | undefined { - return get(this, key); - }, - set( - this: Plugable, - key: Key, - value: Value, - isEqual?: (previous: Value | undefined, value: Value) => boolean - ) { - set(this, key, value, isEqual); - }, - on(this: Plugable, key: K, listener: (value: Val) => void) { - return on(this, key, listener); - }, +const proto: PlugableApi = { + getThrow(this: Plugable, key: Key): Value { + return getThrow(this, key); + }, + get(this: Plugable, key: Key): Value | undefined { + return get(this, key); + }, + set( + this: Plugable, + key: Key, + value: Value, + isEqual?: (previous: Value | undefined, value: Value) => boolean + ) { + set(this, key, value, isEqual); + }, + on(this: Plugable, key: K, listener: (value: Val) => void) { + return on(this, key, listener); + }, }; export function createPlugable(): Plugable { - const rec = Object.create(proto) as Plugable; - rec[internals] = { - parent: undefined, - listeners: new Map(), - handlerToRec: new WeakMap(), - }; - return rec; + const rec = Object.create(proto) as Plugable; + rec[internals] = { + parent: undefined, + listeners: new Map(), + handlerToRec: new WeakMap(), + }; + return rec; } -export function inheritPlugable(rec: Plugable) { - const inherited = Object.create(rec) as Plugable; - inherited[internals] = Object.create(rec[internals]) as PlugableInternals; - inherited[internals].parent = rec; - return inherited; +export function inheritPlugable(rec: Plugable): Plugable { + const inherited = Object.create(rec) as Plugable; + inherited[internals] = Object.create(rec[internals]) as PlugableInternals; + inherited[internals].parent = rec; + return inherited; } export function createKey(debugName?: string): Key { - return Symbol(debugName) as Key; + return Symbol(debugName) as Key; } export function getThrow(rec: Plugable, key: Key): Value { - const value = get(rec, key); - if (value === undefined || value === null) { - throw new Error(`missing value for key ${String(key)}`); - } - return value; + const value = get(rec, key); + if (value === undefined || value === null) { + throw new Error(`missing value for key ${String(key)}`); + } + return value; } export function get(rec: Plugable, key: Key): Value | undefined { - return (rec as Record, Value>)[key]; + return (rec as Record, Value>)[key]; } -export function set( - rec: Plugable, - key: Key, - value: Value, - isEqual = (previous: Value | undefined, value: Value) => previous === value -) { - if (!isEqual(get(rec, key), value)) { - (rec as Record, Value>)[key] = value; - dispatch(rec, key, value); - } +export function set(rec: Plugable, key: Key, value: Value, isEqual = tripleEqual): void { + if (!isEqual(get(rec, key), value)) { + (rec as Record, Value>)[key] = value; + dispatch(rec, key, value); + } } -export function on(rec: Plugable, key: K, listener: (value: Val) => void) { - const { listeners, handlerToRec } = rec[internals]; - let handlers = listeners.get(key); - if (!handlers) { - handlers = new Set(); - listeners.set(key, handlers); - } - handlers.add(listener); - handlerToRec.set(listener, rec); - return () => { - handlers?.delete(listener); - }; +export function on(rec: Plugable, key: K, listener: (value: Val) => void): () => void { + const { listeners, handlerToRec } = rec[internals]; + let handlers = listeners.get(key); + if (!handlers) { + handlers = new Set(); + listeners.set(key, handlers); + } + handlers.add(listener); + handlerToRec.set(listener, rec); + return () => { + handlers?.delete(listener); + }; } -function dispatch(rec: Plugable, key: Key, value: T) { - const { listeners, handlerToRec } = rec[internals]; - listeners.get(key)?.forEach((listener) => shouldDispatch(rec, handlerToRec.get(listener), key) && listener(value)); +function tripleEqual(previous: T | undefined, value: T): boolean { + return previous === value; } -function hasOwn(obj: unknown, key: string | symbol) { - return Object.prototype.hasOwnProperty.call(obj, key); +function dispatch(rec: Plugable, key: Key, value: T): void { + const { listeners, handlerToRec } = rec[internals]; + listeners.get(key)?.forEach((listener) => shouldDispatch(rec, handlerToRec.get(listener), key) && listener(value)); +} + +function hasOwn(obj: unknown, key: string | symbol): boolean { + return Object.prototype.hasOwnProperty.call(obj, key); } function shouldDispatch(dispatcherRec: Plugable, handlerRec: Plugable | undefined, key: string | symbol): boolean { - if (dispatcherRec === handlerRec) { - return true; - } else if (handlerRec && !hasOwn(handlerRec, key)) { - return shouldDispatch(dispatcherRec, handlerRec[internals].parent, key); - } else { - return false; - } + if (dispatcherRec === handlerRec) { + return true; + } else if (handlerRec && !hasOwn(handlerRec, key)) { + return shouldDispatch(dispatcherRec, handlerRec[internals].parent, key); + } else { + return false; + } } diff --git a/packages/plugable/src/types.ts b/packages/plugable/src/types.ts index bc16f206..55cd82fd 100644 --- a/packages/plugable/src/types.ts +++ b/packages/plugable/src/types.ts @@ -5,17 +5,20 @@ export type Key = symbol & { [secret]: V | undefined }; export type Val = T extends Key ? V : never; export type PlugableInternals = { - parent: Plugable | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly listeners: Map void>>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly handlerToRec: WeakMap<(value: any) => void, Plugable>; + parent: Plugable | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly listeners: Map void>>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly handlerToRec: WeakMap<(value: any) => void, Plugable>; }; -export type Plugable = { - get(key: Key): Value | undefined; - set(key: Key, value: Value, isEqual?: (previous: Value | undefined, value: Value) => boolean): void; - on(key: K, listener: (value: Val) => void): () => void; - [internals]: PlugableInternals; +export type PlugableApi = { + getThrow(key: Key): Value; + get(key: Key): Value | undefined; + set(key: Key, value: Value, isEqual?: (previous: Value | undefined, value: Value) => boolean): void; + on(key: K, listener: (value: Val) => void): () => void; }; +export type Plugable = PlugableApi & { + [internals]: PlugableInternals; +}; From 3706a62e23961e100100055ce53d8c7c1d2a000a Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Thu, 4 May 2023 17:26:47 +0300 Subject: [PATCH 07/10] * improve types * add more tests * expose createKey on the api --- packages/plugable/src/plugable.ts | 5 +- packages/plugable/src/test/test.unit.ts | 74 ++++++++++++++++++++++++- packages/plugable/src/types.ts | 1 + 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/plugable/src/plugable.ts b/packages/plugable/src/plugable.ts index 3d43f1ca..685ec8fd 100644 --- a/packages/plugable/src/plugable.ts +++ b/packages/plugable/src/plugable.ts @@ -1,6 +1,9 @@ import { type Plugable, type Key, internals, PlugableInternals, Val, PlugableApi } from './types'; const proto: PlugableApi = { + createKey = never>(debugName?: string): Key { + return createKey(debugName); + }, getThrow(this: Plugable, key: Key): Value { return getThrow(this, key); }, @@ -37,7 +40,7 @@ export function inheritPlugable(rec: Plugable): Plugable { return inherited; } -export function createKey(debugName?: string): Key { +export function createKey = never>(debugName?: string): Key { return Symbol(debugName) as Key; } diff --git a/packages/plugable/src/test/test.unit.ts b/packages/plugable/src/test/test.unit.ts index c54f50b1..3b2fa86d 100644 --- a/packages/plugable/src/test/test.unit.ts +++ b/packages/plugable/src/test/test.unit.ts @@ -1,6 +1,6 @@ import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { createPlugable, createKey, inheritPlugable, set, get, on } from '../plugable'; +import { createPlugable, createKey, inheritPlugable, set, get, on, getThrow } from '../plugable'; chai.use(chaiAsPromised); describe('Plugable', () => { @@ -12,6 +12,18 @@ describe('Plugable', () => { expect(get(rec, key)).to.equal('hello'); }); + it('getThrow', () => { + const rec = createPlugable(); + const key = createKey(); + + expect(() => { + getThrow(rec, key); + }).to.throw(`missing value for key`); + + set(rec, key, 'hello'); + expect(get(rec, key)).to.equal('hello'); + }); + it('emit on set', () => { const rec = createPlugable(); const key = createKey(); @@ -23,6 +35,53 @@ describe('Plugable', () => { expect(res[0]).to.equal('hello'); }); + it('same value does not trigger event', () => { + const rec = createPlugable(); + const key = createKey(); + const res = new Array(); + + on(rec, key, res.push.bind(res)); + set(rec, key, 'hello'); + set(rec, key, 'hello'); + expect(res[0]).to.equal('hello'); + expect(res).to.have.length(1); + }); + + it('remove listener', () => { + const rec = createPlugable(); + const key = createKey(); + const res = new Array(); + + const off = on(rec, key, res.push.bind(res)); + set(rec, key, 'hello'); + off(); + set(rec, key, 'world'); + expect(res).to.have.length(1); + }); + + it('multiple listeners', () => { + const rec = createPlugable(); + const key = createKey(); + const resA = new Array(); + const resB = new Array(); + + const offA = on(rec, key, resA.push.bind(resA)); + const offB = on(rec, key, resB.push.bind(resB)); + set(rec, key, 'hello'); + offA(); + set(rec, key, 'world'); + + expect(resA).to.have.length(1); + expect(resB).to.have.length(2); + + offB(); + + set(rec, key, 'goodbye'); + + expect(resA).to.have.length(1); + expect(resB).to.have.length(2); + }); + it('emit on child when set', () => { const parent = createPlugable(); const child = inheritPlugable(parent); @@ -35,6 +94,19 @@ describe('Plugable', () => { expect(res[0]).to.equal('hello'); }); + it('child does not affect parent', () => { + const parent = createPlugable(); + const child = inheritPlugable(parent); + const key = createKey(); + const res = new Array(); + + on(parent, key, res.push.bind(res)); + set(child, key, 'hello'); + + expect(res).to.eql([]); + expect(get(parent, key)).to.equal(undefined); + }); + it('emit on child when set on parent (no child override)', () => { const parent = createPlugable(); const child = inheritPlugable(parent); diff --git a/packages/plugable/src/types.ts b/packages/plugable/src/types.ts index 55cd82fd..44d5a619 100644 --- a/packages/plugable/src/types.ts +++ b/packages/plugable/src/types.ts @@ -13,6 +13,7 @@ export type PlugableInternals = { }; export type PlugableApi = { + createKey = never>(debugName?: string): Key; getThrow(key: Key): Value; get(key: Key): Value | undefined; set(key: Key, value: Value, isEqual?: (previous: Value | undefined, value: Value) => boolean): void; From 7c63ee965b119d6ab6de14c85184f5011042c882 Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Sat, 1 Jul 2023 02:05:00 +0300 Subject: [PATCH 08/10] docs: added introduction --- packages/plugable/README.md | 288 ++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) diff --git a/packages/plugable/README.md b/packages/plugable/README.md index 416bccbd..ac0e42d3 100644 --- a/packages/plugable/README.md +++ b/packages/plugable/README.md @@ -1 +1,289 @@ # Plugable + +## Introduction: + +The Plugable System is a JavaScript library that provides a flexible and extensible approach to creating objects with dynamically assignable properties and event-driven communication. It allows you to create plugable objects that can store values associated with keys and notify registered listeners when those values change. This documentation will guide you through the usage and features of the Plugable System, helping you understand how to integrate it into your JavaScript projects. + +The Plugable System offers a simple and intuitive API for working with plugable objects. It provides methods for creating keys, getting and setting values, registering and managing listeners, and inheriting from existing plugable objects. By leveraging the Plugable System, you can design modular and reusable components that can be easily extended and customized. + +## Key Features: + +1. Plugable Objects: The core concept of the Plugable System is the plugable object. These objects act as containers for storing values associated with keys. Plugable objects can be created using the `createPlugable()` function and can inherit from other plugable objects using the `inheritPlugable()` function. + +2. Key Creation: The `createKey()` function allows you to create unique keys that can be used as property keys in plugable objects. These keys ensure that values are stored and accessed consistently. + +3. Value Access: The Plugable System provides two methods for accessing values stored in plugable objects. The `getThrow()` function retrieves a value associated with a key and throws an error if the value is not found. The `get()` function retrieves a value associated with a key and returns `undefined` if the value is not found. + +4. Value Modification: The `set()` function enables you to set the value associated with a key in a plugable object. It automatically dispatches the new value to the registered listeners, notifying them of the change. + +5. Event System: The Plugable System includes an event system that allows you to register listeners for specific keys in plugable objects. The `on()` function is used to register listeners, and they will be notified whenever the associated value changes. + +6. Inheritance: Plugable objects support inheritance, allowing you to create a hierarchy of plugable objects. Inherited plugable objects retain their own listeners and values while also inheriting the values and listeners from their parent plugable objects. + +By utilizing these features, you can build highly modular and flexible applications that are easily extensible and maintainable. The Plugable System provides a powerful mechanism for creating objects with dynamic properties and event-driven behavior, enabling you to implement complex and interactive systems with ease. + +Next, let's delve into the installation process and the basic usage of the Plugable System. + +## Installation + +To get started with the Plugable System, you need to install the `@wixc3/plugable` package in your JavaScript project. The package can be installed using npm or yarn. Make sure you have Node.js and npm or yarn installed on your development machine before proceeding with the installation. + +### Using npm + +Open your terminal or command prompt and run the following command to install the `@wixc3/plugable` package: + +```bash +npm install @wixc3/plugable +``` + +## Basic Usage + +Now that you have the `@wixc3/plugable` package installed, you can begin working with plugable objects and utilizing the provided features. + +### Creating Plugable Objects + +To create a new plugable object, use the `createPlugable()` function: + +```javascript +import { createPlugable } from '@wixc3/plugable'; + +const myPlugable = createPlugable(); +``` + +### Creating and Using Keys + +The `createKey()` function is used to create unique keys, which will be used as property keys in plugable objects: + +```javascript +import { createKey } from '@wixc3/plugable'; + +const nameKey = createKey('name'); +const ageKey = createKey('age'); +``` + +You can then use these keys to set and retrieve values in your plugable object: + +```javascript +myPlugable.set(nameKey, 'John Doe'); +myPlugable.set(ageKey, 30); + +console.log(myPlugable.get(nameKey)); // Output: "John Doe" +console.log(myPlugable.get(ageKey)); // Output: 30 +``` + +### Registering Listeners + +The Plugable System's event system allows you to register listeners for specific keys in plugable objects using the `on()` function: + +```javascript +import { on } from '@wixc3/plugable'; + +const unsubscribe = on(myPlugable, ageKey, (newValue) => { + console.log(`Age has been updated to: ${newValue}`); +}); + +myPlugable.set(ageKey, 35); // Output: "Age has been updated to: 35" + +// Later, if you want to unsubscribe the listener: +unsubscribe(); +``` + +## Advanced Usage + +The `@wixc3/plugable` package provides advanced features and functionalities that enhance the flexibility and extensibility of the Plugable System. In this section, we will explore these features based on the provided tests and previous knowledge. + +### Inheriting Plugable Objects + +The Plugable System supports the concept of inheritance, allowing you to create child plugable objects that inherit properties and listeners from a parent plugable object. This enables hierarchical relationships and enables cascading updates. + +To create a child plugable object that inherits from a parent plugable object, use the `inheritPlugable()` function: + +```javascript +import { inheritPlugable } from '@wixc3/plugable'; + +const parent = createPlugable(); +const child = inheritPlugable(parent); +``` + +With inheritance, changes made to the parent plugable object will propagate to the child plugable object, while changes made to the child plugable object will not affect the parent. + +### Event Emission in Inherited Plugable Objects + +When a key is set or updated in a parent plugable object, the event is emitted not only in the parent but also in any child plugable objects that have registered listeners for that key. This allows you to handle events and updates at different levels of the plugable hierarchy. + +Consider the following example: + +```javascript +import { createPlugable, createKey, inheritPlugable, set, on } from '@wixc3/plugable'; + +const parent = createPlugable(); +const child = inheritPlugable(parent); +const key = createKey(); +const res = new Array(); + +on(child, key, (value) => res.push(value)); +set(parent, key, 'hello'); + +console.log(res); // Output: ['hello'] +``` + +In the above example, we create a parent plugable object and a child plugable object that inherits from the parent. We then register a listener on the child for a specific key and set a value for that key in the parent. As a result, the event is emitted in the child, and the value is added to the `res` array. + +### Managing Overrides in Inherited Plugable Objects + +In the Plugable System, when a key is set or updated in a parent plugable object, the event is emitted to child plugable objects unless there is an override for that key in a child object. An override occurs when a child plugable object sets a different value for a key that already exists in its parent. + +Let's examine how overrides are managed using the provided tests: + +```javascript +import { createPlugable, createKey, inheritPlugable, set, get } from '@wixc3/plugable'; + +const parent = createPlugable(); +const child = inheritPlugable(parent); +const key = createKey(); +const res = new Array(); + +set(child, key, 'world'); +on(child, key, (value) => res.push(value)); +set(parent, key, 'hello'); + +console.log(res); // Output: [] + +const value = get(child, key); +console.log(value); // Output: 'world' +``` + +In the above example, we create a parent plugable object and a child plugable object that inherits from the parent. We set a value of `'world'` for the `key` in the child object before registering a listener on the child for that key. Then, we set a value of `'hello'` for the `key` in the parent object. + +Since the child plugable object has an override for the `key`, the event emitted from the parent is not propagated to the child. Therefore, the `res` array remains empty. However, we can still access the overridden value `'world'` by directly querying the child object using `get(child, key)`. + +This behavior allows you to selectively override values in child plugable objects while maintaining the inheritance of other values and event handling from the parent object. + + +### Custom Value Comparison + +By default, the `set()` function compares the previous value with the new value using the triple equals (`===`) operator. However, you can customize the value comparison logic by passing an optional `isEqual` function as the last argument to the `set()` function. The `isEqual` function takes the previous value and the new value as arguments and returns a boolean indicating whether they are considered equal. + +For example: + +```javascript +import { createPlugable, createKey, set } from '@wixc3/plugable'; + +const myKey = createKey(); + +// Custom value comparison function +function customValueComparison(previous: number | undefined, value: number): boolean { + // Compare values based on divisibility by 2 + return value % 2 === 0; +} + +const plugable = createPlugable(); +set(plugable, myKey, 2); // Initial value + +console.log(get(plugable, myKey)); // Output: 2 + +// Set a new value that is odd +set(plugable, myKey, 5, customValueComparison); + +console.log(get(plugable, myKey)); // Output: 2 (Previous value is retained) + +// Set a new value that is even +set(plugable, myKey, 6, customValueComparison); + +console.log(get(plugable, myKey)); // Output: 6 (Value is updated) +``` + +In this simplified example, we create a custom value comparison function `customValueComparison` that checks whether a number is even. When setting a new value using the `set` function, we pass the `customValueComparison` function as the `isEqual` parameter. If the new value is even, it is considered equal to the previous value, and the update is ignored. However, if the new value is odd, it is considered different, and the update is applied. + +This example demonstrates how you can define your own custom logic for determining when two values should be considered equal. It allows you to have control over state updates based on specific value comparisons, enabling you to optimize rendering and avoid unnecessary re-renders in React components. + +### Summary + +By managing overrides in inherited plugable objects, the Plugable System provides fine-grained control over event propagation and value inheritance. You can selectively override values in child objects, ensuring that events are emitted only to the appropriate listeners + + +## React Adapter: Managing Plugable State in React Components + +The `@wixc3/plugable` package provides a React adapter that allows you to integrate plugable state management into your React components seamlessly. This section will explain the usage of the React adapter and its associated hooks: `PlugableContext`, `usePlugable`, and `usePlugableValue`. These hooks enable easy access to plugable state and provide automatic re-rendering of components when the state changes. + +### PlugableContext + +The `PlugableContext` is a React context that holds the reference to a plugable object. It allows child components to access the plugable state without the need for prop drilling. The `PlugableContext` should be initialized with a valid plugable object at an appropriate level in your component tree, typically in a higher-level component. + +Example usage: + +```javascript +import { PlugableContext, createPlugable } from '@wixc3/plugable'; + +const plugable = createPlugable(); + +function App() { + return ( + + {/* Your component hierarchy */} + + ); +} +``` + +### usePlugable + +The `usePlugable` hook is used to retrieve the plugable object from the `PlugableContext`. It simplifies accessing the plugable state within your React components. If the `PlugableContext` is not initialized properly, an error will be thrown. + +Example usage: + +```javascript +import { usePlugable } from '@wixc3/plugable'; + +function MyComponent() { + const plugable = usePlugable(); + + // Use the plugable state and perform operations + // ... +} +``` + +### usePlugableValue + +The `usePlugableValue` hook is used to access a specific value from the plugable state based on a provided key. It automatically subscribes the component to updates of that value, ensuring that the component is re-rendered whenever the value changes. + +Example usage: + +```javascript +import { usePlugableValue } from '@wixc3/plugable'; +import { myData } from './keys'; + +function DisplayData() { + const data = usePlugableValue(myData); + + return ( +
+ {/* Display the data */} + {data &&

{data}

} +
+ ); +} +``` + +In the above example, the `DisplayData` component utilizes the `usePlugableValue` hook to access the value associated with the key `myData` from the plugable state. The component automatically re-renders whenever the value of `myData` changes, reflecting the updated data in the UI. + + +By leveraging the React adapter and the provided hooks, you can easily manage plugable state within your React components, enabling efficient and reactive data flow. + +## Benefits of Using PlugableContext for React State Management + +Using the `PlugableContext` as the context for React provides several benefits: + +1. **Centralized State Management**: The `PlugableContext` allows you to centralize your state management logic in a single context, making it easier to manage and access state across different components without the need for prop drilling. + +2. **Component Decoupling**: With the `PlugableContext`, components can access the shared state without directly depending on each other. This decoupling improves the modularity and reusability of components, as they can rely on the context to provide the necessary data and functionality. + +3. **Dynamic Updates**: The plugable state stored in the `PlugableContext` can be dynamically updated, and any component subscribed to the context will automatically receive the updated values. This enables real-time data propagation and ensures that components stay in sync with the latest state changes. + +4. **Child Context Override**: The `PlugableContext` supports the concept of child context override, where you can create a new `Plugable` object that inherits from the parent `Plugable` and overrides specific values or adds new values. This allows you to customize the state for specific parts of your component tree while maintaining the overall state management structure provided by the parent context. + +5. **Flexible and Scalable Architecture**: The `PlugableContext` pattern allows you to build a flexible and scalable architecture for your React application. It supports composition and inheritance of plugable objects, enabling you to create complex state hierarchies and manage state at different levels of your component tree. + +6. **Efficient Rerendering**: The `usePlugable` and `usePlugableValue` hooks provided by the `@wixc3/plugable` package are optimized to only rerender components when the relevant state they depend on changes. This helps to improve performance by avoiding unnecessary rerenders in components that are not affected by state updates. + +By leveraging the `PlugableContext` in your React application, you can benefit from centralized state management, component decoupling, dynamic updates, child context override, and a scalable architecture that promotes reusability and maintainability. From f4e323dbfc3ac6d8089819092b71ebbb5d56563b Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Sat, 1 Jul 2023 02:27:50 +0300 Subject: [PATCH 09/10] docs: add type safety section --- packages/plugable/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/plugable/README.md b/packages/plugable/README.md index ac0e42d3..94342e12 100644 --- a/packages/plugable/README.md +++ b/packages/plugable/README.md @@ -202,6 +202,12 @@ This example demonstrates how you can define your own custom logic for determini By managing overrides in inherited plugable objects, the Plugable System provides fine-grained control over event propagation and value inheritance. You can selectively override values in child objects, ensuring that events are emitted only to the appropriate listeners +## Strong Typing: Ensuring Type Safety and Flexible Key-Based Management + +1. Key-Based Type Safety: This package leverages TypeScript's strong typing capabilities by associating values with unique keys. This ensures type safety when accessing and manipulating values within a plugable object. The use of keys enables compile-time type checking, preventing type errors and providing a reliable way to work with plugable values. + +2. No Central Type: This system does not enforce a central type for the plugable object itself. This flexibility allows developers to create plugable objects with varying structures and sets of keys, tailored to their specific application requirements. Each plugable object can have its own unique set of keys, representing the specific values it manages. This approach promotes flexibility and modularity in the design of plugable systems, as different parts of the application can define their own sets of keys without being restricted to a predefined centralized structure. + ## React Adapter: Managing Plugable State in React Components The `@wixc3/plugable` package provides a React adapter that allows you to integrate plugable state management into your React components seamlessly. This section will explain the usage of the React adapter and its associated hooks: `PlugableContext`, `usePlugable`, and `usePlugableValue`. These hooks enable easy access to plugable state and provide automatic re-rendering of components when the state changes. From 48b8079026a86de6f6e22a56afa5ef8987de078b Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Sun, 2 Jul 2023 10:27:35 +0300 Subject: [PATCH 10/10] update readme --- packages/plugable/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugable/README.md b/packages/plugable/README.md index 94342e12..52cac3c6 100644 --- a/packages/plugable/README.md +++ b/packages/plugable/README.md @@ -129,7 +129,7 @@ console.log(res); // Output: ['hello'] In the above example, we create a parent plugable object and a child plugable object that inherits from the parent. We then register a listener on the child for a specific key and set a value for that key in the parent. As a result, the event is emitted in the child, and the value is added to the `res` array. -### Managing Overrides in Inherited Plugable Objects +### Managing Overrides In the Plugable System, when a key is set or updated in a parent plugable object, the event is emitted to child plugable objects unless there is an override for that key in a child object. An override occurs when a child plugable object sets a different value for a key that already exists in its parent.