@octanejs/jotai 0.1.48 → 0.1.50

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Poimandres
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @octanejs/jotai
2
2
 
3
- [jotai](https://github.com/pmndrs/jotai) for the [octane](https://github.com/octanejs/octane) UI framework.
3
+ Jotai 3 atoms and stores for Octane. The package imports Jotai's vanilla core and ports its React hooks and provider to Octane.
4
4
 
5
5
  ## Installation
6
6
 
@@ -9,89 +9,48 @@ npm install @octanejs/jotai
9
9
  pnpm add @octanejs/jotai
10
10
  ```
11
11
 
12
- jotai separates a framework-agnostic **vanilla core** (`atom`, `createStore`,
13
- `getDefaultStore` + all of `vanilla/utils`) from a small **React binding**
14
- (`Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom`). This package
15
- reuses the vanilla core unchanged (re-exported verbatim from `jotai/vanilla`) and
16
- reimplements only the binding on octane's hooks — deliberately preserving
17
- upstream's implementation shape (a force-update `useReducer` + effect
18
- subscription, not `useSyncExternalStore`), so re-render behavior matches jotai on
19
- React. The public surface matches jotai 1:1 — existing jotai code works by
20
- changing the import.
21
-
22
12
  ```tsx
23
- // before
24
- import { atom, useAtom } from 'jotai';
25
- // after
26
13
  import { atom, useAtom } from '@octanejs/jotai';
27
14
 
28
15
  const countAtom = atom(0);
29
16
 
30
17
  function Counter() @{
31
18
  const [count, setCount] = useAtom(countAtom);
32
- <button onClick={() => setCount((c) => c + 1)}>count is {count as string}</button>
19
+ <button onClick={() => setCount((value) => value + 1)}>{count as string}</button>
33
20
  }
34
21
  ```
35
22
 
36
23
  ## Entry points
37
24
 
38
- | import | what you get | notes |
39
- | --- | --- | --- |
40
- | `@octanejs/jotai` | `atom`, `createStore`, `getDefaultStore`, `Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom` | vanilla verbatim + the octane-bound binding |
41
- | `@octanejs/jotai/vanilla` | `atom`, `createStore`, `getDefaultStore` + types | re-exported verbatim from jotai |
42
- | `@octanejs/jotai/vanilla/utils` | `RESET`, `atomWithReset`, `atomWithStorage`, `atomWithReducer`, `atomFamily`, `selectAtom`, `splitAtom`, `loadable`, `unwrap`, … | re-exported verbatim (all framework-agnostic) |
43
- | `@octanejs/jotai/vanilla/internals` | `INTERNAL_*` store building blocks | re-exported verbatim; unstable by upstream contract |
44
- | `@octanejs/jotai/react` | `Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom` | the binding, ported to octane hooks |
45
- | `@octanejs/jotai/react/utils` | `useResetAtom`, `useAtomCallback`, `useHydrateAtoms`, `useReducerAtom` | ported to octane hooks |
46
- | `@octanejs/jotai/utils` | everything from `vanilla/utils` + `react/utils` | mirror of `jotai/utils` |
47
-
48
- `jotai/babel/*` (React-specific compile-time plugins) is not shipped.
49
-
50
- ## How it works
51
-
52
- octane keys hooks by a compiler-injected per-call-site `Symbol`, appended as the
53
- last argument of every `use*` call. The hooks here **forward** that slot to the
54
- base hooks they compose (deriving a stable sub-slot per composed base hook), so
55
- `useAtom(a)` and `useAtom(b)` in one component — or the same atom used twice —
56
- stay independent, exactly like distinct call sites in React.
57
-
58
- The binding is a line-for-line port of `jotai/react`: a reader holds a
59
- `[value, store, atom]` tuple in a force-update reducer and subscribes to the
60
- store in an effect. That means the same observable behavior as jotai on React,
61
- including:
62
-
63
- - **`useSetAtom` never re-renders the writer.** A component that only writes an
64
- atom doesn't subscribe to it.
65
- - **Derived atoms bail out.** A dependency write that recomputes to an
66
- `Object.is`-equal value never notifies readers.
67
- - **Readers mount with two renders** (the subscription effect re-checks the
68
- value after subscribing) — same as upstream.
69
-
70
- ## Async atoms + Suspense
71
-
72
- An atom whose value is a promise suspends the reader through octane's `use()`
73
- (React-19 parity) on jotai's identity-stable *continuable promise*. Use a
74
- suspense boundary (`@try { } @pending { } @catch (e) { }` or `<Suspense>`), or
75
- skip suspending entirely with the vanilla `loadable`/`unwrap` escape hatches:
25
+ | Import | Surface |
26
+ | --- | --- |
27
+ | `@octanejs/jotai` | Vanilla atoms/stores, `Provider`, `useStore`, `useAtom`, `useSetAtom`, `useAtomValue`, `useAtomValueRaw`, `useAtomValueRawSync` |
28
+ | `@octanejs/jotai/react` | Provider and hooks |
29
+ | `@octanejs/jotai/react/utils` | `useResetAtom`, `useReducerAtom`, `useAtomCallback`, `useHydrateAtoms` |
30
+ | `@octanejs/jotai/vanilla` | Direct re-exports of `jotai/vanilla` |
31
+ | `@octanejs/jotai/vanilla/utils` | Direct re-exports of `jotai/vanilla/utils` |
32
+ | `@octanejs/jotai/vanilla/internals` | Upstream's unstable store building blocks |
33
+ | `@octanejs/jotai/utils` | Vanilla utilities and hook utilities |
76
34
 
77
- ```tsx
78
- const userAtom = atom(async () => (await fetch('/api/user')).json());
79
-
80
- function Profile() @{
81
- <div>
82
- @try {
83
- <UserName />
84
- } @pending {
85
- <span>loading…</span>
86
- } @catch (e) {
87
- <span>failed: {(e as Error).message}</span>
88
- }
89
- </div>
90
- }
91
- ```
35
+ Framework-neutral callers can import directly from `jotai/vanilla` and `jotai/vanilla/utils`. These imports share the same atoms and stores with the Octane binding.
36
+
37
+ ## Jotai 3 migration
38
+
39
+ - `useAtomValueRaw` returns the atom value without unwrapping promises or suspending. It subscribes through an effect.
40
+ - `useAtomValueRawSync` also returns the raw value and uses `useSyncExternalStore` for synchronous store consistency.
41
+ - `useAtomValue` and `useAtom` retain Suspense integration. Their obsolete `delay` option is removed.
42
+ - `loadable` is removed. Use the retained `unwrap` utility or read raw promise values for non-suspending reads.
43
+ - `atomFamily` moves to `jotai-family`; install that package and import `atomFamily` directly from it.
44
+ - The atom read function's `setSelf` option is removed. Its replacement depends on the use case; see the [upstream migration guide](https://github.com/pmndrs/jotai/blob/89d4fddd1949628e50952fc8ac1b09786248dfca/docs/guides/migrating-to-v3.mdx).
45
+ - Upstream's internal store API advances from Rev3 to Rev4. It remains unstable.
46
+ - The existing `INTERNAL_InferAtomTuples` type remains available from both utility entry points for compatibility.
47
+
48
+ Readers no longer force a second render immediately after subscribing. `useSetAtom` still does not subscribe the writer. Hook slots are forwarded to keep multiple atom hooks in one component independent.
49
+
50
+ ## Async atoms and server rendering
51
+
52
+ `useAtomValue` unwraps Jotai's stable continuable promises through Octane's `use()`. Use a Suspense boundary for pending values. Both raw hooks preserve the promise as a value.
92
53
 
93
- ## Status
54
+ Create a store per server request and pass it to `Provider`. Server rendering does not mount atom subscriptions. The hydration conformance test checks reuse of the server DOM, live client updates, and subscription teardown.
94
55
 
95
- Current scope, known divergences, and verification status are tracked in the
96
- generated [bindings status table](../../docs/bindings-status.md), sourced from
97
- this package's [`status.json`](./status.json).
56
+ See [UPSTREAM.md](./UPSTREAM.md) for the immutable source pin, API crosswalk, test adaptation, and recorded evidence.
package/UPSTREAM.md ADDED
@@ -0,0 +1,69 @@
1
+ # Jotai upstream
2
+
3
+ `@octanejs/jotai` ports the provider and React hooks from Jotai 3.0.0 and imports the framework-neutral core from `jotai/vanilla`, `/vanilla/utils`, and `/vanilla/internals`.
4
+
5
+ ## Immutable identity and license
6
+
7
+ - Release: `jotai@3.0.0`
8
+ - Commit: `89d4fddd1949628e50952fc8ac1b09786248dfca`
9
+ - Source: https://github.com/pmndrs/jotai/tree/89d4fddd1949628e50952fc8ac1b09786248dfca
10
+ - MIT license: exact upstream bytes in packaged `LICENSE.upstream`; SHA-256 `0530d5d58026f4bb73367d195946f6a516a648ea62dd8b84763318f64d3cb3e2`.
11
+ - npm integrity: `sha512-KxhbmsJUp/tmrv6aZgO+nzB3H9K8C0xmk5i3aGkwy/EANg73DVBkggv3zVHaAi5PlNlQgTgP4uiVepo+JC+wSA==`.
12
+ - npm archive SHA-256: `dc9cfb4c901424eea4be448048a274c56472ab0956ee4e4e7d1623bc98e4ef00`.
13
+ - Signed npm provenance resolves the release tag to this commit; npm does not publish a `gitHead` for this release.
14
+ - Tested dependency: 3.0.0; supported catalog range: `^3.0.0`.
15
+ - React runtime oracle: React and React DOM 19.2.7. Original type compiler: TypeScript 6.0.3, with React types 19.2.18 and React DOM types 19.2.7.
16
+
17
+ `audit/upstream.lock.json` authenticates the byte-exact repository source and tests under `upstream/`. The npm artifact is retained for published-declaration verification. Neither tree is published. Vanilla source appears only in the pristine test environment; runtime code imports the installed upstream dependency.
18
+
19
+ ## Complete export crosswalk
20
+
21
+ The table includes runtime and type exports from every published runtime entry. Root and vanilla utilities import the same upstream implementation. Provider/hooks and the four hook utilities are adapted to Octane. All seven entries have exact runtime export-set checks and positive public type assertions in `tests/types/public.ts`, including upstream's unstable `INTERNAL_*` exports. Package metadata describes the Octane package itself.
22
+
23
+ | Entry | Exports | Disposition |
24
+ | --- | --- | --- |
25
+ | `@octanejs/jotai` | `Atom`, `ExtractAtomArgs`, `ExtractAtomResult`, `ExtractAtomValue`, `Getter`, `INTERNAL_overrideCreateStore`, `PrimitiveAtom`, `Provider`, `SetStateAction`, `Setter`, `WritableAtom`, `atom`, `createStore`, `getDefaultStore`, `useAtom`, `useAtomValue`, `useAtomValueRaw`, `useAtomValueRawSync`, `useSetAtom`, `useStore` | Imported vanilla exports plus ported hooks/provider. |
26
+ | `@octanejs/jotai/utils` | `RESET`, `atomWithDefault`, `atomWithLazy`, `atomWithObservable`, `atomWithReducer`, `atomWithRefresh`, `atomWithReset`, `atomWithStorage`, `createJSONStorage`, `freezeAtom`, `freezeAtomCreator`, `selectAtom`, `splitAtom`, `unstable_withStorageValidator`, `unwrap`, `useAtomCallback`, `useHydrateAtoms`, `useReducerAtom`, `useResetAtom` | Imported vanilla exports plus ported hooks/provider. |
27
+ | `@octanejs/jotai/vanilla` | `Atom`, `ExtractAtomArgs`, `ExtractAtomResult`, `ExtractAtomValue`, `Getter`, `INTERNAL_overrideCreateStore`, `PrimitiveAtom`, `SetStateAction`, `Setter`, `WritableAtom`, `atom`, `createStore`, `getDefaultStore` | Imported from the matching Jotai vanilla entry. |
28
+ | `@octanejs/jotai/vanilla/utils` | `RESET`, `atomWithDefault`, `atomWithLazy`, `atomWithObservable`, `atomWithReducer`, `atomWithRefresh`, `atomWithReset`, `atomWithStorage`, `createJSONStorage`, `freezeAtom`, `freezeAtomCreator`, `selectAtom`, `splitAtom`, `unstable_withStorageValidator`, `unwrap` | Imported from the matching Jotai vanilla entry. |
29
+ | `@octanejs/jotai/vanilla/internals` | `INTERNAL_AtomOnInit`, `INTERNAL_AtomOnMount`, `INTERNAL_AtomRead`, `INTERNAL_AtomState`, `INTERNAL_AtomStateMap`, `INTERNAL_AtomWrite`, `INTERNAL_BuildingBlocks`, `INTERNAL_Callbacks`, `INTERNAL_ChangedAtoms`, `INTERNAL_EnsureAtomState`, `INTERNAL_FlushCallbacks`, `INTERNAL_InvalidateDependents`, `INTERNAL_InvalidatedAtoms`, `INTERNAL_KEY_abortHandlersMap`, `INTERNAL_KEY_abortPromise`, `INTERNAL_KEY_atomOnInit`, `INTERNAL_KEY_atomOnMount`, `INTERNAL_KEY_atomRead`, `INTERNAL_KEY_atomStateMap`, `INTERNAL_KEY_atomWrite`, `INTERNAL_KEY_changedAtoms`, `INTERNAL_KEY_enhanceBuildingBlocks`, `INTERNAL_KEY_ensureAtomState`, `INTERNAL_KEY_flushCallbacks`, `INTERNAL_KEY_invalidateDependents`, `INTERNAL_KEY_invalidatedAtoms`, `INTERNAL_KEY_mountAtom`, `INTERNAL_KEY_mountCallbacks`, `INTERNAL_KEY_mountDependencies`, `INTERNAL_KEY_mountedMap`, `INTERNAL_KEY_readAtomState`, `INTERNAL_KEY_recomputeInvalidatedAtoms`, `INTERNAL_KEY_registerAbortHandler`, `INTERNAL_KEY_setAtomStateValueOrPromise`, `INTERNAL_KEY_storeEpochHolder`, `INTERNAL_KEY_storeGet`, `INTERNAL_KEY_storeHooks`, `INTERNAL_KEY_storeSet`, `INTERNAL_KEY_storeSub`, `INTERNAL_KEY_unmountAtom`, `INTERNAL_KEY_unmountCallbacks`, `INTERNAL_KEY_writeAtomState`, `INTERNAL_MountAtom`, `INTERNAL_MountDependencies`, `INTERNAL_Mounted`, `INTERNAL_MountedMap`, `INTERNAL_ReadAtomState`, `INTERNAL_RecomputeInvalidatedAtoms`, `INTERNAL_Store`, `INTERNAL_StoreHooks`, `INTERNAL_UnmountAtom`, `INTERNAL_WriteAtomState`, `INTERNAL_addPendingPromiseToDependency`, `INTERNAL_buildStoreRev4`, `INTERNAL_getBuildingBlocksRev4`, `INTERNAL_getMountedOrPendingDependents`, `INTERNAL_hasInitialValue`, `INTERNAL_initializeStoreHooksRev4`, `INTERNAL_isActuallyWritableAtom`, `INTERNAL_isAtomStateInitialized`, `INTERNAL_isPromiseLike`, `INTERNAL_returnAtomValue`, `INTERNAL_shouldThrowSynchronously` | Imported from the matching Jotai vanilla entry. |
30
+ | `@octanejs/jotai/react` | `Provider`, `useAtom`, `useAtomValue`, `useAtomValueRaw`, `useAtomValueRawSync`, `useSetAtom`, `useStore` | Ported hooks/provider. |
31
+ | `@octanejs/jotai/react/utils` | `useAtomCallback`, `useHydrateAtoms`, `useReducerAtom`, `useResetAtom` | Ported hooks/provider. |
32
+
33
+ ## Source boundary
34
+
35
+ `src/react/Provider.tsrx` and `src/react/store.ts` correspond to upstream `src/react/Provider.ts`. `src/react/utils.ts` retains the four modules under upstream `src/react/utils/`. Other hook and continuable-promise modules preserve upstream layout. `src/internal.ts` supplies the Octane call-site slot adaptation. `audit/source-ledger.json` records hashes for the complete shipped closure and identifies the adapted boundary.
36
+
37
+ Octane's `use()` handles Suspense; React 18's fallback is unnecessary. Raw hook subscriptions and the Rev4 abort-handler protocol follow Jotai 3. Provider context is deduplicated by renderer identity, retaining the existing Octane binding contract. Providers render one stable JSX shape when the store prop changes.
38
+
39
+ The migration removes `delay`, `loadable`, `atomFamily`, the atom-read `setSelf` option, and Rev3 internals, as upstream did. The existing `INTERNAL_InferAtomTuples` utility export is retained for compatibility and checked against Jotai's published `dist/react/utils/useHydrateAtoms.d.ts` declaration. See README for consumer guidance.
40
+
41
+ ## Complete upstream test crosswalk
42
+
43
+ `audit/registrations.json` preserves all 404 immutable registrations across 44 test files. `audit/crosswalk.json` maps each registration to its generated Octane test file. Both pristine and adapted runtime suites execute all 404 registrations without skips. Eleven registrations in the four explicit type-test files are additionally compiled in separate pristine and adapted programs; the hydration utility suite's negative type controls are also compiled.
44
+
45
+ The committed lock generates ignored `tests/upstream/`. Import repointing and the Octane JSX pragma are mechanical rewrites. Committed patches contain only these test adaptations:
46
+
47
+ - React class error boundaries become functional Octane error boundaries, preserving the error, fallback and retry assertions.
48
+ - Commit-count hooks move from JSX child expressions into component setup so they observe the owning component. Their effects use explicit `null` dependencies for the original every-render observation.
49
+ - The observable error-boundary fixture moves to module scope for Octane compilation.
50
+
51
+ The pristine type lane preserves upstream's `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`. The adapted lane uses the repository's strict configuration without these additional flags because imported Octane runtime source does not satisfy them. Both lanes retain `strict: true`, `skipLibCheck: false`, every upstream positive assertion, and every negative control.
52
+
53
+ `audit/react-parity.json` registers pristine/adapted runtime and type lanes, the four existing React/Octane differential scenarios, and the SSR/hydration conformance scenario. The latter proves no server subscriptions, existing DOM adoption, click/external-store updates, and teardown. Conformance checks additionally cover slot identity, provider scope/swap, subscription cleanup, async rejection/resolution and utilities.
54
+
55
+ ## Reproduce evidence
56
+
57
+ ```sh
58
+ node scripts/react-port/materialize.mjs run --package-dir packages/jotai
59
+ node scripts/react-parity/verify-provenance.mjs --package-dir packages/jotai
60
+ pnpm --dir packages/jotai test
61
+ node scripts/react-parity/harness.mjs run-required --manifest packages/jotai/audit/react-parity.json
62
+ ./packages/jotai/node_modules/.bin/tsc --noEmit -p packages/jotai/tsconfig.pristine.json
63
+ pnpm exec tsrx-tsc --noEmit -p packages/jotai/tsconfig.adapted.json
64
+ pnpm exec tsrx-tsc --noEmit -p packages/jotai/tsconfig.json
65
+ pnpm exec tsrx-tsc --noEmit -p packages/jotai/tests/types/tsconfig.json
66
+ pnpm packages:pack:check
67
+ ```
68
+
69
+ The shared port evidence gate records the actual commands and verifies the package contract, licenses, crosswalk, and shipped closure. The declared imported surfaces retain dependency, exports, public types and consumer checks; copied React surfaces retain their full upstream suite obligations. The ownership declaration does not remove prior evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/jotai",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -31,7 +31,10 @@
31
31
  "types": "src/index.ts",
32
32
  "files": [
33
33
  "src",
34
- "README.md"
34
+ "README.md",
35
+ "UPSTREAM.md",
36
+ "LICENSE",
37
+ "LICENSE.upstream"
35
38
  ],
36
39
  "exports": {
37
40
  ".": "./src/index.ts",
@@ -43,20 +46,34 @@
43
46
  "./utils": "./src/utils.ts"
44
47
  },
45
48
  "dependencies": {
46
- "jotai": "^2.20.2"
49
+ "jotai": "^3.0.0"
47
50
  },
48
51
  "peerDependencies": {
49
- "octane": "0.1.51"
52
+ "octane": "^0.1.51 || ^0.2.0"
50
53
  },
51
54
  "devDependencies": {
52
- "@tsrx/react": "^0.2.61",
55
+ "@tsrx/react": "^0.2.71",
53
56
  "esbuild": "^0.28.1",
54
- "react": "^19.2.7",
55
- "react-dom": "^19.2.7",
57
+ "react": "19.2.7",
58
+ "react-dom": "19.2.7",
56
59
  "vitest": "^4.1.10",
57
- "octane": "0.1.51"
60
+ "@testing-library/react": "^16.3.2",
61
+ "@testing-library/jest-dom": "^6.9.1",
62
+ "rxjs": "^7.8.2",
63
+ "wonka": "6.3.6",
64
+ "jest-leak-detector": "30.5.1",
65
+ "@types/node": "^24.13.3",
66
+ "vite": "^8.1.5",
67
+ "typescript": "6.0.3",
68
+ "@types/react": "19.2.18",
69
+ "@types/react-dom": "19.2.7",
70
+ "octane": "0.2.11",
71
+ "@octanejs/testing-library": "0.1.51"
58
72
  },
59
73
  "scripts": {
60
- "test": "vitest run"
74
+ "test": "vitest run --root ../.. --config vitest.config.js --project jotai --project jotai-differential --project jotai-pristine --project jotai-hydration",
75
+ "upstream:check": "node ../../scripts/react-port/materialize.mjs run --check --package-dir .",
76
+ "upstream:test": "node ../../scripts/react-parity/run-pristine.mjs jotai",
77
+ "upstream:verify": "node ../../scripts/react-parity/verify-provenance.mjs --package-dir ."
61
78
  }
62
79
  }
package/src/index.ts CHANGED
@@ -1,20 +1,3 @@
1
- // @octanejs/jotai — jotai for the octane renderer.
2
- //
3
- // jotai cleanly separates a framework-agnostic vanilla core (`atom`,
4
- // `createStore`, `getDefaultStore` + all of vanilla/utils) from a small React
5
- // binding (`Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom`).
6
- // This package reuses the vanilla core UNCHANGED (re-exported verbatim from
7
- // `jotai/vanilla`) and reimplements only the binding on octane's hooks —
8
- // preserving upstream's useReducer-force-update implementation rather than
9
- // rewriting it on useSyncExternalStore, so re-render behavior matches jotai on
10
- // React. The public surface matches jotai 1:1: existing jotai code works by
11
- // changing the import from `jotai` to `@octanejs/jotai`.
12
- //
13
- // The one octane-specific detail is hook slots: octane keys hooks by a
14
- // compiler-injected per-call-site Symbol, appended as the LAST argument of
15
- // every `use*` call. The hooks here FORWARD that slot (deriving stable
16
- // sub-slots when one hook composes several base hooks — see internal.ts), so
17
- // `useAtom(a)` and `useAtom(b)` in one component stay independent, just like
18
- // in React.
1
+ // Jotai 3 vanilla exports and Octane provider/hooks.
19
2
  export * from './vanilla';
20
3
  export * from './react';
@@ -7,7 +7,7 @@ import { useRef, type OctaneNode } from 'octane';
7
7
  import { createStore } from 'jotai/vanilla';
8
8
  import { StoreContext, type Store } from './store.ts';
9
9
 
10
- export function Provider(props: { children?: OctaneNode; store?: Store }) @{
10
+ export function Provider(props: { children?: OctaneNode; store?: Store }) {
11
11
  const { children, store } = props;
12
12
 
13
13
  const storeRef = useRef<Store | null>(null);
@@ -15,5 +15,7 @@ export function Provider(props: { children?: OctaneNode; store?: Store }) @{
15
15
  storeRef.current = createStore();
16
16
  }
17
17
 
18
- <StoreContext.Provider value={store || storeRef.current!}>{children}</StoreContext.Provider>
18
+ return <StoreContext.Provider
19
+ value={store || storeRef.current!}
20
+ >{children}</StoreContext.Provider>;
19
21
  }
@@ -1,4 +1,5 @@
1
1
  // Type declaration for the .tsrx component (resolved by relative path).
2
2
  import type { Store } from './store';
3
+ import type { JSX, OctaneNode } from 'octane';
3
4
 
4
- export declare const Provider: (props: { children?: unknown; store?: Store }) => unknown;
5
+ export declare const Provider: (props: { children?: OctaneNode; store?: Store }) => JSX.Element;
@@ -0,0 +1,54 @@
1
+ import {
2
+ INTERNAL_getBuildingBlocksRev4 as INTERNAL_getBuildingBlocks,
3
+ INTERNAL_KEY_registerAbortHandler as KEY_registerAbortHandler,
4
+ } from 'jotai/vanilla/internals';
5
+ import type { INTERNAL_Store as Store } from 'jotai/vanilla/internals';
6
+
7
+ export const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
8
+ typeof (x as PromiseLike<unknown>)?.then === 'function';
9
+
10
+ const continuablePromiseMap = new WeakMap<PromiseLike<unknown>, Promise<unknown>>();
11
+
12
+ export const createContinuablePromise = <T>(
13
+ store: Store,
14
+ promise: PromiseLike<T>,
15
+ getValue: () => PromiseLike<T> | T,
16
+ ): Promise<unknown> => {
17
+ const buildingBlocks = INTERNAL_getBuildingBlocks(store);
18
+ const registerAbortHandler = buildingBlocks[KEY_registerAbortHandler];
19
+ let continuablePromise = continuablePromiseMap.get(promise);
20
+ if (!continuablePromise) {
21
+ continuablePromise = new Promise<T>((resolve, reject) => {
22
+ let curr = promise;
23
+ const onFulfilled = (me: PromiseLike<T>) => (v: T) => {
24
+ if (curr === me) {
25
+ resolve(v);
26
+ }
27
+ };
28
+ const onRejected = (me: PromiseLike<T>) => (e: unknown) => {
29
+ if (curr === me) {
30
+ reject(e);
31
+ }
32
+ };
33
+ const onAbort = () => {
34
+ try {
35
+ const nextValue = getValue();
36
+ if (isPromiseLike(nextValue)) {
37
+ continuablePromiseMap.set(nextValue, continuablePromise!);
38
+ curr = nextValue;
39
+ nextValue.then(onFulfilled(nextValue), onRejected(nextValue));
40
+ registerAbortHandler(buildingBlocks, store, nextValue, onAbort);
41
+ } else {
42
+ resolve(nextValue);
43
+ }
44
+ } catch (e) {
45
+ reject(e);
46
+ }
47
+ };
48
+ promise.then(onFulfilled(promise), onRejected(promise));
49
+ registerAbortHandler(buildingBlocks, store, promise, onAbort);
50
+ });
51
+ continuablePromiseMap.set(promise, continuablePromise);
52
+ }
53
+ return continuablePromise;
54
+ };
@@ -1,37 +1,10 @@
1
- // useAtomValue — port of jotai's react/useAtomValue.ts. Upstream reads the
2
- // atom through a force-update reducer holding a `[value, store, atom]` tuple
3
- // and subscribes in an effect (re-render on store notify; an unconditional
4
- // post-subscribe rerender catches updates that raced between render and
5
- // effect — so mounting renders twice, in React and here alike). The port keeps
6
- // that shape exactly: octane's `useReducer` has React's semantics (lazy init;
7
- // a no-op dispatch still renders once with children bailing), and a
8
- // render-phase `rerender()` on store/atom swap mutates the reducer state
9
- // synchronously and schedules a self-quiescing re-render while THIS render
10
- // uses the locally-computed value.
11
- //
12
- // Async atom values suspend through octane's `use()` on a "continuable"
13
- // promise: a wrapper whose identity is WeakMap-stable across atom
14
- // recomputations (aborted fetches chain into it via the store's abort-handler
15
- // registry). That stability is what lets octane's thenable replay see the SAME
16
- // promise on every re-render until it settles — do not simplify it away.
17
- import { use, useDebugValue, useEffect, useReducer } from 'octane';
18
- import { INTERNAL_getBuildingBlocksRev3 as INTERNAL_getBuildingBlocks } from 'jotai/vanilla/internals';
1
+ // Jotai 3 separates the concurrent raw subscription from Suspense unwrapping.
2
+ import { use } from 'octane';
19
3
  import type { Atom, ExtractAtomValue } from 'jotai/vanilla';
20
- import { useStore, type Store } from './store';
4
+ import { isPromiseLike } from './continuablePromise';
5
+ import { useAtomValueRaw } from './useAtomValueRaw';
21
6
  import { splitSlot, subSlot } from '../internal';
22
7
 
23
- // The consumer's bundler substitutes the whole `process.env.NODE_ENV` expression
24
- // below, so it must stay written out literally. Declared module-locally — never
25
- // `declare global`, which would ship in the tarball — so this file type-checks in
26
- // a browser app that has no `@types/node`.
27
- declare const process: { env: { NODE_ENV?: string } };
28
-
29
- const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
30
- typeof (x as PromiseLike<unknown>)?.then === 'function';
31
-
32
- // Opt-in (`unstable_promiseStatus`) decoration of the suspended promise with
33
- // React 19's `status`/`value`/`reason` convention. Upstream defaults this to
34
- // "React.use is missing"; octane always has `use`, so it defaults to false.
35
8
  const attachPromiseStatus = <T>(
36
9
  promise: PromiseLike<T> & {
37
10
  status?: 'pending' | 'fulfilled' | 'rejected';
@@ -54,136 +27,30 @@ const attachPromiseStatus = <T>(
54
27
  }
55
28
  };
56
29
 
57
- const continuablePromiseMap = new WeakMap<PromiseLike<unknown>, Promise<unknown>>();
58
-
59
- const createContinuablePromise = <T>(
60
- store: Store,
61
- promise: PromiseLike<T>,
62
- getValue: () => PromiseLike<T> | T,
63
- ) => {
64
- const buildingBlocks = INTERNAL_getBuildingBlocks(store);
65
- const registerAbortHandler = buildingBlocks[26];
66
- let continuablePromise = continuablePromiseMap.get(promise);
67
- if (!continuablePromise) {
68
- continuablePromise = new Promise<T>((resolve, reject) => {
69
- let curr = promise;
70
- const onFulfilled = (me: PromiseLike<T>) => (v: T) => {
71
- if (curr === me) {
72
- resolve(v);
73
- }
74
- };
75
- const onRejected = (me: PromiseLike<T>) => (e: unknown) => {
76
- if (curr === me) {
77
- reject(e);
78
- }
79
- };
80
- const onAbort = () => {
81
- try {
82
- const nextValue = getValue();
83
- if (isPromiseLike(nextValue)) {
84
- continuablePromiseMap.set(nextValue, continuablePromise!);
85
- curr = nextValue;
86
- nextValue.then(onFulfilled(nextValue), onRejected(nextValue));
87
- registerAbortHandler(buildingBlocks, store, nextValue, onAbort);
88
- } else {
89
- resolve(nextValue);
90
- }
91
- } catch (e) {
92
- reject(e);
93
- }
94
- };
95
- promise.then(onFulfilled(promise), onRejected(promise));
96
- registerAbortHandler(buildingBlocks, store, promise, onAbort);
97
- });
98
- continuablePromiseMap.set(promise, continuablePromise);
99
- }
100
- return continuablePromise;
101
- };
102
-
103
- type Options = Parameters<typeof useStore>[0] & {
104
- /** @deprecated delay option is deprecated and will be removed in v3. https://github.com/pmndrs/jotai/pull/3264 */
105
- delay?: number;
30
+ type Options = Parameters<typeof useAtomValueRaw>[1] & {
106
31
  unstable_promiseStatus?: boolean;
107
32
  };
108
33
 
109
34
  export function useAtomValue<Value>(atom: Atom<Value>, options?: Options): Awaited<Value>;
110
-
111
35
  export function useAtomValue<AtomType extends Atom<unknown>>(
112
36
  atom: AtomType,
113
37
  options?: Options,
114
38
  ): Awaited<ExtractAtomValue<AtomType>>;
115
-
116
39
  export function useAtomValue<Value>(
117
40
  atom: Atom<Value>,
118
41
  ...rest: [options?: Options, slot?: symbol]
119
42
  ) {
120
43
  const [user, slot] = splitSlot(rest);
121
44
  const options = user[0] as Options | undefined;
122
- const { delay, unstable_promiseStatus: promiseStatus = false } = options || {};
123
- const store = useStore(options);
124
-
125
- const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] = useReducer<
126
- readonly [Value, Store, Atom<Value>],
127
- void,
128
- undefined
129
- >(
130
- (prev) => {
131
- const nextValue = store.get(atom);
132
- if (Object.is(prev[0], nextValue) && prev[1] === store && prev[2] === atom) {
133
- return prev;
134
- }
135
- return [nextValue, store, atom];
136
- },
137
- undefined,
138
- () => [store.get(atom), store, atom],
139
- subSlot(slot, 'uav:r'),
45
+ const value = (useAtomValueRaw as <T>(a: Atom<T>, o?: Options, s?: symbol) => T)(
46
+ atom,
47
+ options,
48
+ subSlot(slot, 'value:raw'),
140
49
  );
141
-
142
- let value = valueFromReducer;
143
- if (storeFromReducer !== store || atomFromReducer !== atom) {
144
- rerender();
145
- value = store.get(atom);
146
- }
147
-
148
- useEffect(
149
- () => {
150
- const unsub = store.sub(atom, () => {
151
- if (promiseStatus) {
152
- try {
153
- const value = store.get(atom);
154
- if (isPromiseLike(value)) {
155
- attachPromiseStatus(createContinuablePromise(store, value, () => store.get(atom)));
156
- }
157
- } catch {
158
- // ignore
159
- }
160
- }
161
- if (typeof delay === 'number') {
162
- if (process.env.NODE_ENV !== 'production') {
163
- console.warn(
164
- '[DEPRECATED] delay option is deprecated and will be removed in v3. https://github.com/pmndrs/jotai/pull/3264',
165
- );
166
- }
167
- // delay rerendering to wait a promise possibly to resolve
168
- setTimeout(rerender, delay);
169
- return;
170
- }
171
- rerender();
172
- });
173
- rerender();
174
- return unsub;
175
- },
176
- [store, atom, delay, promiseStatus],
177
- subSlot(slot, 'uav:e'),
178
- );
179
-
180
- useDebugValue(value);
181
50
  if (isPromiseLike(value)) {
182
- const promise = createContinuablePromise(store, value, () => store.get(atom));
183
- if (promiseStatus) {
184
- attachPromiseStatus(promise);
185
- }
186
- return use(promise);
51
+ // Octane always provides use(); upstream's React 18 fallback is unnecessary.
52
+ if (options?.unstable_promiseStatus) attachPromiseStatus(value);
53
+ return use(value);
187
54
  }
188
55
  return value as Awaited<Value>;
189
56
  }
@@ -0,0 +1,53 @@
1
+ import { useDebugValue, useEffect, useReducer } from 'octane';
2
+ import type { Atom, ExtractAtomValue } from 'jotai/vanilla';
3
+ import { createContinuablePromise, isPromiseLike } from './continuablePromise';
4
+ import { useStore } from './store';
5
+
6
+ import { splitSlot, subSlot } from '../internal';
7
+
8
+ type Store = ReturnType<typeof useStore>;
9
+
10
+ type Options = Parameters<typeof useStore>[0];
11
+
12
+ export function useAtomValueRaw<Value>(atom: Atom<Value>, options?: Options): Value;
13
+
14
+ export function useAtomValueRaw<AtomType extends Atom<unknown>>(
15
+ atom: AtomType,
16
+ options?: Options,
17
+ ): ExtractAtomValue<AtomType>;
18
+
19
+ export function useAtomValueRaw<Value>(
20
+ atom: Atom<Value>,
21
+ ...rest: [options?: Options, slot?: symbol]
22
+ ) {
23
+ const [user, slot] = splitSlot(rest);
24
+ const options = user[0] as Options | undefined;
25
+ const store = useStore(options);
26
+ const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] = useReducer<
27
+ readonly [Value, Store, typeof atom],
28
+ void,
29
+ undefined
30
+ >(
31
+ (prev) => {
32
+ const nextValue = store.get(atom);
33
+ if (Object.is(prev[0], nextValue) && prev[1] === store && prev[2] === atom) {
34
+ return prev;
35
+ }
36
+ return [nextValue, store, atom];
37
+ },
38
+ undefined,
39
+ () => [store.get(atom), store, atom],
40
+ subSlot(slot, 'raw:reducer'),
41
+ );
42
+ let value = valueFromReducer;
43
+ if (storeFromReducer !== store || atomFromReducer !== atom) {
44
+ rerender();
45
+ value = store.get(atom);
46
+ }
47
+ useEffect(() => store.sub(atom, rerender), [store, atom], subSlot(slot, 'raw:effect'));
48
+ useDebugValue(value);
49
+ if (isPromiseLike(value)) {
50
+ return createContinuablePromise(store, value, () => store.get(atom));
51
+ }
52
+ return value;
53
+ }
@@ -0,0 +1,47 @@
1
+ import { useCallback, useDebugValue, useSyncExternalStore } from 'octane';
2
+ import type { Atom, ExtractAtomValue } from 'jotai/vanilla';
3
+ import { createContinuablePromise, isPromiseLike } from './continuablePromise';
4
+ import { useStore } from './store';
5
+
6
+ import { splitSlot, subSlot } from '../internal';
7
+
8
+ type Options = Parameters<typeof useStore>[0];
9
+
10
+ export function useAtomValueRawSync<Value>(atom: Atom<Value>, options?: Options): Value;
11
+
12
+ export function useAtomValueRawSync<AtomType extends Atom<unknown>>(
13
+ atom: AtomType,
14
+ options?: Options,
15
+ ): ExtractAtomValue<AtomType>;
16
+
17
+ export function useAtomValueRawSync<Value>(
18
+ atom: Atom<Value>,
19
+ ...rest: [options?: Options, slot?: symbol]
20
+ ) {
21
+ const [user, slot] = splitSlot(rest);
22
+ const options = user[0] as Options | undefined;
23
+ const store = useStore(options);
24
+ const getSnapshot = useCallback(
25
+ () => {
26
+ const value = store.get(atom);
27
+ if (isPromiseLike(value)) {
28
+ return createContinuablePromise(store, value, () => store.get(atom));
29
+ }
30
+ return value;
31
+ },
32
+ [store, atom],
33
+ subSlot(slot, 'sync:snapshot'),
34
+ );
35
+ const value = useSyncExternalStore(
36
+ useCallback(
37
+ (callback: () => void) => store.sub(atom, callback),
38
+ [store, atom],
39
+ subSlot(slot, 'sync:subscribe'),
40
+ ),
41
+ getSnapshot,
42
+ getSnapshot,
43
+ subSlot(slot, 'sync:store'),
44
+ );
45
+ useDebugValue(value);
46
+ return value;
47
+ }
@@ -128,8 +128,6 @@ type InferAtomTuples<T> = {
128
128
  : never;
129
129
  };
130
130
 
131
- // For internal use only
132
- // This can be changed without notice.
133
131
  export type INTERNAL_InferAtomTuples<T> = InferAtomTuples<T>;
134
132
 
135
133
  const hydratedMap: WeakMap<Store, WeakSet<AnyWritableAtom>> = new WeakMap();
package/src/react.ts CHANGED
@@ -1,10 +1,7 @@
1
- // `@octanejs/jotai/react` — the binding layer, ported from jotai's react.ts.
2
- // Upstream's implementation shape is kept deliberately (a force-update
3
- // useReducer + effect subscription, NOT useSyncExternalStore) so behavior —
4
- // including re-render timing and write-only non-re-rendering — matches jotai
5
- // on React.
6
1
  export { Provider } from './react/Provider.tsrx';
7
2
  export { useStore } from './react/store';
8
3
  export { useAtomValue } from './react/useAtomValue';
9
4
  export { useSetAtom } from './react/useSetAtom';
10
5
  export { useAtom } from './react/useAtom';
6
+ export { useAtomValueRaw } from './react/useAtomValueRaw';
7
+ export { useAtomValueRawSync } from './react/useAtomValueRawSync';
@@ -1,8 +1,8 @@
1
1
  // `@octanejs/jotai/vanilla/utils` — re-exported verbatim from jotai.
2
2
  //
3
3
  // Every vanilla util (RESET, atomWithReset, atomWithDefault, atomWithStorage,
4
- // atomWithReducer, atomWithRefresh, atomWithLazy, atomWithObservable, atomFamily,
5
- // selectAtom, splitAtom, loadable, unwrap, freezeAtom, …) is a framework-agnostic
4
+ // atomWithReducer, atomWithRefresh, atomWithLazy, atomWithObservable,
5
+ // selectAtom, splitAtom, unwrap, freezeAtom, …) is a framework-agnostic
6
6
  // atom factory or store helper — it composes with octane's `useAtom`/`useAtomValue`
7
7
  // unchanged, so we re-export it as-is (mirroring src/vanilla.ts).
8
8
  export * from 'jotai/vanilla/utils';