Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/async/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @solid-primitives/async

## 0.0.1

- Move from @solidjs/router
21 changes: 21 additions & 0 deletions packages/async/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions packages/async/README.md
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback="loading user...">
<p>{user()}</p>
</Suspense>
);
}
```

## 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());
```
50 changes: 50 additions & 0 deletions packages/async/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
175 changes: 175 additions & 0 deletions packages/async/src/index.ts
Original file line number Diff line number Diff line change
@@ -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> = {
(): T;
latest: T;
}

export function createAsync<T>(
fn: (prev: T) => Promise<T>,
options: {
name?: string;
initialValue: T;
deferStream?: boolean;
}
): AccessorWithLatest<T>;
export function createAsync<T>(
fn: (prev: T | undefined) => Promise<T>,
options?: {
name?: string;
initialValue?: T;
deferStream?: boolean;
}
): AccessorWithLatest<T | undefined>;
export function createAsync<T>(
fn: (prev: T | undefined) => Promise<T>,
options?: {
name?: string;
initialValue?: T;
deferStream?: boolean;
}
): AccessorWithLatest<T | undefined> {
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<T> = (() => resource()) as any;
Object.defineProperty(resultAccessor, 'latest', {
get() {
return (resource as any).latest;
}
})

return resultAccessor;
}

export function createAsyncStore<T>(
fn: (prev: T) => Promise<T>,
options: {
name?: string;
initialValue: T;
deferStream?: boolean;
reconcile?: ReconcileOptions;
}
): AccessorWithLatest<T>;
export function createAsyncStore<T>(
fn: (prev: T | undefined) => Promise<T>,
options?: {
name?: string;
initialValue?: T;
deferStream?: boolean;
reconcile?: ReconcileOptions;
}
): AccessorWithLatest<T | undefined>;
export function createAsyncStore<T>(
fn: (prev: T | undefined) => Promise<T>,
options: {
name?: string;
initialValue?: T;
deferStream?: boolean;
reconcile?: ReconcileOptions;
} = {}
): AccessorWithLatest<T | undefined> {
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<T> = (() => resource()) as any;
Object.defineProperty(resultAccessor, 'latest', {
get() {
return (resource as any).latest;
}
})

return resultAccessor;
}

function createDeepSignal<T>(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<T | null>, Setter<T | null>];
}

// 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<T>(fn: (prev: T | undefined) => Promise<T>, prev: T | undefined) {
if (isServer || !sharedConfig.context) return fn(prev);
Comment on lines +163 to +164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need some explanation on this one, it looks wild.

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;
}
}
12 changes: 12 additions & 0 deletions packages/async/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"references": [],
"include": [
"src"
]
}
Loading