0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-17 23:11:29 -05:00

WIP: adding an @astrojs/store package and useStore for framework integrations

This commit is contained in:
Tony Sullivan 2022-05-29 19:08:14 -05:00
parent 9b530bdece
commit ac4b16d7ec
17 changed files with 346 additions and 0 deletions

View file

@ -0,0 +1,36 @@
{
"name": "@astrojs/store",
"description": "Add framework-agnostic stores to your Astro projects",
"version": "0.0.1",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/astro-store"
},
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://astro.build",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000"
},
"devDependencies": {
"@types/chai": "^4.3.1",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.1.1",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"mocha": "^9.2.2"
}
}

View file

@ -0,0 +1,14 @@
const contexts = new Map();
export function getContext<T>(key: any): T {
return contexts.get(key);
}
export function setContext<T>(key: any, value: T): T {
contexts.set(key, value);
return value;
}
export function hasContext(key: any): boolean {
return contexts.has(key);
}

View file

@ -0,0 +1,2 @@
export * from './context';
export * from './store';

View file

@ -0,0 +1,158 @@
import { getStoreValue, isFunction, noop, runAll, safeNotEqual, subscribe } from './utils';
export type Subscriber<T> = (value: T) => void;
export type Unsubscriber = () => void;
export type Updater<T> = (value: T) => T;
export type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void;
type Invalidator<T> = (value?: T) => void;
export interface Readable<T> {
subscribe(this: void, run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber;
}
export interface Writable<T> extends Readable<T> {
set(this: void, value: T): void;
update(this: void, updater: Updater<T>): void;
}
type SubscribeInvalidateTuple<T> = [Subscriber<T>, Invalidator<T>];
const subscriberQueue: any[] = [];
export function readable<T>(value?: T, start?: StartStopNotifier<T>): Readable<T> {
return {
subscribe: writable(value, start).subscribe
}
}
export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writable<T> {
let stop: Unsubscriber | null = null;
const subscribers: Set<SubscribeInvalidateTuple<T>> = new Set();
function set(newValue: T): void {
if (safeNotEqual(value, newValue)) {
value = newValue;
if (stop !== null) { // store is ready
const runQueue = !subscriberQueue.length;
for (const subscriber of subscribers) {
subscriber[1]();
subscriberQueue.push(subscriber, value);
}
if (runQueue) {
for (let i = 0; i < subscriberQueue.length; i += 2) {
subscriberQueue[i][0](subscriberQueue[i + 1]);
}
subscriberQueue.length = 0;
}
}
}
}
function update(fn: Updater<T>): void {
set(fn(value!));
}
function subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop): Unsubscriber {
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
subscribers.add(subscriber);
if (subscribers.size === 1) {
stop = start(set) || noop;
}
run(value!);
return () => {
subscribers.delete(subscriber);
if (subscribers.size === 0) {
stop!();
stop = null;
}
}
}
return {
set,
update,
subscribe
};
}
type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>] | Array<Readable<any>>;
type StoresValues<T> = T extends Readable<infer U> ? U :
{ [K in keyof T]: T[K] extends Readable<infer U> ? U : never };
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
initialValue?: T
): Readable<T>;
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>) => T,
initialValue?: T
): Readable<T>;
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>) => T
): Readable<T>;
export function derived<T>(stores: Stores, fn: Function, initialValue?: T): Readable<T> {
const isSingle = !Array.isArray(stores);
const storesArray: Array<Readable<any>> = isSingle
? [stores as Readable<any>]
: stores as Array<Readable<any>>;
const auto = fn.length < 2;
return readable(initialValue, (set) => {
let initialized = false;
const values: any[] = [];
let pending = 0;
let cleanup = noop;
const sync = () => {
if (pending) {
return;
}
cleanup();
const result = fn(isSingle ? values[0] : values, set);
if (auto) {
set(result as T);
} else {
cleanup = isFunction(result) ? result as Unsubscriber : noop;
}
};
const unsubscribers = storesArray.map((store, i) => subscribe(
store,
(value: any) => {
values[i] = value;
pending &= ~(1 << i);
if (initialized) {
sync();
}
},
() => {
pending |= (1 << i);
}
));
initialized = true;
sync();
return function stop() {
runAll(unsubscribers);
cleanup();
};
});
}
export const get = getStoreValue;

View file

@ -0,0 +1,36 @@
import type { Readable } from "./store";
export function getStoreValue<T>(store: Readable<T>): T {
let value: T | undefined = undefined;
subscribe(store, (t: T) => value = t)();
return value!;
}
export function isFunction(thing: any): thing is Function {
return typeof thing === 'function';
}
export function noop() {}
export function run(fn: Function) {
fn();
}
export function runAll(fns: Function[]) {
fns.forEach(run);
}
export function safeNotEqual(a: any, b: any) {
return a != a
? b == b
: a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
export function subscribe<T>(store: Readable<T>, ...callbacks: any[]) {
if (store == null) {
return noop;
}
const result = (store.subscribe as any)(...callbacks);
return result.unsubscribe ? () => result.unsubscribe() : result;
}

View file

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"allowJs": true,
"module": "ES2020",
"outDir": "./dist",
"target": "ES2020"
}
}

View file

@ -34,6 +34,7 @@
"preact-render-to-string": "^5.2.0"
},
"devDependencies": {
"@astrojs/store": "workspace:*",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"preact": "^10.7.3"

View file

@ -0,0 +1,13 @@
import { useEffect, useState } from 'preact/hooks';
import { get } from '@astrojs/store';
import type { Readable } from '@astrojs/store';
export function useStore<T>(readable: Readable<T>) {
const [state, setState] = useState(get(readable));
useEffect(() => {
return readable.subscribe(setState);
}, []);
return state;
}

View file

@ -38,6 +38,7 @@
"devDependencies": {
"@types/react": "^17.0.45",
"@types/react-dom": "^17.0.17",
"@astrojs/store": "workspace:*",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"react": "^18.1.0",

View file

@ -0,0 +1,13 @@
import { useEffect, useState } from 'react';
import { get } from '@astrojs/store';
import type { Readable } from '@astrojs/store';
export function useStore<T>(readable: Readable<T>) {
const [state, setState] = useState(get(readable));
useEffect(() => {
return readable.subscribe(setState);
}, []);
return state;
}

View file

@ -34,6 +34,7 @@
"babel-preset-solid": "^1.4.2"
},
"devDependencies": {
"@astrojs/store": "workspace:*",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"solid-js": "^1.4.3"

View file

@ -0,0 +1,13 @@
import { createEffect, createSignal, onCleanup } from 'solid-js';
import { get } from '@astrojs/store';
import type { Readable } from '@astrojs/store';
export function useStore<T>(readable: Readable<T>) {
const [state, setState] = createSignal(get(readable));
createEffect(() => {
onCleanup(readable.subscribe(setState as (value: T) => void));
});
return state;
}

View file

@ -37,6 +37,7 @@
"vite": "^2.9.9"
},
"devDependencies": {
"@astrojs/store": "workspace:*",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"svelte": "^3.48.0"

View file

@ -0,0 +1,5 @@
import type { Readable } from '@astrojs/store';
export function useStore<T>(readable: Readable<T>) {
return readable;
}

View file

@ -35,6 +35,7 @@
"vite": "^2.9.9"
},
"devDependencies": {
"@astrojs/store": "workspace:*",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"vue": "^3.2.36"

View file

@ -0,0 +1,12 @@
import { getCurrentScope, onScopeDispose, readonly, shallowRef } from 'vue';
import { get } from '@astrojs/store';
import type { Readable } from '@astrojs/store';
export function useStore<T>(readable: Readable<T>) {
const state = shallowRef(get(readable));
const unsubscribe = readable.subscribe(value => state.value = value);
getCurrentScope() && onScopeDispose(unsubscribe);
return readonly(state);
}

29
pnpm-lock.yaml generated
View file

@ -659,6 +659,26 @@ importers:
chai-as-promised: 7.1.1_chai@4.3.6
mocha: 9.2.2
packages/astro-store:
specifiers:
'@types/chai': ^4.3.1
'@types/chai-as-promised': ^7.1.5
'@types/mocha': ^9.1.1
astro: workspace:*
astro-scripts: workspace:*
chai: ^4.3.6
chai-as-promised: ^7.1.1
mocha: ^9.2.2
devDependencies:
'@types/chai': 4.3.1
'@types/chai-as-promised': 7.1.5
'@types/mocha': 9.1.1
astro: link:../astro
astro-scripts: link:../../scripts
chai: 4.3.6
chai-as-promised: 7.1.1_chai@4.3.6
mocha: 9.2.2
packages/astro/e2e/fixtures/astro-component:
specifiers:
astro: workspace:*
@ -1788,6 +1808,7 @@ importers:
packages/integrations/preact:
specifiers:
'@astrojs/store': workspace:*
'@babel/plugin-transform-react-jsx': ^7.17.12
astro: workspace:*
astro-scripts: workspace:*
@ -1797,12 +1818,14 @@ importers:
'@babel/plugin-transform-react-jsx': 7.17.12
preact-render-to-string: 5.2.0_preact@10.7.3
devDependencies:
'@astrojs/store': link:../../astro-store
astro: link:../../astro
astro-scripts: link:../../../scripts
preact: 10.7.3
packages/integrations/react:
specifiers:
'@astrojs/store': workspace:*
'@babel/plugin-transform-react-jsx': ^7.17.12
'@types/react': ^17.0.45
'@types/react-dom': ^17.0.17
@ -1813,6 +1836,7 @@ importers:
dependencies:
'@babel/plugin-transform-react-jsx': 7.17.12
devDependencies:
'@astrojs/store': link:../../astro-store
'@types/react': 17.0.45
'@types/react-dom': 17.0.17
astro: link:../../astro
@ -1835,6 +1859,7 @@ importers:
packages/integrations/solid:
specifiers:
'@astrojs/store': workspace:*
astro: workspace:*
astro-scripts: workspace:*
babel-preset-solid: ^1.4.2
@ -1842,6 +1867,7 @@ importers:
dependencies:
babel-preset-solid: 1.4.2
devDependencies:
'@astrojs/store': link:../../astro-store
astro: link:../../astro
astro-scripts: link:../../../scripts
solid-js: 1.4.3
@ -1861,6 +1887,7 @@ importers:
svelte-preprocess: 4.10.7_xxnnhi7j46bwl35r5gwl6d4d6q
vite: 2.9.10
devDependencies:
'@astrojs/store': link:../../astro-store
astro: link:../../astro
astro-scripts: link:../../../scripts
svelte: 3.48.0
@ -1910,6 +1937,7 @@ importers:
packages/integrations/vue:
specifiers:
'@astrojs/store': workspace:*
'@vitejs/plugin-vue': ^2.3.3
astro: workspace:*
astro-scripts: workspace:*
@ -1919,6 +1947,7 @@ importers:
'@vitejs/plugin-vue': 2.3.3_vite@2.9.10+vue@3.2.37
vite: 2.9.10
devDependencies:
'@astrojs/store': link:../../astro-store
astro: link:../../astro
astro-scripts: link:../../../scripts
vue: 3.2.37