Giter VIP home page Giter VIP logo

signia's Introduction

tldraw

Welcome to the public monorepo for tldraw. tldraw is a library for creating infinite canvas experiences in React. It's the software behind the digital whiteboard tldraw.com.

๐Ÿคต Interested in purchasing a commercial license for the tldraw SDK? Fill out this form.

Installation

npm i tldraw

Usage

import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export default function App() {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw />
		</div>
	)
}

Learn more at tldraw.dev.

Local development

The local development server will run our examples app. The basic example will show any changes you've made to the codebase.

To run the local development server, first clone this repo.

Enable corepack to make sure you have the right version of yarn:

corepack enable

Install dependencies:

yarn

Start the local development server:

yarn dev

Open the example project at localhost:5420.

License

The tldraw source code and its distributions are provided under the tldraw license. This license does not permit commercial use. To purchase a commercial license or learn more, please fill out this form.

Trademarks

Copyright (c) 2023-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our trademark guidelines for info on acceptable usage.

Contact

Find us on Twitter/X at @tldraw.

Community

Have questions, comments or feedback? Join our discord or start a discussion. For the latest news and release notes, check out our Substack.

Contribution

Please see our contributing guide. Found a bug? Please submit an issue.

Contributors

Star History

Star History Chart

signia's People

Contributors

c01nd01r avatar ds300 avatar haveyaseen avatar icarusgk avatar mitjabezensek avatar somehats avatar steveruizok avatar susiwen8 avatar tomi-bigpi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

signia's Issues

Update atom state in reaction

Hi!
I've come across a situation where it's necessary to change the state of an atom within a reaction.

type AsyncResource = {
  hasError: Signal<boolean>;
  error: Signal<string>;
}

type NotifyStore = {
  messages: Signal<string[]>
  addMessage: (message) => void
}

const notify: NotifyStore = useNotify();
const asyncResource: AsyncResource = useAsyncResource(() => fetch('/error-response'));

useEffect(() => react('add message', () => {
  if (asyncResource.hasError.value) { 
    notify.addMessage(asyncResource.error.value)
  }
}), [])

When a reaction is triggered, the notify.messages atom gets updated, and execution fails with the error 'cannot change atoms during reaction cycle'.
From what I understand, this behavior corresponds to this test:

it('can not set atom values directly yet', () => {
const a = atom('', 1)
const r = reactor('', () => {
if (a.value < +Infinity) {
a.update((a) => a + 1)
}
})
expect(() => r.start()).toThrowErrorMatchingInlineSnapshot(
`"cannot change atoms during reaction cycle"`
)
})

Are there any workarounds for this situation?

Add Vue to the list of signals ๐Ÿ––๐Ÿผ

Hello Signia! ๐Ÿ‘‹๐Ÿผ

I noticed that Vue is not included in the list of frameworks that implement signals.

Vue also has a reactivity system based on reactive values with explicit wrappers, automatic dependency capturing, and a directed acyclic graph. Adding Vue to the list would make it more comprehensive and useful for readers who are interested in learning about different frameworks that implement signals.

Here is a reference to the Vue documentation, where you can find a detailed explanation of its Connection to Signals

More extensible effect notification

Currently when we end a transaction:

  1. we synchronously fan out and find all the reactor EffectScheduler instances that are out-of-date with the changed Atoms. We "notify" each EffectScheduler that it may need to change by calling reactor.maybeScheduleEffect()
  2. Inside EffectScheduler, maybeScheduleEffect causes us to synchronously re-compute its dependency parent Computeds.
  3. Finally, if any parent computed actually changed after recompute, then the EffectScheduler will schedule the effect for execution. The default "schedule" for effects to also run synchronously, but a user may provide a schedule function to defer it for later.

At Notion, we've noticed that recomputing Computed can take a substantial amount of event loop runtime, even if the result computation doesn't change. Because of this, we also schedule and throttle re-computation (step 2) using a queue and requestAnimationFrame. Would there be any correctness problem in deferring almost everything in maybeScheduleEffect() to an arbitrary later time, like we allow for maybeExecute()? I don't think that would cause any correctness issues.

The most naive change to allow consumers to defer step 2 would be to add scheduleRecompute?: (fn: () => void) => void option to EffectScheduler, and enqueue maybeScheduleEffect calls with that callback.. But I think there's a more interesting way to think about effects and scheduling, especially in light of the effort in #53.

The great thing about a logical clock derivation system is that reading a value is guaranteed to be consistent, no matter how much local or wall-clock time passes between the change of a state and when you perform a read. That means the change notification mechanism of Signia has no bearing on the correctness of Signia's computations.

Because of that property, I think it would be low-risk to move the notification behavior from a single traverse implementation in transaction.ts to a polymorphic method on Child called .notify(). The default implementation of that method on Computed and EffectScheduler would implement the same behavior of traverse as of today:

class _Computed {
	notify() {
    			if (this.lastTraversedEpoch === globalEpoch) {
				return
			}

			this.lastTraversedEpoch = globalEpoch
			this.children.visit(child => child.notify())
	}
}

class EffectScheduler {
	notify() {
			if (this.lastTraversedEpoch === globalEpoch) {
				return
			}

			this.lastTraversedEpoch = globalEpoch
			this.maybeScheduleEffect()
	}
}

By making notify polymorphic, we can implement subclasses of both Computed and EffectScheduler that can arbitrarily accelerate or defer both the scheduling and algorithm for notifying their subgraph. To enable communication between participants, we could add an argument to notify like notify(ChangeSet) or something that especially complex participants could use as a WeakMap key / token or something.

The "push" Computed can be implemebed as a subclass that redefines notify to immediately do node.__unsafe__getWithoutCapture() etc.

Warn or throw error when signal.value is read untracked during component render

Currently users can write a function component that looks like this, that appears to work on first render or if a parent subscribes to an atom, but actually isn't reactive to atom changes. The example comes from Discord:

const Prova = ({name}: {name: Atom<string>})=>{
  return <>
  <input type="text" value={name.value} onChange={(e)=>name.set(e.target.value)}/>
  <div>Your name is {name.value}</div>
  </>
}

Signia and/or Signia-react should either log a warning or throw an error if a component reads an atom during render in a non-reactive way. Starbeam implements this by checking React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner.current, which is only set when in development versions of React.

In Notion's internal state management system, we throw an error when this happens.

Set up package build script

TSC with multiple targets

  • esm
  • cjs
  • bundle?

inlcude src dir but not test files

include .d.ts

what do other similar projects output? preact/signals? jotai? recoil?

for our monorepo we probably want to install directly from github to get "main": "src/index.ts" working nicely, but no big deal if we consume the package directly since source mapping is not super valuable for tlstate code.

Add react bindings

tlstate/react

  • useComputed(derivation)
  • useComputed(name, deriving-fn)
  • useReaction
  • observer

tlstate/react-jsx-plugin

  • copy preact/signals' approach to automatically tracking .value calls.

Optimize flushChanges via WeakMap<Atom, ArraySet<EffectScheduler>>

This strategy comes from https://github.com/starbeamjs/starbeam

Currently when we write to an Atom, we recursively search the children to notify EffectScheduler subscribers. The problem with this kind of graph traversal in Javascript is that it's not very cache friendly. We can skip some work using the lastTraversedEpoch which effectively memoizes the traversal for already-notified subscribers. The work skipping helps in the best case, but doesn't reduce time spent in the worst case.

My proposed optimization is that we maintain a const ATOM_SUBSCRIBERS = new WeakMap<Atom, ArraySet<EffectScheduler>> which stores a direct mapping from mounted atoms to effects. If we can correctly maintain this set, the notification algorithm becomes strictly O(subscribers) instead of O(graph):

function flushChanges(atoms: Iterable<_Atom<any>>) {
  const reactors = new Set<EffectScheduler<unknown>>()
  for (const atom of atoms) {
    ATOM_SUBSCRIBERS.get(atom)?.forEach(effect => reactors.add(effect))
  }
  reactors.forEach(effect => r.maybeScheduleEffect())
} 

I haven't spent much time on the maintenance algorithm for ATOM_SUBSCRIBERS, but we should be able to do it in stopCapturingParents or similar by doing an upwards crawl of the parent. I don't recall how Starbeam implements it.

Maybe the maintenance time plus the storage space of any required memoize overwhelm the savings, but I think it could be a profitable optimization strategy.

Can't use signia-react-jsx jsxImportSource with Vite

Error:

 [vite] Internal server error: Failed to resolve import "signia-react-jsx/jsx-dev-runtime" from "src/main.tsx". Does the file exist?
  Plugin: vite:import-analysis
  File: /home/lippiece/projects/odin/odin-waldo/src/main.tsx:15:6
  1  |  "use strict";
  2  |  import { jsxDEV } from "signia-react-jsx/jsx-dev-runtime";
     |                          ^
  3  |  import { StrictMode } from "react";
  4  |  import ReactDOM from "react-dom/client";
      at formatError (file:///home/lippiece/projects/odin/odin-waldo/node_modules/vite/dist/node/chunks/dep-ca21228b.js:41418:46)
      at TransformContext.error (file:///home/lippiece/projects/odin/odin-waldo/node_modules/vite/dist/node/chunks/dep-ca21228b.js:41414:19)
      at normalizeUrl (file:///home/lippiece/projects/odin/odin-waldo/node_modules/vite/dist/node/chunks/dep-ca21228b.js:39706:33)
      at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
      at async TransformContext.transform (file:///home/lippiece/projects/odin/odin-waldo/node_modules/vite/dist/node/chunks/dep-ca21228b.js:39840:47)
      at async Object.transform (file:///home/lippiece/projects/odin/odin-waldo/node_modules/vite/dist/node/chunks/dep-ca21228b.js:41685:30)
      at async loadAndTransform (file:///home/lippiece/projects/odin/odin-waldo/node_modules/vite/dist/node/chunks/dep-ca21228b.js:39479:29)

Refine API + nomenclature

Initial thoughts

  • new Atom(name, val, opts?) => atom(name, val, opts?)
  • new Derivation(name, derive, opts?) => computed(name, val, opts)
  • @derivation => @computed(opts?)
  • Add @atom(opts?)
  • Replace useDeepComparisons with isEqual option, to avoid depending on lodash.isEqual (also prevents bundle bloat for folks using lodash already)
  • (unsure about this one) replace .get() and .set(val) with .value and .value = val, or just replace .get() and leave .set() alone.
  • diffSinceEpoch(epoch) => getDiffSince(epoch)
  • remove Atom#update, barely useful, a smaller api is more valuable.
  • remove buildIncrementalDerivation

`signia-react-jsx` doesn't work with lazy-loaded components in Vite/React

I can't get signia-react-jsx to work with my Vite/React setup when the component is lazy-loaded via React.lazy.

Minimal reproduction: https://github.com/simon-mathewson/signia-react-jsx-vite-lazy
If you run npm run dev here and click the count button, the count will not increase. If you comment out the lazy load and enable the regular import, it works.

In a different Vite project, I get this error:

Failed to resolve entry for package "signia-react-jsx". The package may have incorrect main/module/exports specified in its package.json.

However, I wasn't able to get this in the minimal reproduction project.

Multiple versions of signia detected.

I'm using signia in my Chrome Extension and my web application, then it throws this error, I wonder this is an expected behavior?
Those are running the same version of signia

Allow squashing history entries

rationale

When updating things in the store, we make potentially many changes to different atoms. our history atom is updated once per .put and once per .remove call, but if nobody reads the history during a transaction we may be creating more history entries than we need. They can be trivially squashed, but we don't currently know when it's safe to do so.

proposal

First let's define the notion of a 'fresh read': the first read to happen after an atom was last changed.

Now let's say we keep track of the lastFreshReadEpoch for each atom and derivation.

On every fresh read (after the initial one?), before exposing the state or history of the derivable, we can squash down any history events since the lastFreshReadEpoch.

impact

This would be a fairly minor perf improvement for very niche use cases. In some situations it may be a big perf win, but I'm struggling to think of what those cases may look like.

Bug in docs code example?

Playground (reproduction): https://codesandbox.io/s/sharp-marco-bcrrpj?file=/src/index.js

Source code (mostly taken from the docs):

import { Patch, produceWithPatches, enablePatches } from 'immer'
import { Atom, atom, computed, isUninitialized } from 'signia'
import { Draft } from 'immer'
import { RESET_VALUE, withDiff } from 'signia'

enablePatches()

class ImmerAtom {
    constructor(name, initialValue) {
        this.atom = atom(name, initialValue, {
            // In order to store diffs, we need to provide the `historyLength` argument
            // to the atom constructor. Otherwise it will not allocate a history buffer.
            historyLength: 10,
        })
    }

    update(fn) {
        const [nextValue, patches] = produceWithPatches(this.atom.value, fn)
        this.atom.set(nextValue, patches)
    }
}

let numMapCalls = 0;
function map(source, fn) {
    return computed(
        source.atom.name + ':mapped',
        (prev, lastComputedEpoch) => {
          numMapCalls++;
            // we need to check whether this is the first time we're running
            if (isUninitialized(prev)) {
                // if so, just map over the array and return it
                return source.atom.value.map(fn)
            }

            // this is not the first time we're running, so we need to calculate the diff of the source atom
            const diffs = source.atom.getDiffSince(lastComputedEpoch)
            console.log("diffs", diffs)
            // if there is not enough history to calculate the diff, this will be the RESET_VALUE constant
            if (diffs === RESET_VALUE) {
                // in which case we need to start over
                return source.atom.value.map(fn)
            }

            // we have diffs and a previous value
            const [next, patches] = produceWithPatches(prev, (draft) => {
                // apply the upstream diffs while generating a new set of downstream diffs
                for (const patch of diffs.flat()) {
                    const index = patch.path[0]
                    if (typeof index !== 'number') {
                        // this will be array length changes
                        draft[patch.path[0]] = patch.value
                        continue
                    }
                    if (patch.op === 'add') {
                        if (patch.path.length === 0) {
                            // this is a new item in the array, we need to splice it in and call the map function on it
                            draft.splice(patch.path[0] , 0, fn(patch.value))
                        } else {
                            // one of the existing items in the array has changed deeply
                            // let's call the map function on the new value
                            draft[index] = fn(source.atom.value[index]) 
                        }
                    } else if (patch.op === 'replace') {
                        // one of the existing items in the array has been fully replaced
                        draft[index] = fn(patch.value) 
                    } else if (patch.op === 'remove') {
                        next.splice(index, 1)
                    }
                }
            })

            // withDiff is a helper function that returns a special value that tells Signia to use the
            // provided value and diff
            return withDiff(next, patches)
        },
        {
            historyLength: 10,
        }
    )
}

const names = new ImmerAtom('names', ['Steve', 'Alex', 'Lu', 'Jamie', 'Mitja'])

let numReverseCalls = 0
const reversedNames = map(names, (name) => {
    numReverseCalls++
    return name.split('').reverse().join('')
})

console.log(reversedNames.value) // [ 'evetS', 'xelA', 'uL', 'eimaJ', 'ajtiM' ]
console.log(numReverseCalls, numMapCalls) // 5

names.update((draft) => {
  draft.push('David')
})

names.update((draft) => {
  draft[0] = 'Sunil'
})
names.update((draft) => {
  draft.pop()
})

console.log(reversedNames.value) // [ 'evetS', 'xelA', 'uL', 'eimaJ', 'ajtiM' ]
console.log(numReverseCalls, numMapCalls) // 5

Error message:

TypeError: Cannot read properties of undefined (reading 'split')
    at eval (https://bcrrpj.csb.app/src/index.js:110:15)

    at eval (https://bcrrpj.csb.app/src/index.js:80:32)

    at Immer.produce (https://bcrrpj.csb.app/node_modules/immer/dist/immer.cjs.development.js:805:20)

    at Immer.produceWithPatches (https://bcrrpj.csb.app/node_modules/immer/dist/immer.cjs.development.js:857:26)

    at _Computed.historyLength [as derive] (https://bcrrpj.csb.app/src/index.js:54:62)

    at _Computed.__unsafe__getWithoutCapture (https://bcrrpj.csb.app/node_modules/signia/dist/Computed.js:84:27)

    at get value [as value] (https://bcrrpj.csb.app/node_modules/signia/dist/Computed.js:105:24)

    at $csb$eval (https://bcrrpj.csb.app/src/index.js:124:27)

    at H (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:99925)

    at K.evaluate (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:112676)

    at ge.evaluateTranspiledModule (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:123058)

    at c (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:112291)

    at loadResources (https://bcrrpj.csb.app/index.html:3:2)

    at $csb$eval (https://bcrrpj.csb.app/index.html:9:3)

    at H (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:99925)

    at K.evaluate (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:112676)

    at ge.evaluateTranspiledModule (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:123058)

    at ge.evaluateModule (https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:122557)

    at https://codesandbox.io/static/js/sandbox.1ed00784d.js:1:319697

    at c (https://codesandbox.io/static/js/vendors~app~embed~sandbox~sandbox-startup.036d91db5.chunk.js:1:4333)

    at Generator._invoke (https://codesandbox.io/static/js/vendors~app~embed~sandbox~sandbox-startup.036d91db5.chunk.js:1:4086)

    at forEach.t.<computed> [as next] (https://codesandbox.io/static/js/vendors~app~embed~sandbox~sandbox-startup.036d91db5.chunk.js:1:4690)

    at e (https://codesandbox.io/static/js/vendors~app~embed~sandbox~sandbox-startup.036d91db5.chunk.js:1:206)

    at a (https://codesandbox.io/static/js/vendors~app~embed~sandbox~sandbox-startup.036d91db5.chunk.js:1:417)

Build fails in Vite

Here's a reproduction. I tried excluding signia-react-jsx/jsx-runtime and the build indeed passes, but then there's a remaining import in the final bundle that points to nowhere and it crashes.

https://codesandbox.io/p/sandbox/damp-sky-7kh23d

Also discovered while creating the repro, for some reason dev mode works on local but it fails in codesandbox.

This is in dev mode in codesandbox:
image

And when building I get (on my local):

RollupError: "jsx" is not exported by "../node_modules/.pnpm/[email protected][email protected]/node_modules/signia-react-jsx/jsx-runtime.mjs", imported by "src/components/layout/index.tsx

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.