From 99ff4ba1eaad57cf132e674589acb023a0a27e3e Mon Sep 17 00:00:00 2001 From: Gordon Brander Date: Fri, 7 Jun 2024 15:38:26 -0700 Subject: [PATCH 1/4] Add common-frp-lit Introduces watch async directive to watch a common-frp signal within an HTML template. --- typescript/package-lock.json | 30 ++++++++--- typescript/package.json | 2 + .../packages/common-frp-lit/package.json | 51 +++++++++++++++++++ .../packages/common-frp-lit/src/index.ts | 37 ++++++++++++++ .../packages/common-frp-lit/tsconfig.json | 20 ++++++++ 5 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 typescript/packages/common-frp-lit/package.json create mode 100644 typescript/packages/common-frp-lit/src/index.ts create mode 100644 typescript/packages/common-frp-lit/tsconfig.json diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 6dc5dd0ee..84b51c1e6 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -330,6 +330,10 @@ "resolved": "packages/common-frp", "link": true }, + "node_modules/@commontools/common-frp-lit": { + "resolved": "packages/common-frp-lit", + "link": true + }, "node_modules/@commontools/data": { "resolved": "common/data", "link": true @@ -2404,9 +2408,9 @@ } }, "node_modules/lit": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.3.tgz", - "integrity": "sha512-l4slfspEsnCcHVRTvaP7YnkTZEZggNFywLEIhQaGhYDczG+tu/vlgm/KaWIEjIp+ZyV20r2JnZctMb8LeLCG7Q==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.4.tgz", + "integrity": "sha512-q6qKnKXHy2g1kjBaNfcoLlgbI3+aSOZ9Q4tiGa9bGYXq5RBXxkVTqTIVmP2VWMp29L4GyvCFm8ZQ2o56eUAMyA==", "dependencies": { "@lit/reactive-element": "^2.0.4", "lit-element": "^4.0.4", @@ -3473,9 +3477,9 @@ "dev": true }, "node_modules/vite": { - "version": "5.2.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", - "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", + "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", "dev": true, "dependencies": { "esbuild": "^0.20.1", @@ -3783,6 +3787,20 @@ "wireit": "^0.14.4" } }, + "packages/common-frp-lit": { + "name": "@commontools/common-frp-lit", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "lit": "^3.1.4" + }, + "devDependencies": { + "tslib": "^2.6.2", + "typescript": "^5.2.2", + "vite": "^5.2.13", + "wireit": "^0.14.4" + } + }, "packages/example-module": { "name": "@commontools/example-module", "version": "0.0.1", diff --git a/typescript/package.json b/typescript/package.json index da32c983a..86fcc82ee 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -36,6 +36,7 @@ "./packages/example-module:build", "./packages/lookslike-prototype:build", "./packages/common-frp:build", + "./packages/common-frp-lit:build", "./common/data:build", "./common/io:build", "./common/module:build", @@ -52,6 +53,7 @@ "./packages/example-module:clean", "./packages/lookslike-prototype:clean", "./packages/common-frp:clean", + "./packages/common-frp-lit:clean", "./common/data:clean", "./common/io:clean", "./common/module:clean", diff --git a/typescript/packages/common-frp-lit/package.json b/typescript/packages/common-frp-lit/package.json new file mode 100644 index 000000000..69122cd11 --- /dev/null +++ b/typescript/packages/common-frp-lit/package.json @@ -0,0 +1,51 @@ +{ + "name": "@commontools/common-frp-lit", + "author": "The Common Authors", + "version": "0.0.1", + "description": "common-frp integration for Lit", + "license": "MIT", + "private": true, + "type": "module", + "scripts": { + "build": "wireit", + "clean": "wireit" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/commontoolsinc/labs.git" + }, + "bugs": { + "url": "https://github.com/commontoolsinc/labs/issues" + }, + "homepage": "https://github.com/commontoolsinc/labs#readme", + "exports": "./lib/index.js", + "files": [ + "./lib/index.js" + ], + "dependencies": { + "lit": "^3.1.4" + }, + "devDependencies": { + "tslib": "^2.6.2", + "typescript": "^5.2.2", + "vite": "^5.2.13", + "wireit": "^0.14.4" + }, + "wireit": { + "build": { + "dependencies": [ + "../common-frp:build" + ], + "files": [ + "./src/**/*" + ], + "output": [ + "./lib/**/*" + ], + "command": "tsc --build -f" + }, + "clean": { + "command": "rm -rf ./lib ./.wireit" + } + } +} diff --git a/typescript/packages/common-frp-lit/src/index.ts b/typescript/packages/common-frp-lit/src/index.ts new file mode 100644 index 000000000..e4519dad3 --- /dev/null +++ b/typescript/packages/common-frp-lit/src/index.ts @@ -0,0 +1,37 @@ +import { signal } from "@commontools/common-frp"; +import {directive} from 'lit/directive.js'; +import {AsyncDirective} from 'lit/async-directive.js'; + +const {state, effect} = signal; + +class WatchDirective extends AsyncDirective { + #isWatching + + constructor(part: any) { + super(part); + this.#isWatching = state(true); + } + + override render(signal: any) { + effect(this.#isWatching, isWatching => { + if (isWatching) { + this.setValue(signal.get()); + } + }); + return signal.get(); + } + + protected override disconnected(): void { + this.#isWatching.send(false) + } + + protected override reconnected(): void { + this.#isWatching.send(true) + } +} + +/** + * Renders a signal and subscribes to it, updating the part when the signal + * changes. + */ +export const watch = directive(WatchDirective) \ No newline at end of file diff --git a/typescript/packages/common-frp-lit/tsconfig.json b/typescript/packages/common-frp-lit/tsconfig.json new file mode 100644 index 000000000..5e1a61b10 --- /dev/null +++ b/typescript/packages/common-frp-lit/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["es2022", "DOM"], + "outDir": "./lib", + "rootDir": "./src", + "strict": false, + "paths": { + "common:module/module@0.0.1": [ + "../../node_modules/@commontools/module/lib/index.d.ts" + ], + "common:io/state@0.0.1": [ + "../../node_modules/@commontools/io/lib/index.d.ts" + ] + } + }, + "include": [ + "src/**/*", + ] +} From 662ad7dbb11702582bf1109c4d1adbf9a349db69 Mon Sep 17 00:00:00 2001 From: Gordon Brander Date: Fri, 7 Jun 2024 15:47:06 -0700 Subject: [PATCH 2/4] Remove watch signal ...we'll need to think more carefully about how this should work in the world where Signals are not automatically cleaned up. --- .../packages/common-frp-lit/src/index.ts | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/typescript/packages/common-frp-lit/src/index.ts b/typescript/packages/common-frp-lit/src/index.ts index e4519dad3..aa6e1c742 100644 --- a/typescript/packages/common-frp-lit/src/index.ts +++ b/typescript/packages/common-frp-lit/src/index.ts @@ -1,33 +1,23 @@ import { signal } from "@commontools/common-frp"; -import {directive} from 'lit/directive.js'; -import {AsyncDirective} from 'lit/async-directive.js'; +import { directive } from 'lit/directive.js'; +import { AsyncDirective } from 'lit/async-directive.js'; -const {state, effect} = signal; +const { effect } = signal; class WatchDirective extends AsyncDirective { - #isWatching + #cancel: (() => void) | undefined = undefined; constructor(part: any) { super(part); - this.#isWatching = state(true); } override render(signal: any) { - effect(this.#isWatching, isWatching => { - if (isWatching) { - this.setValue(signal.get()); - } + this.#cancel?.(); + this.#cancel = effect(signal, value => { + this.setValue(value); }); return signal.get(); } - - protected override disconnected(): void { - this.#isWatching.send(false) - } - - protected override reconnected(): void { - this.#isWatching.send(true) - } } /** From 2937da21ff30208844c9cccfdd30707f5d50c60a Mon Sep 17 00:00:00 2001 From: Gordon Brander Date: Fri, 7 Jun 2024 17:30:15 -0700 Subject: [PATCH 3/4] Update effect to support multiple signals ...this is the common case, as with computed. --- .../packages/common-frp-lit/src/index.ts | 22 +++-- .../packages/common-frp/src/example/basic.ts | 2 +- typescript/packages/common-frp/src/signal.ts | 86 +++++++++++++++++-- 3 files changed, 99 insertions(+), 11 deletions(-) diff --git a/typescript/packages/common-frp-lit/src/index.ts b/typescript/packages/common-frp-lit/src/index.ts index aa6e1c742..db5b21486 100644 --- a/typescript/packages/common-frp-lit/src/index.ts +++ b/typescript/packages/common-frp-lit/src/index.ts @@ -1,23 +1,35 @@ import { signal } from "@commontools/common-frp"; -import { directive } from 'lit/directive.js'; -import { AsyncDirective } from 'lit/async-directive.js'; +import {directive} from 'lit/directive.js'; +import {AsyncDirective} from 'lit/async-directive.js'; -const { effect } = signal; +const {state, effect} = signal; class WatchDirective extends AsyncDirective { #cancel: (() => void) | undefined = undefined; + #isWatching constructor(part: any) { super(part); + this.#isWatching = state(true); } override render(signal: any) { this.#cancel?.(); - this.#cancel = effect(signal, value => { - this.setValue(value); + this.#cancel = effect([this.#isWatching, signal], (isWatching, value) => { + if (isWatching) { + this.setValue(value); + } }); return signal.get(); } + + protected override disconnected(): void { + this.#isWatching.send(false) + } + + protected override reconnected(): void { + this.#isWatching.send(true) + } } /** diff --git a/typescript/packages/common-frp/src/example/basic.ts b/typescript/packages/common-frp/src/example/basic.ts index 93b859b80..afda8f9e3 100644 --- a/typescript/packages/common-frp/src/example/basic.ts +++ b/typescript/packages/common-frp/src/example/basic.ts @@ -11,7 +11,7 @@ const clicks = events(button, 'click') const clickCount = scan(clicks, (state, _) => state + 1, 0) -effect(clickCount, x => { +effect([clickCount], x => { button.textContent = `Clicks: ${x}` console.log(x) }) diff --git a/typescript/packages/common-frp/src/signal.ts b/typescript/packages/common-frp/src/signal.ts index 5ed63d748..aa46b0f08 100644 --- a/typescript/packages/common-frp/src/signal.ts +++ b/typescript/packages/common-frp/src/signal.ts @@ -82,14 +82,90 @@ const sample = (container: Gettable) => container.get() export type Signal = Gettable & Updates & Cancellable export type SignalSubject = Gettable & Updates & Subject +export type Effect = { + ( + upstreams: [Signal], + perform: (a: A) => void + ): Cancel + + ( + upstreams: [Signal, Signal], + perform: (a: A, b: B) => void + ): Cancel + + ( + upstreams: [Signal, Signal, Signal], + perform: (a: A, b: B, c: C) => void + ): Cancel + + ( + upstreams: [Signal, Signal, Signal, Signal], + perform: (a: A, b: B, c: C, d: D) => void + ): Cancel + + ( + upstreams: [Signal, Signal, Signal, Signal, Signal], + perform: (a: A, b: B, c: C, d: D, e: E) => void + ): Cancel + + ( + upstreams: [ + Signal, + Signal, + Signal, + Signal, + Signal, + Signal + ], + perform: (a: A, b: B, c: C, d: D, e: E, f: F) => void + ): Cancel + + ( + upstreams: [ + Signal, + Signal, + Signal, + Signal, + Signal, + Signal, + Signal + ], + perform: (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => void + ): Cancel + + ( + upstreams: [ + Signal, + Signal, + Signal, + Signal, + Signal, + Signal, + Signal, + Signal + ], + perform: (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => void + ): Cancel + + ( + upstreams: Array>, + perform: (...values: Array) => void + ): Cancel +} + /** React to a signal, producing an effect any time it changes */ -export const effect = ( - signal: Signal, - effect: Send +export const effect: Effect = ( + upstreams: Array>, + perform: (...values: Array) => void ) => { - const job = () => effect(signal.get()) + const job = () => perform(...upstreams.map(sample)) + const schedule = () => withReads(job) + job() - return signal[__updates__](() => withReads(job)) + + return combineCancels( + upstreams.map(signal => signal[__updates__](schedule)) + ) } export const state = (initial: T) => { From 51d6859f2b17d1d1970b262c3a96c953d0ffdcca Mon Sep 17 00:00:00 2001 From: Gordon Brander Date: Fri, 7 Jun 2024 17:31:45 -0700 Subject: [PATCH 4/4] Semicolon --- typescript/packages/common-frp-lit/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript/packages/common-frp-lit/src/index.ts b/typescript/packages/common-frp-lit/src/index.ts index db5b21486..37e1de386 100644 --- a/typescript/packages/common-frp-lit/src/index.ts +++ b/typescript/packages/common-frp-lit/src/index.ts @@ -6,7 +6,7 @@ const {state, effect} = signal; class WatchDirective extends AsyncDirective { #cancel: (() => void) | undefined = undefined; - #isWatching + #isWatching; constructor(part: any) { super(part); @@ -24,11 +24,11 @@ class WatchDirective extends AsyncDirective { } protected override disconnected(): void { - this.#isWatching.send(false) + this.#isWatching.send(false); } protected override reconnected(): void { - this.#isWatching.send(true) + this.#isWatching.send(true); } } @@ -36,4 +36,4 @@ class WatchDirective extends AsyncDirective { * Renders a signal and subscribes to it, updating the part when the signal * changes. */ -export const watch = directive(WatchDirective) \ No newline at end of file +export const watch = directive(WatchDirective); \ No newline at end of file