molszanski / iti

~1kB Dependency Injection Library for Typescript and React with a unique support of async flow
https://itijs.org
MIT License
129 stars 6 forks source link

feature: container disposing #21

Closed moltar closed 1 year ago

moltar commented 2 years ago

A useful feature is to be able to dispose of resources from the container.

E.g. disconnect from the databases and so on.

See how Awilix did it.

molszanski commented 2 years ago

@moltar how do you think the api should look like?

You can already do subscribe to upserts like:

let node = makeRoot()
  .add({
    userManual: "Please preheat before use",
    oven: () => new Oven(),
  })

node.on("containerUpdated", ({ key, newContainer }) => {})
node.on("containerUpserted", ({ key, newContainer }) => {
  if(key === "userManual"){
    console.log('we've updated our manual')
  }
})

node.upsert((containers, node) => ({
    userManual: "Works better when hot",
  }))

We can also dispose by

node.upsert((containers, node) => ({
    userManual: null,
  }))

I think I should document this better... Or add some helper functions to make it simpler?

moltar commented 2 years ago

We can also dispose by

But this solution won't await for resources to be freed up (e.g. database disconnected, file handle closed)

Also it feels very verbose.

A few DI frameworks have the concept of disposing or deconstructing, if you will.

moltar commented 2 years ago

I think the interface can be something like this:

class DatabaseConnection {
  disconnect(): Promise<void> {
    // ... disconnect
  }
}

let node = makeRoot()
  .add({
    db: () => new DatabaseConnection(),
  })
  .dispose({
    db: (containers) => containers.db.disconnect()
  })

// disposes of for all resources that have it defined
await node.dispose()

// under the hood (internals)
class Node {
  async dispose(): Promise<void> {
    await Promise.all( this.disposableResources.map((r) => r()) )
  }
}
moltar commented 2 years ago

Another option is to define a Symbol which would expose a disposable method on a resource.

const DISPOSE_METHOD = Symbol('dispose');

interface IDisposable {
  [DISPOSE_METHOD]: () => Promise<void>
}

class Database implements IDisposable {
  async [DISPOSE_METHOD](): Promise<void> {
    console.log('disconnecting')
  }
}
let db = new Database();

db[DISPOSE_METHOD]()

Playground Link


Internals

class Node {
  async dispose(): Promise<void> {
    await Promise.all( resources.filter((r) => !!resource[DISPOSE_METHOD]) )
  }
}
molszanski commented 2 years ago

@moltar thank you for the input 🙏

molszanski commented 2 years ago

Will update it soon. AFK

molszanski commented 1 year ago

Got back from vacations and crunch :) WIP: https://github.com/molszanski/iti/pull/24

molszanski commented 1 year ago

Hi @moltar. In the latest version 0.5.0 I've added support for disposing

basic usage

import { createContainer } from "iti"

container = createContainer()
  .add({ a: () => "123" })
  .addDisposer({ a: () => {} })

container.get("a") === "123" // true
await container.disposeAll() // Promise<void>

async case

const container = createContainer()
  .add(() => ({
    dbConnection: async () => {
      /** connect to db and return an instance */
    },
  }))
  .addDisposer({
    //              ↓ `db` is a resolved value of a `dbConnection` token. Pretty handy
    dbConnection: (db) => db.close(),
  })

const db = await container.get("dbConnection")
await container.disposeAll()

You can launch a live playground on Stackblitz or check the playground repo.

I've updated the docs website too: https://itijs.org/docs/api#disposing

Thank you for your suggestion! 😊 🚀

molszanski commented 1 year ago

@moltar I've tried many APIs and approaches and this seemed the most "clean" from a usage perspective.

molszanski commented 1 year ago

I've played with this solution with some code of my own. Feels good. I think we stick with this API and wait for more feedback. Will close the issue. Let me know if you think it should be improved.

Thank you for you input @moltar ❤️ 🔥 🚀