Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Plugin System #119

Open
bowheart opened this issue Sep 10, 2024 · 0 comments
Open

New Plugin System #119

bowheart opened this issue Sep 10, 2024 · 0 comments
Labels
enhancement New feature or request
Milestone

Comments

@bowheart
Copy link
Collaborator

bowheart commented Sep 10, 2024

The current plan is to completely rework how plugins work in Zedux v2.

The Current Model

Plugins in Zedux v1 are purposefully verbose. You instantiate a class, set up your initial "mods", manually subscribe to the ecosystem's mod store with a verbose effects subscriber, and add ugly checks for mod action types.

const loggingPlugin = new ZeduxPlugin({
  initialMods: ['stateChanged'],

  registerEcosystem: ecosystem => {
    const subscription = ecosystem.modBus.subscribe({
      effects: ({ action }) => {
        if (action.type === ZeduxPlugin.actions.stateChanged.type) {
          console.log('node state updated', action.payload)
        }
      },
    })

    return () => subscription.unsubscribe()
  },
})

myEcosystem.registerPlugin(loggingPlugin)
myEcosystem.unregisterPlugin(loggingPlugin)

Fast forward a couple years. We now know what plugins actually need. We can make this a lot better.

The New Model

Plugins will simply be calls to ecosystem.on.

const cleanup = myEcosystem.on('change', reason => {
  console.log('node state updated', reason)
})

Wow such clean 🤩.

Plugins previously might have maintained internal state, turned on multiple mods, and/or returned a cleanup function. This can all be done in the new model. Conventionally, a "plugin" will now be a simple function that accepts the ecosystem and calls .on. It can instantiate and use atoms in the ecosystem (or its own internal ecosystem), store internal state, and clean it all up on destroy or reset:

const logAtom = atom('log', [])

const loggingPlugin = (ecosystem: Ecosystem) => {
  // other internal state can be tracked here:
  let internalState = []

  const cleanupChange = myEcosystem.on('change', reason => {
    ecosystem.getNode(logAtom).set(state => [...state, reason])
    internalState.push(reason)
  })

  // plugins can listen to `destroy` and/or `reset` to destroy internal state
  const cleanupDestroy = ecosystem.on('destroy', () => {
    cleanupChange()
    cleanupDestroy()
    ecosystem.getNode(logAtom).destroy(true)
    internalState = []
  })
}

loggingPlugin(myEcosystem)

Note that the destroy handler in this example is unnecessary - all four of its cleanup operations would happen automatically if the ecosystem is garbage collected. This is just for demonstration.

Communicating with Plugins

Just like with the new GraphNode#on model described in #115, Ecosystem#on will support custom events. This will allow you to send anything you want to plugins via Ecosystem#send.

myEcosystem.send('myCustomEvent', 'my custom payload type')

Ecosystems will get a new generic for event maps:

const myEcosystem = createEcosystem<undefined, {
  myCustomEvent: string
}>()

We'll also document how to create a typed useEcosystem hook and injectAtomGetters injector (or injectEcosystem if we switch to that) so all interactions with your ecosystem have event types intact.

The Plugin Events

The concept of "mods" shall die, replaced with "plugin events". Some of the mods will have event equivalents. Some will be removed completely:

  • ecosystemWiped - replaced with the reset event
  • edgeCreated - replaced with the edge event
  • edgeRemoved - replaced with the edge event
  • evaluationFinished - replaced with the runEnd event
  • instanceReused - removed. This was never good at what it does. Build or lint plugins should handle atom key uniqueness checking (or even generating).
  • stateChanged - replaced with the change event
  • statusChanged - replaced with the cycle event

The full list of built-in events is:

  • change - called on node state change
  • cycle - called on node lifecycleStatus change (when it becomes active, stale, or destroyed)
  • destroy - called on ecosystem destroy (cycle is used for node destruction)
  • edge - called when an edge is added, removed, or its flags are changed in the graph
  • error - called when a node evaluation errors or atom promise rejects
  • reset - called when the ecosystem is reset
  • runEnd - called when a node finishes evaluating
  • runStart - called when a node starts evaluating

Zedux will no longer track evaluation time internally (previously a feature of the evaluationFinished mod). Instead, use a combination of runStart and runEnd to track it yourself. This gives more control and allows us to remove one of Zedux's only two non-deterministic APIs (ecosystem._idGenerator.now) which will make it easier for users to test Zedux code (especially via snapshot testing).

Full Change List

These APIs will be removed:

  • Ecosystem#registerPlugin
  • Ecosystem#unregisterPlugin
  • Ecosystem#modBus
  • Ecosystem#_mods
  • The ZeduxPlugin class
  • The pluginActions object (accessed via the static ZeduxPlugin.actions property)

These APIs will be added:

  • Ecosystem#on
  • Ecosystem#send
  • A single-letter public property (name TBD) on Ecosystem that functions similarly to the _mods property, except it won't be initialized with all possible keys. Mods will instead add event keys to this object as needed and delete keys when the event has no more listeners. This object maps event keys to a Set of event listeners.

Additional Considerations

Instead of the new concept of a plugin being a simple function, we might want to export a new plugin factory for creating Plugins with event maps intact and add a plugins ecosystem config option to auto-type the ecosystem's event map. Besides automatic type inference, this would allow plugins to return a cleanup function again, removing the need for the new destroy event.

Theoretical futuristic code:

import { createEcosystem, type NodeFilter, plugin } from '@zedux/react'

const Placeholder = {} as any

const loggingPlugin = plugin(
  'logging',
  ecosystem => {
    ecosystem.on('change', reason => {
      console.log('node state updated', reason)
    })

    ecosystem.on('logAll', filterOptions => {
      console.log('all atoms:', ecosystem.findAll(filterOptions))
    })

    // can return cleanup function here (unnecessary in this case)
  },
  {
    logAll: Placeholder as NodeFilter,
  }
)

const ecosystem = createEcosystem('root', {
  plugins: [loggingPlugin],
})

ecosystem.send('logAll', { excludeFlags: ['disable-logging'] })
@bowheart bowheart added the enhancement New feature or request label Sep 10, 2024
@bowheart bowheart added this to the Zedux v2 milestone Sep 11, 2024
@bowheart bowheart mentioned this issue Sep 11, 2024
52 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant