From 9a5a144625f4342876ddb6330e9cb08c12cf299d Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Thu, 30 May 2024 17:18:14 -0700
Subject: [PATCH 01/30] WIP
---
.../2024-05-30-common-frp/package-lock.json | 29 ++
sketches/2024-05-30-common-frp/package.json | 17 +
.../2024-05-30-common-frp/src/common-frp.ts | 308 ++++++++++++++++++
sketches/2024-05-30-common-frp/tsconfig.json | 27 ++
4 files changed, 381 insertions(+)
create mode 100644 sketches/2024-05-30-common-frp/package-lock.json
create mode 100644 sketches/2024-05-30-common-frp/package.json
create mode 100644 sketches/2024-05-30-common-frp/src/common-frp.ts
create mode 100644 sketches/2024-05-30-common-frp/tsconfig.json
diff --git a/sketches/2024-05-30-common-frp/package-lock.json b/sketches/2024-05-30-common-frp/package-lock.json
new file mode 100644
index 000000000..7a8b256e6
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/package-lock.json
@@ -0,0 +1,29 @@
+{
+ "name": "common-frp",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "common-frp",
+ "version": "0.0.1",
+ "license": "MIT",
+ "devDependencies": {
+ "typescript": "^5.4.5"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ }
+ }
+}
diff --git a/sketches/2024-05-30-common-frp/package.json b/sketches/2024-05-30-common-frp/package.json
new file mode 100644
index 000000000..84a32a4b3
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "common-frp",
+ "version": "0.0.1",
+ "description": "Common functional reactive programming utilities",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [
+ "frp"
+ ],
+ "author": "Gordon Brander",
+ "license": "MIT",
+ "devDependencies": {
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/sketches/2024-05-30-common-frp/src/common-frp.ts b/sketches/2024-05-30-common-frp/src/common-frp.ts
new file mode 100644
index 000000000..30b6ade09
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/src/common-frp.ts
@@ -0,0 +1,308 @@
+export const config = {
+ debug: false
+}
+
+const debugLog = (msg: string) => {
+ if (config.debug) {
+ console.debug(msg)
+ }
+}
+
+/**
+ * Pipe a value through a series of functions
+ * @param value - The value to pipe
+ * @param funcs - The functions to pipe the value through
+ * @returns The final value after being piped through all functions
+ */
+export const pipe = (
+ value: any,
+ ...funcs: Array<(value: any) => any>
+) => funcs.reduce((acc, func) => func(acc), value)
+
+export const chooseLeft = (left: T, right: T) => left
+
+const TransactionQueue = (name: string) => {
+ const queue = new Map<(value: any) => void, any>()
+
+ const transact = () => {
+ debugLog(`TransactionQueue ${name}: transacting ${queue.size} jobs`)
+ for (const [perform, value] of queue) {
+ perform(value)
+ }
+ queue.clear()
+ }
+
+ /**
+ * Queue a job to perform during the next transaction,
+ * along with a value to pass to the job
+ * @param perform - The function to perform (used as the unique key in
+ * the queue)
+ * @param value - The value to pass to the perform function
+ * @param choose - A function to choose between two values if more than one
+ * has been set during the transaction (default is chooseLeft)
+ */
+ const withTransaction = (
+ perform: (value: T) => void,
+ value: T,
+ choose: (left: T, right: T) => T = chooseLeft
+ ) => {
+ const left = queue.get(perform)
+ queue.set(perform, left !== undefined ? choose(left, value) : value)
+ }
+
+ return {transact, withTransaction}
+}
+
+const TransactionManager = () => {
+ const streamQueue = TransactionQueue('streams')
+ const cellQueue = TransactionQueue('cells')
+ const computedQueue = TransactionQueue('computed')
+
+ let isScheduled = false
+
+ const schedule = () => {
+ if (isScheduled) {
+ return
+ }
+ debugLog(`TransactionManager: transaction scheduled`)
+ isScheduled = true
+ queueMicrotask(transact)
+ }
+
+ const transact = () => {
+ debugLog(`TransactionManager: transaction start`)
+ debugLog(`TransactionManager: transact events`)
+ // First perform all events
+ streamQueue.transact()
+ debugLog(`TransactionManager: transact cells`)
+ // Then perform all cell updates
+ cellQueue.transact()
+
+ debugLog(`TransactionManager: transact computed`)
+ // Finally, update computed cells
+ computedQueue.transact()
+ isScheduled = false
+ debugLog(`TransactionManager: transaction end`)
+ }
+
+ const withCells = (
+ perform: (value: T) => void,
+ value: T,
+ choose: (left: T, right: T) => T = chooseLeft
+ ) => {
+ cellQueue.withTransaction(perform, value, choose)
+ schedule()
+ }
+
+ const withStreams = (
+ perform: (value: T) => void,
+ value: T,
+ choose: (left: T, right: T) => T = chooseLeft
+ ) => {
+ streamQueue.withTransaction(perform, value, choose)
+ schedule()
+ }
+
+ const withComputed = (
+ perform: (value: T) => void,
+ value: T,
+ choose: (left: T, right: T) => T = chooseLeft
+ ) => {
+ computedQueue.withTransaction(perform, value, choose)
+ schedule()
+ }
+
+ return {withCells, withStreams, withComputed}
+}
+
+const {withCells, withStreams, withComputed} = TransactionManager()
+
+type Topic = {
+ notify: (value: T) => void,
+ sink: (subscriber: (value: T) => void) => () => void
+}
+
+/**
+ * Create one-to-many event broadcast channel for a list of subscribers.
+ * Publishes synchronously to all subscribers.
+ */
+const Topic = (): Topic => {
+ const subscribers = new Set<(value: T) => void>()
+
+ const notify = (value: T) => {
+ for (const subscriber of subscribers) {
+ subscriber(value)
+ }
+ }
+
+ const sink = (subscriber: (value: T) => void) => {
+ subscribers.add(subscriber)
+ return () => subscribers.delete(subscriber)
+ }
+
+ return {notify, sink}
+}
+
+export type Send = (value: T) => void
+
+type Streamable = {
+ sink: (subscriber: (value: T) => void) => () => void
+}
+
+export class Stream implements Streamable {
+ #topic: Topic
+ #choose: (left: T, right: T) => T
+
+ constructor(
+ choose: (left: T, right: T) => T = chooseLeft
+ ) {
+ this.#topic = Topic()
+ this.#choose = choose
+ }
+
+ send(value: T) {
+ withStreams(this.#topic.notify, value, this.#choose)
+ }
+
+ sink(subscriber: (value: T) => void) {
+ return this.#topic.sink(subscriber)
+ }
+}
+
+export class ReadOnlyStream implements Streamable {
+ #stream: Streamable
+
+ constructor(stream: Stream) {
+ this.#stream = stream
+ }
+
+ sink(subscriber: (value: T) => void) {
+ return this.#stream.sink(subscriber)
+ }
+}
+
+export const useStream = (
+ generate: (send: (value: T) => void) => void,
+ choose: (left: T, right: T) => T = chooseLeft
+): Stream => {
+ const {notify, sink} = Topic()
+
+ const send = (value: T) => {
+ withStreams(notify, value, choose)
+ }
+
+ generate(send)
+
+ return {sink}
+}
+
+export const mapStream = (
+ stream: Stream,
+ map: (value: T) => U
+) => useStream(send => {
+ stream.sink(value => send(map(value)))
+})
+
+export const scanStream = (
+ stream: Stream,
+ step: (state: U, value: T) => U,
+ initial: U
+) => {
+ let state = initial
+ return useStream(send => {
+ stream.sink(value => {
+ state = step(state, value)
+ send(state)
+ })
+ })
+}
+
+export const mergeStreams = (
+ left: Stream,
+ right: Stream,
+ choose: (left: T, right: T) => T = chooseLeft
+) => useStream(send => {
+ left.sink(value => send(value))
+ right.sink(value => send(value))
+})
+
+/**
+ * "Hold" the latest value from a stream in a cell
+ * @param stream - the stream to update cell
+ * @param initial - the initial value for the cell
+ * @returns cell
+ */
+export const hold = (stream: Stream, initial: T): Cell => {
+ const [cell, send] = useCell(initial)
+ stream.sink(send)
+ return cell
+}
+
+
+const isEqual = Object.is
+
+export type Cell = {
+ get: () => T,
+ sink: (subscriber: (value: T) => void) => () => void
+}
+
+export const useCell = (value: T): [Cell, Send] => {
+ const {notify, sink} = Topic()
+ let state = value
+
+ const setState = (value: T) => {
+ // Only notify downstream if state has changed
+ if (!isEqual(state, value)) {
+ state = value
+ notify(value)
+ }
+ }
+
+ const get = () => state
+
+ const send = (value: T) => {
+ withCells(notify, value)
+ }
+
+ return [{get, sink}, send]
+}
+
+export const useComputed = (
+ upstream: Array>,
+ calc: () => T
+): Cell => {
+ const {notify, sink} = Topic()
+ let state = calc()
+ let isDirty = false
+
+ const setState = (value: T) => {
+ // Only notify downstream if state has changed
+ if (!isEqual(state, value)) {
+ state = value
+ notify(value)
+ }
+ }
+
+ const get = () => {
+ if (isDirty) {
+ state = calc()
+ isDirty = false
+ }
+ return state
+ }
+
+ const markDirty = () => {
+ isDirty = true
+ withComputed(notify, calc())
+ }
+
+ for (const cell of upstream) {
+ cell.sink(markDirty)
+ }
+
+ const send = (value: T) => {
+ withComputed(notify, value)
+ }
+
+ return {get, sink}
+}
\ No newline at end of file
diff --git a/sketches/2024-05-30-common-frp/tsconfig.json b/sketches/2024-05-30-common-frp/tsconfig.json
new file mode 100644
index 000000000..6cd254116
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es2017",
+ "module": "es2015",
+ "moduleResolution": "node",
+ "lib": ["esnext.array", "esnext", "es2017", "dom"],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "outDir": "./",
+ // Only necessary because @types/uglify-js can't find types for source-map
+ "skipLibCheck": true,
+ "experimentalDecorators": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": []
+ }
\ No newline at end of file
From 3d58db205f761647d10efb49f8cbabc1427c1ac3 Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Fri, 31 May 2024 11:01:30 -0700
Subject: [PATCH 02/30] More progress toward cell and stream types
- Convert to class style
- Four types: stream, readonly stream, cell, computedcell
---
.../2024-05-30-common-frp/src/common-frp.ts | 232 +++++++++---------
1 file changed, 115 insertions(+), 117 deletions(-)
diff --git a/sketches/2024-05-30-common-frp/src/common-frp.ts b/sketches/2024-05-30-common-frp/src/common-frp.ts
index 30b6ade09..1758ee3ad 100644
--- a/sketches/2024-05-30-common-frp/src/common-frp.ts
+++ b/sketches/2024-05-30-common-frp/src/common-frp.ts
@@ -8,28 +8,22 @@ const debugLog = (msg: string) => {
}
}
-/**
- * Pipe a value through a series of functions
- * @param value - The value to pipe
- * @param funcs - The functions to pipe the value through
- * @returns The final value after being piped through all functions
- */
-export const pipe = (
- value: any,
- ...funcs: Array<(value: any) => any>
-) => funcs.reduce((acc, func) => func(acc), value)
+export const chooseLeft = (left: T, _right: T) => left
-export const chooseLeft = (left: T, right: T) => left
+class TransactionQueue {
+ name: string
+ #queue = new Map<(value: any) => void, any>()
-const TransactionQueue = (name: string) => {
- const queue = new Map<(value: any) => void, any>()
+ constructor(name: string) {
+ this.name = name
+ }
- const transact = () => {
- debugLog(`TransactionQueue ${name}: transacting ${queue.size} jobs`)
- for (const [perform, value] of queue) {
+ transact() {
+ debugLog(`TransactionQueue ${this.name}: transacting ${this.#queue.size} jobs`)
+ for (const [perform, value] of this.#queue) {
perform(value)
}
- queue.clear()
+ this.#queue.clear()
}
/**
@@ -41,22 +35,20 @@ const TransactionQueue = (name: string) => {
* @param choose - A function to choose between two values if more than one
* has been set during the transaction (default is chooseLeft)
*/
- const withTransaction = (
+ withTransaction = (
perform: (value: T) => void,
value: T,
choose: (left: T, right: T) => T = chooseLeft
) => {
- const left = queue.get(perform)
- queue.set(perform, left !== undefined ? choose(left, value) : value)
+ const left = this.#queue.get(perform)
+ this.#queue.set(perform, left !== undefined ? choose(left, value) : value)
}
-
- return {transact, withTransaction}
}
const TransactionManager = () => {
- const streamQueue = TransactionQueue('streams')
- const cellQueue = TransactionQueue('cells')
- const computedQueue = TransactionQueue('computed')
+ const streamQueue = new TransactionQueue('streams')
+ const cellQueue = new TransactionQueue('cells')
+ const computedQueue = new TransactionQueue('computed')
let isScheduled = false
@@ -117,9 +109,12 @@ const TransactionManager = () => {
const {withCells, withStreams, withComputed} = TransactionManager()
+export type Send = (value: T) => void
+export type Cancel = () => void
+
type Topic = {
notify: (value: T) => void,
- sink: (subscriber: (value: T) => void) => () => void
+ sink: (subscriber: Send) => Cancel
}
/**
@@ -135,7 +130,7 @@ const Topic = (): Topic => {
}
}
- const sink = (subscriber: (value: T) => void) => {
+ const sink = (subscriber: Send): Cancel => {
subscribers.add(subscriber)
return () => subscribers.delete(subscriber)
}
@@ -143,7 +138,11 @@ const Topic = (): Topic => {
return {notify, sink}
}
-export type Send = (value: T) => void
+const combineCancels = (cancels: Array): Cancel => () => {
+ for (const cancel of cancels) {
+ cancel()
+ }
+}
type Streamable = {
sink: (subscriber: (value: T) => void) => () => void
@@ -182,127 +181,126 @@ export class ReadOnlyStream implements Streamable {
}
export const useStream = (
- generate: (send: (value: T) => void) => void,
- choose: (left: T, right: T) => T = chooseLeft
-): Stream => {
- const {notify, sink} = Topic()
-
- const send = (value: T) => {
- withStreams(notify, value, choose)
- }
-
- generate(send)
-
- return {sink}
+ produce: (send: Send) => void
+) => {
+ const downstream = new Stream()
+ produce(value => downstream.send(value))
+ return new ReadOnlyStream(downstream)
}
export const mapStream = (
- stream: Stream,
- map: (value: T) => U
+ upstream: Stream,
+ transform: (value: T) => U
) => useStream(send => {
- stream.sink(value => send(map(value)))
+ upstream.sink(value => send(transform(value)))
})
export const scanStream = (
- stream: Stream,
+ upstream: Stream,
step: (state: U, value: T) => U,
initial: U
-) => {
+) => useStream(send => {
let state = initial
- return useStream(send => {
- stream.sink(value => {
- state = step(state, value)
- send(state)
- })
+ upstream.sink(value => {
+ state = step(state, value)
+ send(state)
})
-}
-
-export const mergeStreams = (
- left: Stream,
- right: Stream,
- choose: (left: T, right: T) => T = chooseLeft
-) => useStream(send => {
- left.sink(value => send(value))
- right.sink(value => send(value))
})
-/**
- * "Hold" the latest value from a stream in a cell
- * @param stream - the stream to update cell
- * @param initial - the initial value for the cell
- * @returns cell
- */
-export const hold = (stream: Stream, initial: T): Cell => {
- const [cell, send] = useCell(initial)
- stream.sink(send)
- return cell
-}
-
-
const isEqual = Object.is
-export type Cell = {
- get: () => T,
+export type Cellable = {
+ readonly value: T,
sink: (subscriber: (value: T) => void) => () => void
}
-export const useCell = (value: T): [Cell, Send] => {
- const {notify, sink} = Topic()
- let state = value
+export class Cell implements Cellable {
+ #name: string
+ #value: T
+ #topic = Topic()
- const setState = (value: T) => {
- // Only notify downstream if state has changed
- if (!isEqual(state, value)) {
- state = value
- notify(value)
- }
+ constructor(value: T, name: string) {
+ this.#value = value
+ this.#name = name
}
- const get = () => state
+ get name() {
+ return this.#name
+ }
- const send = (value: T) => {
- withCells(notify, value)
+ get value() {
+ return this.#value
}
- return [{get, sink}, send]
-}
+ #setState = (value: T) => {
+ // Only notify downstream if state has changed value
+ if (!isEqual(this.#value, value)) {
+ this.#value = value
+ this.#topic.notify(value)
+ }
+ }
-export const useComputed = (
- upstream: Array>,
- calc: () => T
-): Cell => {
- const {notify, sink} = Topic()
- let state = calc()
- let isDirty = false
-
- const setState = (value: T) => {
- // Only notify downstream if state has changed
- if (!isEqual(state, value)) {
- state = value
- notify(value)
- }
+ send(value: T) {
+ withCells(this.#setState, value)
}
- const get = () => {
- if (isDirty) {
- state = calc()
- isDirty = false
- }
- return state
+ sink(subscriber: Send) {
+ subscriber(this.#value)
+ return this.#topic.sink(subscriber)
}
+}
- const markDirty = () => {
- isDirty = true
- withComputed(notify, calc())
+export const getCell = (cell: Cell) => cell.value
+
+export class ComputedCell implements Cellable {
+ #topic = Topic()
+ #isDirty = false
+ #value: T
+ #recalc: () => T
+ #upstreams: Array>
+ cancel: Cancel
+
+ constructor(
+ upstreams: Array>,
+ calc: (...values: Array) => T
+ ) {
+ this.#upstreams = upstreams
+ this.#recalc = () => calc(...this.#upstreams.map(getCell))
+ this.#value = this.#recalc()
+
+ this.cancel = combineCancels(
+ upstreams.map(cell => cell.sink(value => {
+ withComputed(this.#markDirty, value)
+ }))
+ )
}
- for (const cell of upstream) {
- cell.sink(markDirty)
+ #markDirty = () => {
+ this.#isDirty = true
}
- const send = (value: T) => {
- withComputed(notify, value)
+ get value() {
+ if (this.#isDirty) {
+ this.#value = this.#recalc()
+ this.#isDirty = false
+ }
+ return this.#value
}
- return {get, sink}
-}
\ No newline at end of file
+ sink(subscriber: Send): () => void {
+ return this.#topic.sink(subscriber)
+ }
+}
+
+/**
+ * "Hold" the latest value from a stream in a cell
+ * @param stream - the stream to update cell
+ * @param initial - the initial value for the cell
+ * @returns cell
+ */
+export const hold = (stream: Stream, initial: T): ComputedCell => {
+ const cell = new Cell(initial, 'hold')
+ // TODO deal with cancel
+ stream.sink(value => cell.send(value))
+ return new ComputedCell([cell], (value) => value)
+}
From 3d3f05332796eb1457b3b1eb8c2149797111a6b4 Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Fri, 31 May 2024 16:56:58 -0700
Subject: [PATCH 03/30] Add notes on FRP libs and papers
---
sketches/2024-05-30-common-frp/NOTES.md | 28 +++++++++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 sketches/2024-05-30-common-frp/NOTES.md
diff --git a/sketches/2024-05-30-common-frp/NOTES.md b/sketches/2024-05-30-common-frp/NOTES.md
new file mode 100644
index 000000000..355d44c0a
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/NOTES.md
@@ -0,0 +1,28 @@
+# Notes
+
+Rough notes and references while designing.
+
+## Prior art
+
+Classical FRP:
+
+- [Sodium FRP](https://github.com/SodiumFRP)
+ - Typescript https://github.com/SodiumFRP/sodium-typescript/tree/master/src/lib/sodium
+
+Signals:
+
+- https://github.com/tc39/proposal-signals
+- https://www.solidjs.com/
+- https://preactjs.com/guide/v10/signals/
+
+Observables:
+
+- https://rxjs.dev/
+- https://github.com/tc39/proposal-observable
+- https://developer.apple.com/documentation/combine/
+
+## Concepts
+
+- Blog: [The Evolution of Signals in Javascript](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob). Notes on low-level implementation of signal libraries, from the creator of SolidJS.
+- Blog: [INtorduction to fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) from the creator of SolidJS.
+- Paper: [Push-pull FRP](http://conal.net/papers/push-pull-frp/push-pull-frp.pdf), Conal Elliott
\ No newline at end of file
From 40414f0b47e0f44c568ba6a2630e1a1b3aa87b6d Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Fri, 31 May 2024 17:10:51 -0700
Subject: [PATCH 04/30] More notes
---
sketches/2024-05-30-common-frp/NOTES.md | 57 +++++++++++++++++++------
1 file changed, 45 insertions(+), 12 deletions(-)
diff --git a/sketches/2024-05-30-common-frp/NOTES.md b/sketches/2024-05-30-common-frp/NOTES.md
index 355d44c0a..63fe5ec6a 100644
--- a/sketches/2024-05-30-common-frp/NOTES.md
+++ b/sketches/2024-05-30-common-frp/NOTES.md
@@ -4,25 +4,58 @@ Rough notes and references while designing.
## Prior art
-Classical FRP:
+### Classical FRP
+
+Qualities:
+
+- Behaviors and events (also called cells and streams, or signals and streams)
+ - Behaviors are reactive containers for state (think spreadsheet cell)
+ - Events are streams of events over time
+ - Both are synchronized by a transaction system that makes sure changes happen during discrete "moments" in time, and inconsistent graph states are not observable.
+- Comes in discrete and continuous flavors. We only care about discrete for our purposes.
+- Resulting computation graph is pure.
+- Theory pioneered by Conal Elliott.
+- Full Turing-complete theory of reactive computation.
+
+Libraires:
- [Sodium FRP](https://github.com/SodiumFRP)
- - Typescript https://github.com/SodiumFRP/sodium-typescript/tree/master/src/lib/sodium
+ - [Sodium Typescript](https://github.com/SodiumFRP/sodium-typescript/tree/master/src/lib/sodium)
+
+### Signals
+
+Qualities:
+
+- Combines event streams and behaviors into a single concept.
+- Often uses single-transaction callback registration technique developed by S.js
+- Often uses push-pull FRP
+- Often uses closure and a "reactive scope" stack machine with a helper like `useEffect()` to register listeners
+
+Libraries:
+
+- [TC39 Signals Proposal](https://github.com/tc39/proposal-signals)
+- [SolidJS](https://www.solidjs.com/)
+- [Preact Signals](https://preactjs.com/guide/v10/signals/)
+- [S.js](https://github.com/adamhaile/S) implements transactions and dynamic graph with single-transaction callback registration, eliminating listener memory leaks.
+
+### Observables
-Signals:
+Qualities:
-- https://github.com/tc39/proposal-signals
-- https://www.solidjs.com/
-- https://preactjs.com/guide/v10/signals/
+- No transaction system
+- No state (streams only)
+- Largely static graphs (1st-order FRP) with explicit subscription cancellation for dynamic graphs
-Observables:
+Libraries:
-- https://rxjs.dev/
-- https://github.com/tc39/proposal-observable
-- https://developer.apple.com/documentation/combine/
+- [RxJS](https://rxjs.dev/)
+- [TC39 Observable Proposal](https://github.com/tc39/proposal-observable)
+- [Apple Combine](https://developer.apple.com/documentation/combine/)
## Concepts
+- Paper: [Push-pull FRP](http://conal.net/papers/push-pull-frp/push-pull-frp.pdf), Conal Elliott
+- Book: [Functional Reactive Programming](https://www.manning.com/books/functional-reactive-programming), Manning 2016
+- Talk: [Controlling space and time: understanding the many formulations of FRP](https://www.youtube.com/watch?v=Agu6jipKfYw), Evan Czaplicki
- Blog: [The Evolution of Signals in Javascript](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob). Notes on low-level implementation of signal libraries, from the creator of SolidJS.
-- Blog: [INtorduction to fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) from the creator of SolidJS.
-- Paper: [Push-pull FRP](http://conal.net/papers/push-pull-frp/push-pull-frp.pdf), Conal Elliott
\ No newline at end of file
+- Blog: [Introduction to fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) from the creator of SolidJS.
From 5d8a6799e9be5e36391a724b310ca5b6895ba649 Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Fri, 31 May 2024 17:17:03 -0700
Subject: [PATCH 05/30] More notes
---
sketches/2024-05-30-common-frp/NOTES.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/sketches/2024-05-30-common-frp/NOTES.md b/sketches/2024-05-30-common-frp/NOTES.md
index 63fe5ec6a..310566d51 100644
--- a/sketches/2024-05-30-common-frp/NOTES.md
+++ b/sketches/2024-05-30-common-frp/NOTES.md
@@ -37,6 +37,7 @@ Libraries:
- [SolidJS](https://www.solidjs.com/)
- [Preact Signals](https://preactjs.com/guide/v10/signals/)
- [S.js](https://github.com/adamhaile/S) implements transactions and dynamic graph with single-transaction callback registration, eliminating listener memory leaks.
+- [Elm Signals 3.0.0](https://github.com/elm-lang/core/blob/3.0.0/src/Native/Signal.js). Deprecated, but the implementation can be found here. 1st order FRP.
### Observables
@@ -59,3 +60,4 @@ Libraries:
- Talk: [Controlling space and time: understanding the many formulations of FRP](https://www.youtube.com/watch?v=Agu6jipKfYw), Evan Czaplicki
- Blog: [The Evolution of Signals in Javascript](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob). Notes on low-level implementation of signal libraries, from the creator of SolidJS.
- Blog: [Introduction to fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) from the creator of SolidJS.
+- GitHub: [General Theory of Reactivity](https://github.com/kriskowal/gtor)
From 2c9250855ac3cba6a686e0c47743a856f0ac8e51 Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Sat, 1 Jun 2024 13:30:16 -0700
Subject: [PATCH 06/30] More notes
---
sketches/2024-05-30-common-frp/NOTES.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/sketches/2024-05-30-common-frp/NOTES.md b/sketches/2024-05-30-common-frp/NOTES.md
index 310566d51..cbf4a1857 100644
--- a/sketches/2024-05-30-common-frp/NOTES.md
+++ b/sketches/2024-05-30-common-frp/NOTES.md
@@ -61,3 +61,14 @@ Libraries:
- Blog: [The Evolution of Signals in Javascript](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob). Notes on low-level implementation of signal libraries, from the creator of SolidJS.
- Blog: [Introduction to fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) from the creator of SolidJS.
- GitHub: [General Theory of Reactivity](https://github.com/kriskowal/gtor)
+
+### Cold vs. Hot Observables
+
+The distinction between cold and hot observables (or their equivalent) is a significant part of many FRP systems. Cold observables are those where the data-producing sequence starts anew for each subscriber, whereas hot observables share a single execution path among all subscribers. This distinction affects how data streams are multicast to multiple observers.
+
+- Cold observable: creates a data producer for each subscriber.
+ - The observable is a pure transformation over the data producer.
+ - Examples: [Reducers in Clojure](https://clojure.org/reference/reducers), [RxJS Observables](https://rxjs.dev).
+- Hot observable: Multicast. Maintains a list of subscribers and dispatches to them from a single data producing source.
+ - Examples: [share](https://rxjs.dev/api/index/function/share) in RxJS.
+ - Has to deal with callback cleanup bookkeeping in dynamic graphs.
From 6880092b86db5b4d97adbb2da035caa3dec939f0 Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Sat, 1 Jun 2024 22:38:51 -0700
Subject: [PATCH 07/30] Working on read-only signals and computed cells
The functioanl style of definition is definitely terser and more fluent.
Being able to destructure and restructure cells means we can offer
readonly without a special type.
---
sketches/2024-05-30-common-frp/README.md | 18 +
sketches/2024-05-30-common-frp/index.html | 11 +
.../2024-05-30-common-frp/package-lock.json | 788 +++++++++++++++++-
sketches/2024-05-30-common-frp/package.json | 8 +-
.../2024-05-30-common-frp/src/common-frp.ts | 272 +++---
.../src/example/basic.ts | 15 +
6 files changed, 959 insertions(+), 153 deletions(-)
create mode 100644 sketches/2024-05-30-common-frp/README.md
create mode 100644 sketches/2024-05-30-common-frp/index.html
create mode 100644 sketches/2024-05-30-common-frp/src/example/basic.ts
diff --git a/sketches/2024-05-30-common-frp/README.md b/sketches/2024-05-30-common-frp/README.md
new file mode 100644
index 000000000..568d17894
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/README.md
@@ -0,0 +1,18 @@
+# Common FRP
+
+Common functional reactive programming utilities.
+
+## Goals
+
+- [ ] Classical FRP with behaviors and events (also called cells and streams, or signals and streams)
+- [ ] 1st order FRP, static graph
+- [ ] Graph description can be serialized to JSON
+- [ ] Glitch free: updates happen during discrete moments using a transaction system
+- [ ] Cycles
+
+## Todo
+
+- [ ] Transactions
+- [ ] Transformations should happen during same transaction
+- [ ] Cell to stream happens one transaction later
+
diff --git a/sketches/2024-05-30-common-frp/index.html b/sketches/2024-05-30-common-frp/index.html
new file mode 100644
index 000000000..c2377acd1
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sketches/2024-05-30-common-frp/package-lock.json b/sketches/2024-05-30-common-frp/package-lock.json
index 7a8b256e6..fa314be5b 100644
--- a/sketches/2024-05-30-common-frp/package-lock.json
+++ b/sketches/2024-05-30-common-frp/package-lock.json
@@ -9,7 +9,738 @@
"version": "0.0.1",
"license": "MIT",
"devDependencies": {
- "typescript": "^5.4.5"
+ "typescript": "^5.4.5",
+ "vite": "^5.2.12"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
+ "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
+ "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
+ "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
+ "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
+ "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
+ "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
+ "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
+ "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
+ "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
+ "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
+ "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
+ "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
+ "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
+ "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
+ "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
+ "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
+ "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
+ "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
+ "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
+ "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
+ "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
+ "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz",
+ "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz",
+ "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz",
+ "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz",
+ "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz",
+ "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz",
+ "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz",
+ "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz",
+ "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz",
+ "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz",
+ "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
+ "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz",
+ "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz",
+ "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz",
+ "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz",
+ "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
+ "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.20.2",
+ "@esbuild/android-arm": "0.20.2",
+ "@esbuild/android-arm64": "0.20.2",
+ "@esbuild/android-x64": "0.20.2",
+ "@esbuild/darwin-arm64": "0.20.2",
+ "@esbuild/darwin-x64": "0.20.2",
+ "@esbuild/freebsd-arm64": "0.20.2",
+ "@esbuild/freebsd-x64": "0.20.2",
+ "@esbuild/linux-arm": "0.20.2",
+ "@esbuild/linux-arm64": "0.20.2",
+ "@esbuild/linux-ia32": "0.20.2",
+ "@esbuild/linux-loong64": "0.20.2",
+ "@esbuild/linux-mips64el": "0.20.2",
+ "@esbuild/linux-ppc64": "0.20.2",
+ "@esbuild/linux-riscv64": "0.20.2",
+ "@esbuild/linux-s390x": "0.20.2",
+ "@esbuild/linux-x64": "0.20.2",
+ "@esbuild/netbsd-x64": "0.20.2",
+ "@esbuild/openbsd-x64": "0.20.2",
+ "@esbuild/sunos-x64": "0.20.2",
+ "@esbuild/win32-arm64": "0.20.2",
+ "@esbuild/win32-ia32": "0.20.2",
+ "@esbuild/win32-x64": "0.20.2"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+ "dev": true
+ },
+ "node_modules/postcss": {
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
+ "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.5"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.18.0",
+ "@rollup/rollup-android-arm64": "4.18.0",
+ "@rollup/rollup-darwin-arm64": "4.18.0",
+ "@rollup/rollup-darwin-x64": "4.18.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.18.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.18.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.18.0",
+ "@rollup/rollup-linux-arm64-musl": "4.18.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.18.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.18.0",
+ "@rollup/rollup-linux-x64-gnu": "4.18.0",
+ "@rollup/rollup-linux-x64-musl": "4.18.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.18.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.18.0",
+ "@rollup/rollup-win32-x64-msvc": "4.18.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
}
},
"node_modules/typescript": {
@@ -24,6 +755,61 @@
"engines": {
"node": ">=14.17"
}
+ },
+ "node_modules/vite": {
+ "version": "5.2.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
+ "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.20.1",
+ "postcss": "^8.4.38",
+ "rollup": "^4.13.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/sketches/2024-05-30-common-frp/package.json b/sketches/2024-05-30-common-frp/package.json
index 84a32a4b3..825ac5858 100644
--- a/sketches/2024-05-30-common-frp/package.json
+++ b/sketches/2024-05-30-common-frp/package.json
@@ -4,7 +4,10 @@
"description": "Common functional reactive programming utilities",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "vite build",
+ "preview": "vite preview",
+ "dev": "vite"
},
"keywords": [
"frp"
@@ -12,6 +15,7 @@
"author": "Gordon Brander",
"license": "MIT",
"devDependencies": {
- "typescript": "^5.4.5"
+ "typescript": "^5.4.5",
+ "vite": "^5.2.12"
}
}
diff --git a/sketches/2024-05-30-common-frp/src/common-frp.ts b/sketches/2024-05-30-common-frp/src/common-frp.ts
index 1758ee3ad..fc1e76db3 100644
--- a/sketches/2024-05-30-common-frp/src/common-frp.ts
+++ b/sketches/2024-05-30-common-frp/src/common-frp.ts
@@ -2,28 +2,23 @@ export const config = {
debug: false
}
-const debugLog = (msg: string) => {
+const debugLog = (tag: string, msg: string) => {
if (config.debug) {
- console.debug(msg)
+ console.debug(`[${tag}] ${msg}`)
}
}
export const chooseLeft = (left: T, _right: T) => left
-class TransactionQueue {
- name: string
- #queue = new Map<(value: any) => void, any>()
+const TransactionQueue = (name: string) => {
+ const queue = new Map<(value: any) => void, any>()
- constructor(name: string) {
- this.name = name
- }
-
- transact() {
- debugLog(`TransactionQueue ${this.name}: transacting ${this.#queue.size} jobs`)
- for (const [perform, value] of this.#queue) {
+ const transact = () => {
+ debugLog(name, `transacting ${queue.size} jobs`)
+ for (const [perform, value] of queue) {
perform(value)
}
- this.#queue.clear()
+ queue.clear()
}
/**
@@ -35,20 +30,22 @@ class TransactionQueue {
* @param choose - A function to choose between two values if more than one
* has been set during the transaction (default is chooseLeft)
*/
- withTransaction = (
+ const withTransaction = (
perform: (value: T) => void,
value: T,
choose: (left: T, right: T) => T = chooseLeft
) => {
- const left = this.#queue.get(perform)
- this.#queue.set(perform, left !== undefined ? choose(left, value) : value)
+ const left = queue.get(perform)
+ queue.set(perform, left !== undefined ? choose(left, value) : value)
}
+
+ return {transact, withTransaction}
}
const TransactionManager = () => {
- const streamQueue = new TransactionQueue('streams')
- const cellQueue = new TransactionQueue('cells')
- const computedQueue = new TransactionQueue('computed')
+ const streamQueue = TransactionQueue('streams')
+ const cellQueue = TransactionQueue('cells')
+ const computedQueue = TransactionQueue('computed')
let isScheduled = false
@@ -56,25 +53,25 @@ const TransactionManager = () => {
if (isScheduled) {
return
}
- debugLog(`TransactionManager: transaction scheduled`)
+ debugLog('TransactionManager', `transaction scheduled`)
isScheduled = true
queueMicrotask(transact)
}
const transact = () => {
- debugLog(`TransactionManager: transaction start`)
- debugLog(`TransactionManager: transact events`)
+ debugLog('TransactionManager', 'transaction start')
+ debugLog('TransactionManager', 'transact events')
// First perform all events
streamQueue.transact()
- debugLog(`TransactionManager: transact cells`)
+ debugLog('TransactionManager', 'transact cells')
// Then perform all cell updates
cellQueue.transact()
- debugLog(`TransactionManager: transact computed`)
+ debugLog('TransactionManager', 'transact computed')
// Finally, update computed cells
computedQueue.transact()
isScheduled = false
- debugLog(`TransactionManager: transaction end`)
+ debugLog('TransactionManager', 'transaction end')
}
const withCells = (
@@ -82,6 +79,7 @@ const TransactionManager = () => {
value: T,
choose: (left: T, right: T) => T = chooseLeft
) => {
+ debugLog('withCells', 'queue job')
cellQueue.withTransaction(perform, value, choose)
schedule()
}
@@ -91,6 +89,7 @@ const TransactionManager = () => {
value: T,
choose: (left: T, right: T) => T = chooseLeft
) => {
+ debugLog('withStreams', 'queue job')
streamQueue.withTransaction(perform, value, choose)
schedule()
}
@@ -100,6 +99,7 @@ const TransactionManager = () => {
value: T,
choose: (left: T, right: T) => T = chooseLeft
) => {
+ debugLog('withComputed', 'queue job')
computedQueue.withTransaction(perform, value, choose)
schedule()
}
@@ -109,11 +109,13 @@ const TransactionManager = () => {
const {withCells, withStreams, withComputed} = TransactionManager()
-export type Send = (value: T) => void
export type Cancel = () => void
-type Topic = {
- notify: (value: T) => void,
+export type Send = {
+ send: (value: T) => void
+}
+
+export type Sink = {
sink: (subscriber: Send) => Cancel
}
@@ -121,12 +123,15 @@ type Topic = {
* Create one-to-many event broadcast channel for a list of subscribers.
* Publishes synchronously to all subscribers.
*/
-const Topic = (): Topic => {
- const subscribers = new Set<(value: T) => void>()
+const Topic = () => {
+ const subscribers = new Set>()
- const notify = (value: T) => {
+ /** Get subscribers */
+ const subs = () => subscribers.values()
+
+ const send = (value: T) => {
for (const subscriber of subscribers) {
- subscriber(value)
+ subscriber.send(value)
}
}
@@ -135,7 +140,7 @@ const Topic = (): Topic => {
return () => subscribers.delete(subscriber)
}
- return {notify, sink}
+ return {send, sink, subs}
}
const combineCancels = (cancels: Array): Cancel => () => {
@@ -144,152 +149,118 @@ const combineCancels = (cancels: Array): Cancel => () => {
}
}
-type Streamable = {
- sink: (subscriber: (value: T) => void) => () => void
+export const Stream = () => {
+ const topic = Topic()
+ const send = (value: T) => withStreams(topic.send, value)
+ const sink = (subscriber: Send) => topic.sink(subscriber)
+ return {send, sink}
}
-export class Stream implements Streamable {
- #topic: Topic
- #choose: (left: T, right: T) => T
-
- constructor(
- choose: (left: T, right: T) => T = chooseLeft
- ) {
- this.#topic = Topic()
- this.#choose = choose
- }
-
- send(value: T) {
- withStreams(this.#topic.notify, value, this.#choose)
- }
+const noOp = () => {}
- sink(subscriber: (value: T) => void) {
- return this.#topic.sink(subscriber)
- }
-}
-
-export class ReadOnlyStream implements Streamable {
- #stream: Streamable
-
- constructor(stream: Stream) {
- this.#stream = stream
- }
-
- sink(subscriber: (value: T) => void) {
- return this.#stream.sink(subscriber)
- }
-}
-
-export const useStream = (
- produce: (send: Send) => void
+/**
+ * Generate a stream using a callback.
+ * Returns a read-only stream.
+ */
+export const generateStream = (
+ produce: (send: (value: T) => void) => Cancel|void
) => {
- const downstream = new Stream()
- produce(value => downstream.send(value))
- return new ReadOnlyStream(downstream)
+ const {send, sink} = Stream()
+ const cancel = produce(send) ?? noOp
+ return {cancel, sink}
}
export const mapStream = (
- upstream: Stream,
+ upstream: Sink,
transform: (value: T) => U
-) => useStream(send => {
- upstream.sink(value => send(transform(value)))
+) => generateStream(send => {
+ return upstream.sink({
+ send: value => send(transform(value))
+ })
})
-export const scanStream = (
- upstream: Stream,
- step: (state: U, value: T) => U,
- initial: U
-) => useStream(send => {
- let state = initial
- upstream.sink(value => {
- state = step(state, value)
- send(state)
+export const filterStream = (
+ upstream: Sink,
+ predicate: (value: T) => boolean
+) => generateStream(send => {
+ return upstream.sink({
+ send: value => {
+ if (predicate(value)) {
+ send(value)
+ }
+ }
})
})
const isEqual = Object.is
-export type Cellable = {
- readonly value: T,
- sink: (subscriber: (value: T) => void) => () => void
+export type Gettable = {
+ get(): T
}
-export class Cell implements Cellable {
- #name: string
- #value: T
- #topic = Topic()
+export const get = (container: Gettable) => container.get()
- constructor(value: T, name: string) {
- this.#value = value
- this.#name = name
- }
+export type CellLike = Gettable & Sink
- get name() {
- return this.#name
- }
+export const Cell = (initial: T, name: string) => {
+ const topic = Topic()
+ let state = initial
- get value() {
- return this.#value
- }
+ const get = () => state
+ const getName = () => name
- #setState = (value: T) => {
- // Only notify downstream if state has changed value
- if (!isEqual(this.#value, value)) {
- this.#value = value
- this.#topic.notify(value)
- }
+ const setState = (value: T) => {
+ // Only notify downstream if state has changed value
+ if (!isEqual(state, value)) {
+ state = value
+ topic.send(value)
+ }
}
- send(value: T) {
- withCells(this.#setState, value)
+ const send = (value: T) => {
+ withCells(setState, value)
}
- sink(subscriber: Send) {
- subscriber(this.#value)
- return this.#topic.sink(subscriber)
+ const sink = (subscriber: Send) => {
+ subscriber.send(state)
+ return topic.sink(subscriber)
}
+
+ return {get, name: getName, send, sink}
}
-export const getCell = (cell: Cell) => cell.value
-
-export class ComputedCell implements Cellable {
- #topic = Topic()
- #isDirty = false
- #value: T
- #recalc: () => T
- #upstreams: Array>
- cancel: Cancel
-
- constructor(
- upstreams: Array| >,
- calc: (...values: Array) => T
- ) {
- this.#upstreams = upstreams
- this.#recalc = () => calc(...this.#upstreams.map(getCell))
- this.#value = this.#recalc()
-
- this.cancel = combineCancels(
- upstreams.map(cell => cell.sink(value => {
- withComputed(this.#markDirty, value)
- }))
- )
- }
+export const Computed = (
+ upstreams: Array>,
+ calc: (...values: Array) => T
+) => {
+ const topic = Topic()
- #markDirty = () => {
- this.#isDirty = true
- }
+ const recalc = (): T => calc(...upstreams.map(get))
+
+ let isDirty = false
+ let state = recalc()
+
+ const markDirty = () => isDirty = true
- get value() {
- if (this.#isDirty) {
- this.#value = this.#recalc()
- this.#isDirty = false
+ const subject = {
+ send: (value: T) => {
+ withComputed(markDirty, value)
}
- return this.#value
}
- sink(subscriber: Send): () => void {
- return this.#topic.sink(subscriber)
+ const cancel = combineCancels(upstreams.map(cell => cell.sink(subject)))
+
+ const get = () => {
+ if (isDirty) {
+ state = recalc()
+ isDirty = false
+ }
+ return state
}
+
+ const sink = (subscriber: Send) => topic.sink(subscriber)
+
+ return {get, sink, cancel}
}
/**
@@ -298,9 +269,10 @@ export class ComputedCell implements Cellable {
* @param initial - the initial value for the cell
* @returns cell
*/
-export const hold = (stream: Stream, initial: T): ComputedCell => {
- const cell = new Cell(initial, 'hold')
- // TODO deal with cancel
- stream.sink(value => cell.send(value))
- return new ComputedCell([cell], (value) => value)
+export const hold = (stream: Sink, initial: T) => {
+ const {get, sink, send} = Cell(initial, 'hold')
+ const cancel = stream.sink({
+ send: value => send(value)
+ })
+ return {get, sink, cancel}
}
diff --git a/sketches/2024-05-30-common-frp/src/example/basic.ts b/sketches/2024-05-30-common-frp/src/example/basic.ts
new file mode 100644
index 000000000..30cf09d8f
--- /dev/null
+++ b/sketches/2024-05-30-common-frp/src/example/basic.ts
@@ -0,0 +1,15 @@
+import {Cell, Computed, config} from '../common-frp'
+
+config.debug = true
+
+const a = Cell(0, 'foo')
+
+setInterval(() => {
+ a.send(a.get() + 1)
+}, 1000)
+
+const b = Computed([a], (a) => a + 1)
+
+b.sink({
+ send: (b) => console.log(b)
+})
\ No newline at end of file
From 1b3db0986ccc28f9b96970014994bc488d119d88 Mon Sep 17 00:00:00 2001
From: Gordon Brander
Date: Tue, 4 Jun 2024 09:01:19 -0700
Subject: [PATCH 08/30] Microtask queue-based topological sort
- Allows for glitch-free reactivity.
- Does not allow for cycles
- To allow cycles, we'll need to complete the implementation in
experimental
---
sketches/2024-05-30-common-frp/NOTES.md | 40 ++-
sketches/2024-05-30-common-frp/README.md | 20 +-
sketches/2024-05-30-common-frp/index.html | 1 +
.../2024-05-30-common-frp/src/common-frp.ts | 215 ++++---------
sketches/2024-05-30-common-frp/src/dom.ts | 9 +
.../src/example/basic.ts | 25 +-
.../experimental/common-frp-transaction.ts | 304 ++++++++++++++++++
7 files changed, 449 insertions(+), 165 deletions(-)
create mode 100644 sketches/2024-05-30-common-frp/src/dom.ts
create mode 100644 sketches/2024-05-30-common-frp/src/experimental/common-frp-transaction.ts
diff --git a/sketches/2024-05-30-common-frp/NOTES.md b/sketches/2024-05-30-common-frp/NOTES.md
index cbf4a1857..cc075baec 100644
--- a/sketches/2024-05-30-common-frp/NOTES.md
+++ b/sketches/2024-05-30-common-frp/NOTES.md
@@ -2,6 +2,29 @@
Rough notes and references while designing.
+## Design
+
+- Discrete classical FRP
+ - Streams
+ - Events over time. Update during a moment.
+ - Independent. They may not depend upon each other's state.
+ - They may depend upon cells, but must get the cell's state before
+ the cell graph is updated.
+ - They act as IO input to the cell graph.
+ - Cells
+ - Reactive containers for state. Always have a value.
+ - Computed Cells
+ - Reactive computed states, derived from cells and other computed cells.
+
+## Implementation
+
+- Transaction
+ - Update streams: Update streams:
+ - Update cells: mutate cell state and mark computed cells dirty (push)
+ - Dispatch "I am dirty" notification immediately during cell update phase to downstream cells
+ - Update sinks: get updated cell and computed state. Computed state is recomputed if dirty.
+ - Subscribe with sinks
+
## Prior art
### Classical FRP
@@ -16,8 +39,23 @@ Qualities:
- Resulting computation graph is pure.
- Theory pioneered by Conal Elliott.
- Full Turing-complete theory of reactive computation.
+- 10 primitives:
+ - map, merge, hold, snapshot, filter, lift, never, constant, sample, and switch. (Functional Reactive Programming, 2.3, Manning)
+
+> Each FRP system has its own policy for merging simultaneous events. Sodium’s policy is as follows:
+>
+> - If the input events on the two input streams are simultaneous, merge combines them into one. merge takes a combining function as a second argument for this purpose. The signature of the combining function is A combine(A left, A right).
+> -The combining function is not used in the (usually more common) case where the input events are not simultaneous.
+> -You invoke merge like this: s1.merge(s2, f). If merge needs to combine simul- taneous events, the event from s1 is passed as the left argument of the combin- ing function f, and the event from s2 is passed on the right.
+> -The s1.orElse(s2) variant of merge doesn’t take a combining function. In the simultaneous case, the left s1 event takes precedence and the right s2 event is dropped. This is equivalent to s1.merge(s2, (l, r) -> l). The name orElse() was chosen to remind you to be careful, because events can be dropped.
+>
+> This policy has some nice results:
+> - There can only ever be one event per transaction in a given stream.
+> - There’s no such thing as event-processing order within a transaction. All events that occur in different streams within the same transaction are truly simultaneous in that there’s no detectable order between them.
+>
+> (Functional Reactive Programming, 2.6.1, Manning)
-Libraires:
+Libraries:
- [Sodium FRP](https://github.com/SodiumFRP)
- [Sodium Typescript](https://github.com/SodiumFRP/sodium-typescript/tree/master/src/lib/sodium)
diff --git a/sketches/2024-05-30-common-frp/README.md b/sketches/2024-05-30-common-frp/README.md
index 568d17894..7b4626a68 100644
--- a/sketches/2024-05-30-common-frp/README.md
+++ b/sketches/2024-05-30-common-frp/README.md
@@ -4,15 +4,15 @@ Common functional reactive programming utilities.
## Goals
-- [ ] Classical FRP with behaviors and events (also called cells and streams, or signals and streams)
-- [ ] 1st order FRP, static graph
+- [x] Classical FRP with behaviors and events (also called cells and streams, or signals and streams)
+ - [x] Behaviors (called cells) are reactive containers for state that can be formalized as functions of time
+ - [x] Events (called streams) are streams of events over time
+- [x] 1st order FRP (static graph)
- [ ] Graph description can be serialized to JSON
-- [ ] Glitch free: updates happen during discrete moments using a transaction system
-- [ ] Cycles
-
-## Todo
-
+ - [x] All graph dependencies held in array (in contrast to S.js style or Signals style where graph is defined via VM execution trace)
+- [x] Glitch free: updates happen during discrete moments using a transaction system
+- [x] Transformations should happen during same transaction
- [ ] Transactions
-- [ ] Transformations should happen during same transaction
-- [ ] Cell to stream happens one transaction later
-
+ - [ ] (see experimental for WIP)
+- [ ] Cycles
+ - [ ] TODO: switch from microtask-based topological sorting to transaction-based push-pull (see experimental for WIP)
diff --git a/sketches/2024-05-30-common-frp/index.html b/sketches/2024-05-30-common-frp/index.html
index c2377acd1..4b276d716 100644
--- a/sketches/2024-05-30-common-frp/index.html
+++ b/sketches/2024-05-30-common-frp/index.html
@@ -7,5 +7,6 @@
+
| | | | | |