flauwekeul / honeycomb

Create hex grids easily, in node or the browser.
https://abbekeultjes.nl/honeycomb
MIT License
630 stars 57 forks source link

4.0 - custom store? #85

Closed etodanik closed 2 years ago

etodanik commented 2 years ago

It could be idea to not work with a "built-in" store but rather to provide an adapter that satisfies a certain interface, with functions like getHex, e.t.c

In our example - we have an already existing state for the hex that travels through network, and it is an unnecessary chore to keep converting back / forth - and we don't wanna store our state on the honeycomb and have it manage that.

Would you consider detaching the state and allowing clients to provide it via adapters or something?

flauwekeul commented 2 years ago

I don't quite understand what you mean. First of all, which version are you using? In the latest alpha (v4.0.0-alpha.5) Grid indeed has a "store": a Map. In the latest version of the next branch, Grid also has a Map that stores the hexes.

In either version, Grid fulfils two functions: a container for hexes and methods "to do things with those hexes" (mainly iterating and querying). Do you want a way to "to do things with those hexes" but not be a container for those hexes? Could you share some code to clarify your request?

For what it's worth: Grid also has fromJSON() and toJSON() methods that should make it easier to (de)serialize a grid. Assuming that's what you mean by "that travels through network".

Finally, I'm about to merge another big change into the next branch. It should be the last big change for now. I want to focus on writing tests and documentation to get v4 released.

etodanik commented 2 years ago

Yes, I'm using your latest commit, always ;) I love danger. So when you see me talk about anything, it's always with the latest commits.

So, I think of it like the mental model of working with "history" in react-router or a store adapter in any caching library. In the 'history' example, you have a History API that has methods like pop(), push() and others. If you're prototyping you're probably gonna use a default browserHistory adapter, but say if you want to integrate your routing into a more complex model - you'd provide your own adapter that satisfies all these method signatures and do more advanced things with it like logging, synchronising your history state with the global app state, plugging routing into an event sourcing or redux model e.t.c

Same thing with cache, you normally start with something like memoryStore in most models, and then you'd replace it with your own - it could be a more advanced memoryStore, it could be something that later goes to redis, e.t.c e.t.c

With honeycomb - we already have a store for the hexes in Redux. That store could get manipulated by actions in a reducer.

Of course, yes - we could "live" with constantly serialising (toJSON()) and deserialising (fromJSON()) our data... Right now actually to avoid having this persistent store do createGrid() from scratch any time we want to work on it .... But a more elegant way feels like having something that we supply to createGrid() that has all the required methods (getHex(), e.t.c e.t.c) and we implement to conveniently and directly re-use the Redux global store to read from the grid, instead of having to constantly go back & forth.

By the way, I'm more than willing to create a PR for that and write tests e.t.c, if conceptually you're OK with the idea but don't have time / desire to implement it yourself just yet.

This pattern of working with a library (providing an adapter, with a simple very basic "in-memory" adapter provided by default) works really well for a lot of libs that are very heavily used and has always been very good DX.

flauwekeul commented 2 years ago

Yes, I'm using your latest commit, always ;) I love danger. So when you see me talk about anything, it's always with the latest commits.

😄 Great!

I see what you mean. Wouldn't extending Grid be what you need? Something like this:

// this would be your custom store
interface Store<T extends Hex> {
  hexes: IterableIterator<T>
  getSize(): number
  find(coordinates: HexCoordinates): T | undefined
}

class CustomGrid<T extends Hex> extends Grid<T> {
  get size(): number {
    return this.#store.getSize()
  }

  [Symbol.iterator]() {
    return this.#store.hexes
  }

  #store: Store<T>

  constructor(hexClass: HexConstructor<T>, store: Store<T>) {
    super(hexClass)
    this.#store = store
  }

  getHex(coordinates: HexCoordinates): T | undefined {
    return this.#store.find(coordinates)
  }
}

I could make an interface to make it more clear what properties your CustomGrid (which doesn't have to be a class) should implement. Something like this:

// there should probably be more properties than these:
interface HexStore<T extends Hex> {
  readonly size: number
  [Symbol.iterator](): IterableIterator<T>
  getHex(coordinates: HexCoordinates): T | undefined
}

class CustomGrid<T extends Hex> implements HexStore<T> {
  // ...
}
// or:
function createGrid<T extends Hex>(store): HexStore<T> {
  // ...
}

Would that meet your needs?

etodanik commented 2 years ago

Yup, this works! An interface + a "blessed" way to do it would be exactly the answer.

flauwekeul commented 2 years ago

I added these interfaces. Please let me know if this works for you.

flauwekeul commented 2 years ago

:tada: This issue has been resolved in version 4.0.0-beta.1 :tada:

The release is available on:

Your semantic-release bot :package::rocket: