[meeting note] AFFiNE Infrastructure roadmap
Jotai Contxt + Module Scope
- Unidirectional data stream
- Context
SSR Support
const effects = collectEffects([...AllModules])
const state = runEffects(effects, context)
renderServerSideApp(state, context)
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
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 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.
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];
});
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
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.
Example code: