effect-atom-tanstack-solid-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +76 -0
- package/dist/index.d.mts +27 -0
- package/dist/index.mjs +141 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 [these people](https://github.com/carloitaben/effect-atom-tanstack-router/graphs/contributors)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# effect-atom-tanstack-solid-router
|
|
2
|
+
|
|
3
|
+
TanStack Router SSR hydration integration for Effect Atom, mirroring `@tanstack/router-ssr-query-core`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
First, add `AtomRegistry` to your root route context.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/routes/__root.tsx
|
|
11
|
+
import { createRootRouteWithContext } from "@tanstack/solid-router"
|
|
12
|
+
import type { AtomRegistry } from "effect/unstable/reactivity/AtomRegistry"
|
|
13
|
+
|
|
14
|
+
export const Route = createRootRouteWithContext<{
|
|
15
|
+
registry: AtomRegistry
|
|
16
|
+
}>()({
|
|
17
|
+
// ...
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then, setup the atom registry and the integration.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/router.tsx
|
|
25
|
+
import { createRouter } from "@tanstack/solid-router"
|
|
26
|
+
import { setupRouterAtomIntegration } from "effect-atom-tanstack-solid-router"
|
|
27
|
+
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
28
|
+
|
|
29
|
+
export function getRouter() {
|
|
30
|
+
const registry = AtomRegistry.make({ scheduleTask })
|
|
31
|
+
|
|
32
|
+
const router = createRouter({
|
|
33
|
+
context: { registry },
|
|
34
|
+
// ...
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
setupRouterAtomIntegration({
|
|
38
|
+
router,
|
|
39
|
+
registry,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return router
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
There are two ways to interact with an atom, depending on whether the loader needs the data.
|
|
47
|
+
|
|
48
|
+
### Streaming
|
|
49
|
+
|
|
50
|
+
Kick off loading without waiting. The atom starts fetching on the server, then streams to the client while the component suspends on it. Use this when the loader itself doesn't need the value.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
loader: ({ context }) => {
|
|
54
|
+
context.registry.get(postsAtom)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Blocking
|
|
59
|
+
|
|
60
|
+
Await the atom and return data from the loader. Use this when the loader needs the value.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { awaitAtomSettled } from "effect-atom-tanstack-solid-router"
|
|
64
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
65
|
+
|
|
66
|
+
loader: async ({ params: { postId }, context }) => {
|
|
67
|
+
const data = await awaitAtomSettled(context.registry, postAtom(postId))
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
title: AsyncResult.getOrThrow(data).title,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> [!WARNING]
|
|
76
|
+
> Atoms must be marked `Atom.serializable()` and require `Atom.keepAlive` for them to be picked up by the integration.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
2
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
3
|
+
import { AnyRouter } from "@tanstack/solid-router";
|
|
4
|
+
import { AtomRegistry } from "effect/unstable/reactivity/AtomRegistry";
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
type DehydratedAtomEntry = {
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
};
|
|
10
|
+
type DehydratedRouterAtomState = {
|
|
11
|
+
atom: {
|
|
12
|
+
readonly initial?: Array<DehydratedAtomEntry>;
|
|
13
|
+
readonly stream: ReadableStream<Array<DehydratedAtomEntry>>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type RouterAtomIntegrationOptions<TRouter extends AnyRouter> = {
|
|
17
|
+
readonly router: TRouter;
|
|
18
|
+
readonly registry: AtomRegistry;
|
|
19
|
+
readonly wrapRegistry?: boolean;
|
|
20
|
+
};
|
|
21
|
+
declare function setupRouterAtomIntegration<TRouter extends AnyRouter>(opts: RouterAtomIntegrationOptions<TRouter>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Waits for an atom to leave `AsyncResult.Initial` before resolving.
|
|
24
|
+
*/
|
|
25
|
+
declare const awaitAtomSettled: <A, E>(registry: AtomRegistry, atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => Promise<AsyncResult.AsyncResult<A, E>>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { DehydratedRouterAtomState, RouterAtomIntegrationOptions, awaitAtomSettled, setupRouterAtomIntegration };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { RegistryContext } from "@effect/atom-solid";
|
|
2
|
+
import { isServer } from "@tanstack/router-core/isServer";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
5
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
6
|
+
import { createComponent } from "solid-js";
|
|
7
|
+
//#region src/index.ts
|
|
8
|
+
const isSettled = (value) => !AsyncResult.isAsyncResult(value) || !AsyncResult.isInitial(value);
|
|
9
|
+
const encode = (atom, value) => atom[Atom.SerializableTypeId].encode(value);
|
|
10
|
+
const keyOf = (atom) => atom[Atom.SerializableTypeId].key;
|
|
11
|
+
function setupRouterAtomIntegration(opts) {
|
|
12
|
+
const { router, registry } = opts;
|
|
13
|
+
if (opts.wrapRegistry !== false) {
|
|
14
|
+
const originalWrap = router.options.Wrap;
|
|
15
|
+
router.options.Wrap = (props) => createComponent(RegistryContext.Provider, {
|
|
16
|
+
value: registry,
|
|
17
|
+
get children() {
|
|
18
|
+
return originalWrap ? originalWrap(props) : props.children;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (isServer || router.isServer) {
|
|
23
|
+
const originalDehydrate = router.options.dehydrate;
|
|
24
|
+
router.options.dehydrate = async () => {
|
|
25
|
+
const originalDehydrated = await originalDehydrate?.();
|
|
26
|
+
const initial = [];
|
|
27
|
+
const sentKeys = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const node of registry.getNodes().values()) {
|
|
29
|
+
if (!Atom.isSerializable(node.atom)) continue;
|
|
30
|
+
const value = node.value();
|
|
31
|
+
if (!isSettled(value)) continue;
|
|
32
|
+
const key = keyOf(node.atom);
|
|
33
|
+
initial.push({
|
|
34
|
+
key,
|
|
35
|
+
value: encode(node.atom, value)
|
|
36
|
+
});
|
|
37
|
+
sentKeys.add(key);
|
|
38
|
+
}
|
|
39
|
+
let controller;
|
|
40
|
+
const stream = new ReadableStream({ start(c) {
|
|
41
|
+
controller = c;
|
|
42
|
+
} });
|
|
43
|
+
let closed = false;
|
|
44
|
+
let pending;
|
|
45
|
+
const flushPending = () => {
|
|
46
|
+
const batch = pending;
|
|
47
|
+
pending = void 0;
|
|
48
|
+
if (!batch || batch.size === 0) return;
|
|
49
|
+
try {
|
|
50
|
+
controller.enqueue(Array.from(batch.values()));
|
|
51
|
+
} catch {}
|
|
52
|
+
};
|
|
53
|
+
const schedule = (entry) => {
|
|
54
|
+
if (closed || sentKeys.has(entry.key)) return;
|
|
55
|
+
sentKeys.add(entry.key);
|
|
56
|
+
if (!pending) {
|
|
57
|
+
pending = /* @__PURE__ */ new Map();
|
|
58
|
+
queueMicrotask(flushPending);
|
|
59
|
+
}
|
|
60
|
+
pending.set(entry.key, entry);
|
|
61
|
+
};
|
|
62
|
+
const unsubscribes = /* @__PURE__ */ new Set();
|
|
63
|
+
const watch = (atom) => {
|
|
64
|
+
if (!Atom.isSerializable(atom) || sentKeys.has(keyOf(atom))) return;
|
|
65
|
+
const unsubscribe = registry.subscribe(atom, (value) => {
|
|
66
|
+
if (!isSettled(value)) return;
|
|
67
|
+
unsubscribe();
|
|
68
|
+
unsubscribes.delete(unsubscribe);
|
|
69
|
+
schedule({
|
|
70
|
+
key: keyOf(atom),
|
|
71
|
+
value: encode(atom, value)
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
unsubscribes.add(unsubscribe);
|
|
75
|
+
};
|
|
76
|
+
for (const node of registry.getNodes().values()) watch(node.atom);
|
|
77
|
+
const originalOnNodeAdded = registry.onNodeAdded;
|
|
78
|
+
registry.onNodeAdded = (node) => {
|
|
79
|
+
originalOnNodeAdded?.(node);
|
|
80
|
+
watch(node.atom);
|
|
81
|
+
};
|
|
82
|
+
const finalize = () => {
|
|
83
|
+
if (closed) return;
|
|
84
|
+
closed = true;
|
|
85
|
+
flushPending();
|
|
86
|
+
registry.onNodeAdded = originalOnNodeAdded;
|
|
87
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
88
|
+
unsubscribes.clear();
|
|
89
|
+
try {
|
|
90
|
+
controller.close();
|
|
91
|
+
} catch {}
|
|
92
|
+
};
|
|
93
|
+
Option.getOrThrowWith(Option.fromUndefinedOr(router.serverSsr), () => /* @__PURE__ */ new Error("Router SSR is unavailable. Call setupRouterAtomIntegration only after TanStack Start initializes server SSR.")).onRenderFinished(finalize);
|
|
94
|
+
router.serverSsrLifecycle = {
|
|
95
|
+
...router.serverSsrLifecycle,
|
|
96
|
+
onServerSsrAttach: [...router.serverSsrLifecycle?.onServerSsrAttach ?? [], (serverSsr) => serverSsr.onCleanup(finalize)]
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
...originalDehydrated,
|
|
100
|
+
atom: {
|
|
101
|
+
...initial.length > 0 && { initial },
|
|
102
|
+
stream
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const originalHydrate = router.options.hydrate;
|
|
109
|
+
router.options.hydrate = async (dehydrated) => {
|
|
110
|
+
await originalHydrate?.(dehydrated);
|
|
111
|
+
const atomState = dehydrated?.atom;
|
|
112
|
+
if (!atomState) return;
|
|
113
|
+
if (atomState.initial) for (const entry of atomState.initial) registry.setSerializable(entry.key, entry.value);
|
|
114
|
+
const reader = atomState.stream.getReader();
|
|
115
|
+
const pump = ({ done, value }) => {
|
|
116
|
+
if (done) return;
|
|
117
|
+
for (const entry of value) registry.setSerializable(entry.key, entry.value);
|
|
118
|
+
return reader.read().then(pump);
|
|
119
|
+
};
|
|
120
|
+
reader.read().then(pump).catch((error) => {
|
|
121
|
+
console.error("[atom-start-ssr] error reading atom stream:", error);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Waits for an atom to leave `AsyncResult.Initial` before resolving.
|
|
127
|
+
*/
|
|
128
|
+
const awaitAtomSettled = (registry, atom) => {
|
|
129
|
+
const current = registry.get(atom);
|
|
130
|
+
if (!AsyncResult.isInitial(current)) return Promise.resolve(current);
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const unsubscribe = registry.subscribe(atom, (value) => {
|
|
133
|
+
if (!AsyncResult.isInitial(value)) {
|
|
134
|
+
resolve(value);
|
|
135
|
+
unsubscribe();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
//#endregion
|
|
141
|
+
export { awaitAtomSettled, setupRouterAtomIntegration };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "effect-atom-tanstack-solid-router",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TanStack Router SSR hydration integration for Effect Atom",
|
|
5
|
+
"homepage": "https://github.com/carloitaben/effect-atom-tanstack-router#readme",
|
|
6
|
+
"bugs": "https://github.com/carloitaben/effect-atom-tanstack-router/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/carloitaben/effect-atom-tanstack-router.git"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"default": "./dist/index.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@effect/atom-solid": "4.0.0-rc.112",
|
|
28
|
+
"@effect/vitest": "4.0.0-rc.112",
|
|
29
|
+
"@tanstack/router-core": "^1.171.27",
|
|
30
|
+
"@tanstack/solid-router": "^1.170.30",
|
|
31
|
+
"@types/react": "^19.0.8",
|
|
32
|
+
"effect": "4.0.0-rc.112",
|
|
33
|
+
"solid-js": "^1.9.15",
|
|
34
|
+
"tsdown": "^0.22.14",
|
|
35
|
+
"typescript": "7.0.2",
|
|
36
|
+
"vitest": "^4.1.11"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@effect/atom-solid": ">=4.0.0-beta",
|
|
40
|
+
"@tanstack/router-core": "^1.171.27",
|
|
41
|
+
"@tanstack/solid-router": ">=1.170.30",
|
|
42
|
+
"effect": ">=4.0.0-beta",
|
|
43
|
+
"solid-js": ">=1.9.0 <2.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"test": "vitest",
|
|
48
|
+
"typecheck": "tsc"
|
|
49
|
+
}
|
|
50
|
+
}
|