Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 .changeset/rude-keys-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/async": patch
---

Initial patch release
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/set

## 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.
41 changes: 41 additions & 0 deletions packages/async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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.

<Callout type="caution">
Using `query` in `createResource` directly will not work since the fetcher is
not reactive. This means that it will not invalidate properly.
</Callout>

This is light wrapper over [`createResource`](/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](/solid-router/reference/data-apis/cache) helper.



```tsx title="component.tsx" {6,10}
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
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"
}
}
165 changes: 165 additions & 0 deletions packages/async/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* 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"
]
}
28 changes: 17 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.