AFFiNE icon indicating copy to clipboard operation
AFFiNE copied to clipboard

[meeting note] AFFiNE Infrastructure roadmap

Open himself65 opened this issue 2 years ago • 5 comments

Jotai Contxt + Module Scope

  • Unidirectional data stream
  • Context

SSR Support

const effects = collectEffects([...AllModules])
const state = runEffects(effects, context)

renderServerSideApp(state, context)

himself65 avatar Apr 13 '23 04:04 himself65

Prototype 1

import { atom } from "jotai";

type FakeUser = {
  id: string
  name: string
  username: string
  email: string
  address: {
    street: string
    suite: string
    city: string
    zipcode: string
    geo: {
      lat: string
      lng: string
    }
  }
  phone: string
  website: string
  company: {
    name: string
    catchPhrase: string
    bs: string
  }
}

// primitive value
const userIdAtom = atom<string | null>(null)

// effect
const userAtom = atom<Promise<FakeUser | null>>(async (get) => {
  const id = get(userIdAtom)
  if (id === null) {
    return null
  } else {
    // we need a fetch atom here
    return await fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((r) => r.json())
  }
})

// dispatch
const changeUserAtom = atom(null, (get, set, id: string) => {
  set(userIdAtom, id)
})

Problem:

  • Need a fetch atom -> Need context
  • Need a way to wrap atoms -> Need module

himself65 avatar Apr 13 '23 05:04 himself65

Prototype 2

function createModule (
  name: string,
  createModule: (context: Context) => (
    Atom<unknown> |
    PrimitiveAtom<unknown> |
    WritableAtom<unknown, unknown[], unknown>)[]
) {
  // todo
}

createModule('user', (context) => {
  const fetch = context.store.get(context.atoms.fetchAtom)
  const userIdAtom = atom<string | null>(null);

  const userAtom = atom<Promise<FakeUser | null>>(async (get) => {
    const id = get(userIdAtom);
    if (id === null) {
      return null;
    } else {
      // we need a fetch atom here
      return await fetch(`https://jsonplaceholder.typicode.com/users/${id}`).
        then((r) => r.json());
    }
  });

  const changeUserAtom = atom(null, (get, set, id: string) => {
    set(userIdAtom, id);
  });

  return [userIdAtom, userAtom, changeUserAtom];
})

himself65 avatar Apr 13 '23 05:04 himself65

@Himself65 we have migrated from the roadmap label to using the GitHub milestone, since it appears on the GitHub project view. I'll attach this to current milestone, but we can reassign it to future ones.

doodlewind avatar Apr 13 '23 05:04 doodlewind

import type { Atom, PrimitiveAtom, WritableAtom } from "jotai";
import { atom, createStore } from "jotai";
type FakeUser = {
  id: string
  name: string
  username: string
  email: string
  address: {
    street: string
    suite: string
    city: string
    zipcode: string
    geo: {
      lat: string
      lng: string
    }
  }
  phone: string
  website: string
  company: {
    name: string
    catchPhrase: string
    bs: string
  }
}
type Getter = <Value>(atom: Atom<Value>) => Value;
type Setter = <Value, Args extends unknown[], Result>(
  atom: WritableAtom<Value, Args, Result>, ...args: Args) => Result;
type SetAtom<Args extends unknown[], Result> = <A extends Args>(...args: A) => Result;
type Read<Value, SetSelf = never> = (get: Getter, options: {
  readonly signal: AbortSignal;
  readonly setSelf: SetSelf;
}) => Value;
type Write<Args extends unknown[], Result> = (
  get: Getter, set: Setter, ...args: Args) => Result;
type WithInitialValue<Value> = {
  init: Value;
};
type OnUnmount = () => void;
type OnMount<Args extends unknown[], Result> = <S extends SetAtom<Args, Result>>(setAtom: S) => OnUnmount | void;

const vIsPrimitiveAtom = Symbol("isPrimitiveAtom");
const vIsEffectAtom = Symbol("isEffectAtom");
const vIsDispatchAtom = Symbol("isDispatchAtom");

type WithAffine<Atom extends object> = Atom & {
  affineTag: symbol
}

function createPrimitiveAtom<Value> (initialValue: Value): WithAffine<PrimitiveAtom<Value> & WithInitialValue<Value>> {
  const config = atom(initialValue);
  Object.defineProperty(config, "affineTag", {
    value: vIsPrimitiveAtom,
    writable: false
  });
  return config as any;
}

function createEffectAtom<Result = unknown> (read: Read<Promise<Result>>): WithAffine<Atom<Promise<Result>>> {
  const config = atom<Promise<Result>>(read);
  Object.defineProperty(config, "affineTag", {
    value: vIsEffectAtom,
    writable: false
  });
  return config as any;
}

function createDispatchAtom<Args extends unknown[], Result> (write: Write<Args, Result>): WithAffine<WritableAtom<null, Args, Result>> {
  const config = atom<null, Args, Result>(null, write);
  Object.defineProperty(config, "affineTag", {
    value: vIsDispatchAtom
  });
  return config as any;
}

interface Context {
  store: ReturnType<typeof createStore>,
  atoms: {
    fetchAtom: Atom<typeof fetch>
  }
}

const globalFetchAtom = atom(fetch);
const globalStore = createStore();

function createModule (
  name: string,
  createModule: (context: Context) => (
    WithAffine<Atom<unknown>> |
    WithAffine<PrimitiveAtom<unknown>> |
    WithAffine<WritableAtom<unknown, unknown[], unknown>>)[]
) {
  // todo
  const atoms = createModule({
    store: globalStore,
    atoms: {
      fetchAtom: globalFetchAtom
    }
  });
  atoms.forEach((atom) => {
    if (atom.affineTag === vIsPrimitiveAtom) {
      console.log("primitive atom", atom.debugLabel);
    } else if (atom.affineTag === vIsEffectAtom) {
      console.log("effect atom", atom.debugLabel);
    } else if (atom.affineTag === vIsDispatchAtom) {
      console.log("dispatch atom", atom.debugLabel);
    } else {
      throw new Error("unknown atom type");
    }
  });
}

createModule("user", (context) => {
  const fetch = context.store.get(context.atoms.fetchAtom);
  const userIdAtom = createPrimitiveAtom<string | null>(null);
  const userAtom = createEffectAtom<FakeUser | null>(async (get) => {
    const id = get(userIdAtom);
    if (id === null) {
      return null;
    } else {
      return (await fetch(`https://jsonplaceholder.typicode.com/users/${id}`).
        then((r) => r.json())) as FakeUser;
    }
  });
  const changeUserAtom = createDispatchAtom((get, set, id: string) => {
    set(userIdAtom, id);
  });
  return [userIdAtom, userAtom, changeUserAtom];
});

himself65 avatar Apr 13 '23 06:04 himself65

This has been delayed since we do not have many atoms to be a module, but we put most of the state into index.jotai.ts in each component

himself65 avatar Jul 01 '23 07:07 himself65

AFFiNE is a local first app so we won't add built-in SSR in the apps/core code.

However, We are going to use a separate SSR server like apps/share to only render the public workspace and share pages with 100% SSR/SEO support. This will make things much easier and feature optional.

himself65 avatar Aug 29 '23 23:08 himself65

Example code: 612c4f47-a2d9-4589-9a02-7a21cf43a2f9 img_v2_9ed0d9f3-50d7-4a58-948b-61ecb338248g

himself65 avatar Aug 29 '23 23:08 himself65