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
6 changes: 6 additions & 0 deletions .changeset/type-safe-derived-stores.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/signals": patch
"solid-js": patch
---

Require complete explicit seeds for derived stores, preventing required properties from being absent at runtime while exposed as present in the store type. Object projections may instead omit the seed and use a return-only initializer that supplies a complete value on every resolution; array projections continue to require an explicit seed.
35 changes: 28 additions & 7 deletions packages/signals/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ export type {
StoreNode,
StoreOptions,
ProjectionOptions,
SeededProjectionOptions,
NotWrappable,
SolidStore
} from "./store.js";
export type { Merge, Omit } from "./utils.js";

export { isWrappable, $TRACK, $PROXY, $TARGET } from "./store.js";

import type { NoFn, ProjectionOptions, Store, StoreOptions, StoreSetter } from "./store.js";
import type {
NoFn,
NoArray,
ProjectionOptions,
SeededProjectionOptions,
Store,
StoreOptions,
StoreSetter
} from "./store.js";
import type { Refreshable } from "../core/index.js";
import {
createStoreNext,
Expand All @@ -24,21 +33,33 @@ import {
import { reconcileNextState } from "./next/reconcile.js";
import { createStoreDerivedNext } from "./next/projection.js";

export { createProjectionNext as createProjection } from "./next/projection.js";
export {
createProjectionNext as createProjection,
createProjectionHydrationReplayNext as createProjectionHydrationReplay,
createStoreHydrationReplayNext as createStoreHydrationReplay
} from "./next/projection.js";
export { storeIsShallow, storeHasFamily, storeHasOptimisticFamily } from "./next/store.js";
export { createOptimisticStoreNext as createOptimisticStore } from "./next/optimistic.js";
export {
createOptimisticStoreNext as createOptimisticStore,
createOptimisticStoreHydrationReplayNext as createOptimisticStoreHydrationReplay
} from "./next/optimistic.js";

/** Public createStore: plain form `(initialValue, options?)` and derived writable
* form `(fn, seed, options?)`. */
* forms `(fn)` / `(fn, seed, options?)`. */
export function createStore<T extends object = {}>(
initialValue: NoFn<T> | Store<NoFn<T>>,
initialValue: T & NoFn<T>,
options?: StoreOptions
): [get: Store<T>, set: StoreSetter<T>];
export function createStore<T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
seed: Partial<T> | Store<NoFn<T>>,
fn: (() => T | Promise<T> | AsyncIterable<T>) & NoArray<T> & NoFn<T>,
seed?: null,
options?: ProjectionOptions
): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
export function createStore<T extends object = {}>(
fn: ((draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>) & NoFn<T>,
seed: T,
options?: SeededProjectionOptions
): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
export function createStore(first: any, second?: any, third?: any): any {
if (typeof first === "function") return createStoreDerivedNext(first, second, third);
return createStoreNext(first, !!second?.shallow);
Expand Down
72 changes: 61 additions & 11 deletions packages/signals/src/store/next/optimistic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,21 @@ import { installOptimisticEngine } from "../../core/optimistic.js";
import {
$TARGET,
markRawIngest,
type NoArray,
type NoFn,
type ProjectionOptions,
type SeededProjectionOptions,
type Store,
type StoreOptions,
type StoreSetter
} from "../store.js";
import { runProjectionComputedNext } from "./projection.js";
import {
runProjectionComputedNext,
validateSeedlessOptions,
validateStoreValue,
createReplayStoreValidator,
type ProjectionResultValidator
} from "./projection.js";
import {
bumpDeep,
authoritativeRead,
Expand Down Expand Up @@ -179,27 +187,53 @@ function familyHasLiveOverrides(fam: { overlaid?: Set<any> }): boolean {
}

export function createOptimisticStoreNext<T extends object = {}>(
initialValue: NoFn<T> | Store<NoFn<T>>,
initialValue: T & NoFn<T>,
options?: StoreOptions
): [get: Store<T>, set: StoreSetter<T>];
export function createOptimisticStoreNext<T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
seed: Partial<T> | Store<NoFn<T>>,
fn: (() => T | Promise<T> | AsyncIterable<T>) & NoArray<T> & NoFn<T>,
seed?: null,
options?: ProjectionOptions
): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
export function createOptimisticStoreNext<T extends object = {}>(
first: T | ((store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>),
second?: Partial<T> | NoFn<T> | Store<NoFn<T>> | StoreOptions,
third?: ProjectionOptions
fn: ((draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>) & NoFn<T>,
seed: T,
options?: SeededProjectionOptions
): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
export function createOptimisticStoreNext<T extends object = {}>(
first:
| T
| ((draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>)
| (() => T | Promise<T> | AsyncIterable<T>),
second?: T | StoreOptions | null,
third?: SeededProjectionOptions
): [get: Store<T>, set: StoreSetter<T>] {
const derived = typeof first === "function";
const seeded = !derived || second != null;
const options = (derived ? third : second) as SeededProjectionOptions | undefined;
if (!seeded) validateSeedlessOptions(options);
const derive =
derived && !seeded ? () => (first as () => T | Promise<T> | AsyncIterable<T>)() : first;
return createOptimisticStoreNextInternal<T>(
derive,
(derived ? (second ?? {}) : first) as T,
options,
derived && !seeded ? validateStoreValue : undefined
);
}

function createOptimisticStoreNextInternal<T extends object = {}>(
first: T | ((draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>),
initialValue: T,
options?: SeededProjectionOptions,
validateResult?: ProjectionResultValidator<T>
): [get: Store<T>, set: StoreSetter<T>] {
// Engine first (armed nodes need optimisticWrite installed before any
// node exists), then the next-shape hooks.
installOptimisticEngine();
installNextBlockedHalf();

const derived = typeof first === "function";
const options = (derived ? third : second) as ProjectionOptions | undefined;
const initialValue = (derived ? second : first) as T;

const fam: StoreNextFamily = {
map: new WeakMap(),
Expand All @@ -224,7 +258,7 @@ export function createOptimisticStoreNext<T extends object = {}>(
}

if (derived) {
const fn = first as (store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>;
const fn = first as (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>;
// #3146: an async settle event belongs to the flight's OWN transaction.
// A live declared one re-enters (a merge if the generic settle path
// already entered a graph-stamped stranger — the landing supersedes any
Expand Down Expand Up @@ -321,7 +355,8 @@ export function createOptimisticStoreNext<T extends object = {}>(
fn,
options?.key === undefined ? "id" : options.key,
wrapCommit,
aroundDraftWrite
aroundDraftWrite,
validateResult
)
);
} finally {
Expand All @@ -347,6 +382,21 @@ export function createOptimisticStoreNext<T extends object = {}>(
];
}

/** @internal Hydration replay starts with a complete snapshot, then mutates a private draft. */
export function createOptimisticStoreHydrationReplayNext<T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
replaying: () => boolean,
options?: ProjectionOptions
): [get: Refreshable<Store<T>>, set: StoreSetter<T>] {
validateSeedlessOptions(options);
return createOptimisticStoreNextInternal<T>(
fn,
{} as T,
options,
createReplayStoreValidator(replaying)
) as [get: Refreshable<Store<T>>, set: StoreSetter<T>];
}

/** Resolve a retained transition through its merge chain (`_done` holds the
* merge target while merged, `true` once settled). Null = dead. */
function liveTransition(txn: Transition): Transition | null {
Expand Down
Loading
Loading