diff --git a/packages/async/CHANGELOG.md b/packages/async/CHANGELOG.md new file mode 100644 index 000000000..21c6fa453 --- /dev/null +++ b/packages/async/CHANGELOG.md @@ -0,0 +1,5 @@ +# @solid-primitives/async + +## 0.0.1 + +- Move from @solidjs/router diff --git a/packages/async/LICENSE b/packages/async/LICENSE new file mode 100644 index 000000000..d0f4f2652 --- /dev/null +++ b/packages/async/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Solid Primitives Working Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/async/README.md b/packages/async/README.md new file mode 100644 index 000000000..5dede5a3d --- /dev/null +++ b/packages/async/README.md @@ -0,0 +1,50 @@ +# createAsync + +An asynchronous primitive with a function that tracks similar to `createMemo`. +`createAsync` expects a promise back that is then turned into a Signal. +Reading it before it is ready causes Suspense/Transitions to trigger. + +> [!WARNING] +> Using `query` in `createResource` directly will not work since the fetcher is +> not reactive. This means that it will not invalidate properly. + +This is light wrapper over [`createResource`](https://docs.solidjs.com/reference/basic-reactivity/create-resource) which serves as a stand-in for a future primitive being brought to Solid core in 2.0. +It is recommended that `createAsync` be used in favor of `createResource` specially when in a **SolidStart** app because `createAsync` works better in conjunction with the [cache](https://docs.solidjs.com/solid-router/reference/data-apis/cache) helper. + + + +```tsx +import { createAsync } from "@solid-primitives/async"; +import { Suspense } from "solid-js"; +import { getUser } from "./api"; + +export function Component () => { + const user = createAsync(() => getUser(params.id)); + + return ( + +

{user()}

+
+ ); +} +``` + +## Options + +| Name | Type | Default | Description | +| ------------ | ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | `undefined` | A name for the resource. This is used for debugging purposes. | +| deferStream | `boolean` | `false` | If true, Solid will wait for the resource to resolve before flushing the stream. | +| initialValue | `any` | `undefined` | The initial value of the resource. | +| onHydrated | `function` | `undefined` | A callback that is called when the resource is hydrated. | +| ssrLoadFrom | `"server" \| "initial"` | `"server"` | The source of the initial value for SSR. If set to `"initial"`, the resource will use the `initialValue` option instead of the value returned by the fetcher. | +| storage | `function` | `createSignal` | A function that returns a signal. This can be used to create a custom storage for the resource. This is still experimental + + +# createAsyncStore + +Similar to createAsync except it uses a deeply reactive store. Perfect for applying fine-grained changes to large model data that updates. + +```jsx +const todos = createAsyncStore(() => getTodos()); +``` diff --git a/packages/async/package.json b/packages/async/package.json new file mode 100644 index 000000000..b6b08d30b --- /dev/null +++ b/packages/async/package.json @@ -0,0 +1,50 @@ +{ + "name": "@solid-primitives/async", + "version": "0.0.1", + "description": "Primitives for async files.", + "license": "MIT", + "homepage": "https://primitives.solidjs.community/package/async", + "repository": { + "type": "git", + "url": "git+https://github.com/solidjs-community/solid-primitives.git" + }, + "primitive": { + "name": "async", + "stage": 0, + "list": [ + "createAsync" + ], + "category": "Reactivity" + }, + "files": [ + "dist" + ], + "private": false, + "sideEffects": false, + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "browser": {}, + "exports": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "scripts": { + "dev": "tsx ../../scripts/dev.ts", + "build": "tsx ../../scripts/build.ts" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + }, + "typesVersions": {}, + "devDependencies": { + "solid-js": "^1.8.7" + } +} diff --git a/packages/async/src/index.ts b/packages/async/src/index.ts new file mode 100644 index 000000000..e04beabc0 --- /dev/null +++ b/packages/async/src/index.ts @@ -0,0 +1,175 @@ +/* + +Primitive copied from @solidjs/router: https://github.com/solidjs/solid-router/blob/3c214ce2ceb9b7d9d39d143229a8c6145e83e681/src/data/createAsync.ts + +MIT License + +Copyright Ryan Carniato + +*/ + +/** + * This is mock of the eventual Solid 2.0 primitive. It is not fully featured. + */ +import { type Accessor, createResource, sharedConfig, type Setter, untrack } from "solid-js"; +import { createStore, reconcile, type ReconcileOptions, unwrap } from "solid-js/store"; +import { isServer } from "solid-js/web"; + +/** + * As `createAsync` and `createAsyncStore` are wrappers for `createResource`, + * this type allows to support `latest` field for these primitives. + * It will be removed in the future. + */ +export type AccessorWithLatest = { + (): T; + latest: T; +} + +export function createAsync( + fn: (prev: T) => Promise, + options: { + name?: string; + initialValue: T; + deferStream?: boolean; + } +): AccessorWithLatest; +export function createAsync( + fn: (prev: T | undefined) => Promise, + options?: { + name?: string; + initialValue?: T; + deferStream?: boolean; + } +): AccessorWithLatest; +export function createAsync( + fn: (prev: T | undefined) => Promise, + options?: { + name?: string; + initialValue?: T; + deferStream?: boolean; + } +): AccessorWithLatest { + let resource: () => T; + let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest; + [resource] = createResource( + () => subFetch(fn, untrack(prev)), + v => v, + options as any + ); + + const resultAccessor: AccessorWithLatest = (() => resource()) as any; + Object.defineProperty(resultAccessor, 'latest', { + get() { + return (resource as any).latest; + } + }) + + return resultAccessor; +} + +export function createAsyncStore( + fn: (prev: T) => Promise, + options: { + name?: string; + initialValue: T; + deferStream?: boolean; + reconcile?: ReconcileOptions; + } +): AccessorWithLatest; +export function createAsyncStore( + fn: (prev: T | undefined) => Promise, + options?: { + name?: string; + initialValue?: T; + deferStream?: boolean; + reconcile?: ReconcileOptions; + } +): AccessorWithLatest; +export function createAsyncStore( + fn: (prev: T | undefined) => Promise, + options: { + name?: string; + initialValue?: T; + deferStream?: boolean; + reconcile?: ReconcileOptions; + } = {} +): AccessorWithLatest { + let resource: () => T; + let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : unwrap((resource as any).latest); + [resource] = createResource( + () => subFetch(fn, untrack(prev)), + v => v, + { + ...options, + storage: (init: T | undefined) => createDeepSignal(init, options.reconcile) + } as any + ); + + const resultAccessor: AccessorWithLatest = (() => resource()) as any; + Object.defineProperty(resultAccessor, 'latest', { + get() { + return (resource as any).latest; + } + }) + + return resultAccessor; +} + +function createDeepSignal(value: T | undefined, options?: ReconcileOptions) { + const [store, setStore] = createStore({ + value: structuredClone(value) + }); + return [ + () => store.value, + (v: T) => { + typeof v === "function" && (v = v()); + setStore("value", reconcile(structuredClone(v), options)); + return store.value; + } + ] as [Accessor, Setter]; +} + +// mock promise while hydrating to prevent fetching +class MockPromise { + static all() { + return new MockPromise(); + } + static allSettled() { + return new MockPromise(); + } + static any() { + return new MockPromise(); + } + static race() { + return new MockPromise(); + } + static reject() { + return new MockPromise(); + } + static resolve() { + return new MockPromise(); + } + catch() { + return new MockPromise(); + } + then() { + return new MockPromise(); + } + finally() { + return new MockPromise(); + } +} + +function subFetch(fn: (prev: T | undefined) => Promise, prev: T | undefined) { + if (isServer || !sharedConfig.context) return fn(prev); + const ogFetch = fetch; + const ogPromise = Promise; + try { + window.fetch = () => new MockPromise() as any; + Promise = MockPromise as any; + return fn(prev); + } finally { + window.fetch = ogFetch; + Promise = ogPromise; + } +} diff --git a/packages/async/tsconfig.json b/packages/async/tsconfig.json new file mode 100644 index 000000000..38c71ce71 --- /dev/null +++ b/packages/async/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "dist", + "rootDir": "src" + }, + "references": [], + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f69ea2688..0f10bea4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5) vite-plugin-solid: specifier: ^2.10.2 - version: 2.10.2(solid-js@1.8.22)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) + version: 2.11.0(solid-js@1.8.22)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) vitest: specifier: ^2.1.0 version: 2.1.1(@types/node@22.5.4)(jsdom@25.0.0)(sass@1.77.8)(terser@5.31.5) @@ -106,6 +106,12 @@ importers: specifier: ^1.8.7 version: 1.8.20 + packages/async: + devDependencies: + solid-js: + specifier: ^1.8.7 + version: 1.8.22 + packages/audio: dependencies: '@solid-primitives/static-store': @@ -6271,12 +6277,12 @@ packages: '@nuxt/kit': optional: true - vite-plugin-solid@2.10.2: - resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} + vite-plugin-solid@2.11.0: + resolution: {integrity: sha512-G+NiwDj4EAeUE0wt3Ur9f+Lt9oMUuLd0FIxYuqwJSqRacKQRteCwUFzNy8zMEt88xWokngQhiFjfJMhjc1fDXw==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* solid-js: ^1.7.2 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: '@testing-library/jest-dom': optional: true @@ -6312,10 +6318,10 @@ packages: terser: optional: true - vitefu@0.2.5: - resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} + vitefu@1.0.4: + resolution: {integrity: sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: vite: optional: true @@ -8198,7 +8204,7 @@ snapshots: source-map-js: 1.2.0 terracotta: 1.0.5(solid-js@1.8.22) vite-plugin-inspect: 0.7.42(rollup@4.20.0)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) - vite-plugin-solid: 2.10.2(solid-js@1.8.22)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) + vite-plugin-solid: 2.11.0(solid-js@1.8.22)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) transitivePeerDependencies: - '@nuxt/kit' - '@testing-library/jest-dom' @@ -12458,7 +12464,7 @@ snapshots: - rollup - supports-color - vite-plugin-solid@2.10.2(solid-js@1.8.22)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)): + vite-plugin-solid@2.11.0(solid-js@1.8.22)(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)): dependencies: '@babel/core': 7.25.2 '@types/babel__core': 7.20.5 @@ -12467,7 +12473,7 @@ snapshots: solid-js: 1.8.22 solid-refresh: 0.6.3(solid-js@1.8.22) vite: 5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5) - vitefu: 0.2.5(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) + vitefu: 1.0.4(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)) transitivePeerDependencies: - supports-color @@ -12482,7 +12488,7 @@ snapshots: sass: 1.77.8 terser: 5.31.5 - vitefu@0.2.5(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)): + vitefu@1.0.4(vite@5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)): optionalDependencies: vite: 5.4.4(@types/node@22.5.4)(sass@1.77.8)(terser@5.31.5)