@createstore/createstore 1.0.0 → 1.1.1

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.
@@ -11,6 +11,15 @@ import {
11
11
  ClientRoot
12
12
  } from "./react-bindings.js";
13
13
  const __SnapshotContext = RSC ? null : createContext(null);
14
+ let leavingPage = false;
15
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
16
+ window.addEventListener("pagehide", () => {
17
+ leavingPage = true;
18
+ });
19
+ window.addEventListener("pageshow", () => {
20
+ leavingPage = false;
21
+ });
22
+ }
14
23
  const storesByName = /* @__PURE__ */ new Map();
15
24
  const MERGE = /* @__PURE__ */ Symbol("createStore.merge");
16
25
  const QUIET = /* @__PURE__ */ Symbol("createStore.quiet");
@@ -145,12 +154,48 @@ function defineEvents() {
145
154
  return void 0;
146
155
  }
147
156
  function createStore(initialState, options = {}) {
157
+ checkStoreOptions(options);
148
158
  if ("queries" in options)
149
159
  throw new Error(
150
160
  `[${options.name ?? "store"}] the queries option was replaced: declare each query in the state with query(fetch, options), e.g. { product: query((id, signal) => api.product(id, signal)) }, and read it with useGlobalState('product', id).`
151
161
  );
152
162
  return withScopes(initialState, options, makeBase(initialState, options));
153
163
  }
164
+ const STORE_OPTIONS = /* @__PURE__ */ new Set([
165
+ "name",
166
+ "persist",
167
+ "syncTabs",
168
+ "history",
169
+ "devtools",
170
+ "shared",
171
+ "ssr",
172
+ "logger",
173
+ "onChange",
174
+ "offline",
175
+ "validate",
176
+ "events",
177
+ "computed",
178
+ "keyActions",
179
+ "actions",
180
+ "queries"
181
+ ]);
182
+ const warnedOptions = /* @__PURE__ */ new Set();
183
+ function checkStoreOptions(opts) {
184
+ const options = opts;
185
+ const warn = (message) => {
186
+ if (warnedOptions.has(message)) return;
187
+ warnedOptions.add(message);
188
+ console.warn(message);
189
+ };
190
+ const id = options.name ?? "store";
191
+ for (const k of Object.keys(options))
192
+ if (!STORE_OPTIONS.has(k))
193
+ warn(`[${id}] createStore() has no option "${k}", so it is ignored. Options: ${[...STORE_OPTIONS].filter((o) => o !== "queries").join(", ")}.`);
194
+ if (!options.name && (options.persist || options.syncTabs || options.offline || options.shared))
195
+ warn(
196
+ `[store] persist, syncTabs, offline and shared use the store's name to tell stores apart. Without { name: '...' }, every unnamed store uses "store", so two of them would overwrite each other's saved data and mix up their tabs. Give this one a name.`
197
+ );
198
+ }
154
199
  const warnedSharedOnServer = /* @__PURE__ */ new Set();
155
200
  const warnedShape = /* @__PURE__ */ new Set();
156
201
  const SHAPE = /* @__PURE__ */ Symbol("createStore.shape");
@@ -1007,6 +1052,8 @@ function build(initialState, options, sharedDefinition) {
1007
1052
  }
1008
1053
  };
1009
1054
  let hydrated = !persistStorage;
1055
+ let warnedUnsavable = false;
1056
+ const earlyWrites = /* @__PURE__ */ new Set();
1010
1057
  let resolveHydrated;
1011
1058
  const whenHydrated = new Promise((r) => resolveHydrated = r);
1012
1059
  let storageIsAsync = false;
@@ -1033,7 +1080,26 @@ function build(initialState, options, sharedDefinition) {
1033
1080
  saveTimer = void 0;
1034
1081
  if (!persistStorage || !hydrated) return;
1035
1082
  const data = Object.fromEntries(persistedKeys.map((k) => [k, state[k]]));
1036
- const text = JSON.stringify(storageIsAsync ? { $version: persistVersion, $saved: Date.now(), state: data } : { $version: persistVersion, state: data });
1083
+ let text;
1084
+ try {
1085
+ text = JSON.stringify(storageIsAsync ? { $version: persistVersion, $saved: Date.now(), state: data } : { $version: persistVersion, state: data });
1086
+ } catch (error) {
1087
+ if (!warnedUnsavable) {
1088
+ warnedUnsavable = true;
1089
+ const bad = persistedKeys.find((k) => {
1090
+ try {
1091
+ JSON.stringify(state[k]);
1092
+ return false;
1093
+ } catch {
1094
+ return true;
1095
+ }
1096
+ });
1097
+ console.warn(
1098
+ `[${name}] couldn't save${bad ? ` "${bad}"` : ""}: ${error.message}. Saved state is JSON, so keep persisted keys to strings, numbers, booleans, null, arrays and plain objects, or leave this key out of saving with persist: { exclude: [...] }. Nothing is saved until it is fixed.`
1099
+ );
1100
+ }
1101
+ return;
1102
+ }
1037
1103
  lastSavedText = text;
1038
1104
  quietly(() => {
1039
1105
  const written = persistStorage.setItem(persistKey, text);
@@ -1138,9 +1204,12 @@ function build(initialState, options, sharedDefinition) {
1138
1204
  }
1139
1205
  if (rescued != null && isSavedState(rescued) && savedAt(rescued) > savedAt(stored)) text = rescued;
1140
1206
  const values = trusted(pick(readSaved(text)), "storage");
1207
+ for (const k of earlyWrites) delete values[k];
1208
+ const keptEarly = earlyWrites.size > 0;
1209
+ earlyWrites.clear();
1141
1210
  hydrated = true;
1142
1211
  if (!destroyed) apply(values, "storage", "loaded from storage", false);
1143
- if (!destroyed && text === rescued && rescued != null) saveNow();
1212
+ if (!destroyed && (keptEarly || text === rescued && rescued != null)) saveNow();
1144
1213
  finishHydration();
1145
1214
  },
1146
1215
  (error) => {
@@ -1245,6 +1314,7 @@ function build(initialState, options, sharedDefinition) {
1245
1314
  updateHistoryInfo();
1246
1315
  }
1247
1316
  function batch(fn) {
1317
+ if (typeof fn !== "function") throw new Error(`[${name}] batch() takes a function that makes the changes: store.batch(() => { store.setState('a', 1); store.setState('b', 2); }).`);
1248
1318
  batchDepth++;
1249
1319
  if (batchDepth === 1 && historyLimit) batchChanges = /* @__PURE__ */ Object.create(null);
1250
1320
  let result;
@@ -1337,6 +1407,7 @@ function build(initialState, options, sharedDefinition) {
1337
1407
  (k) => isStateKey(k) && !Object.is(state[k], values[k])
1338
1408
  );
1339
1409
  if (keys.length === 0) return;
1410
+ if (!hydrated && source === "local") for (const k of keys) earlyWrites.add(k);
1340
1411
  const prev = {};
1341
1412
  for (const k of keys) {
1342
1413
  prev[k] = state[k];
@@ -1572,15 +1643,6 @@ function build(initialState, options, sharedDefinition) {
1572
1643
  ownPromises.add(promise);
1573
1644
  return promise;
1574
1645
  }
1575
- let leavingPage = false;
1576
- if (isBrowser && typeof window.addEventListener === "function") {
1577
- window.addEventListener("pagehide", () => {
1578
- leavingPage = true;
1579
- });
1580
- window.addEventListener("pageshow", () => {
1581
- leavingPage = false;
1582
- });
1583
- }
1584
1646
  const SERVER_LOADING = Object.freeze({
1585
1647
  state: "loading",
1586
1648
  loading: true,
@@ -2082,11 +2144,15 @@ function build(initialState, options, sharedDefinition) {
2082
2144
  }
2083
2145
  const k = cacheKey(q, params);
2084
2146
  const prev = cache2.get(k)?.data;
2147
+ outdateRequest(k);
2085
2148
  setEntry(k, {
2086
2149
  data: typeof updater === "function" ? updater(prev) : updater,
2087
2150
  status: "success"
2088
2151
  });
2089
2152
  }
2153
+ function outdateRequest(k) {
2154
+ if (inflight.has(k)) versions.set(k, (versions.get(k) ?? 0) + 1);
2155
+ }
2090
2156
  function abort(q) {
2091
2157
  if (isQuery(q)) inlineAborts.get(q)?.abort();
2092
2158
  for (const [k, c] of [...namedAborts])
@@ -2224,6 +2290,8 @@ function build(initialState, options, sharedDefinition) {
2224
2290
  };
2225
2291
  const api = Object.assign(Object.create(setterProtoOf(def, API_NAMES, "apiProto")), apiMembers);
2226
2292
  Object.defineProperty(api, SETTER_OF, { value: (k) => setterFor(k) });
2293
+ if (options.actions != null && typeof options.actions !== "function")
2294
+ throw new Error(`[${name}] actions must be a function that receives the store's api and returns your actions: createStore(state, { actions: (api) => ({ add(x) { api.setState('items', (i) => [...i, x]); } }) }).`);
2227
2295
  const actions = options.actions?.(api) ?? {};
2228
2296
  const keyActionDefs = options.keyActions ?? {};
2229
2297
  const ssrCfg = options.ssr;
@@ -2618,6 +2686,7 @@ function build(initialState, options, sharedDefinition) {
2618
2686
  setter = (value, serverCall) => {
2619
2687
  const prev = cache2.get(k)?.data;
2620
2688
  const next = typeof value === "function" ? value(prev) : value;
2689
+ outdateRequest(k);
2621
2690
  setEntry(k, { data: next, status: "success" });
2622
2691
  if (!serverCall) return Promise.resolve();
2623
2692
  const done = Promise.resolve().then(serverCall).then(