@ai-matrx/records 0.58.27 → 0.58.28

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog — @ai-matrx/records
2
2
 
3
+ ## 0.58.28
4
+ ### lane RENDER-AUDIT (2026-09-26)
5
+
6
+ - **A re-read page keeps every row that did not change.** `useRecords` / `useRecordPage` hand back the SAME object for a row the store answered exactly as before (same id, same everything it carries), so a page read after one write — the write door's own re-read, a reconnect, a colleague's new row — no longer gives every record a new identity and redraws every row of a grid. What is on screen is still exactly what the door answered.
7
+ - **The realtime subscription is no longer torn down on every page change.** `useLiveRows` listed the page's rows in its effect, so every patch, read or optimistic write left the channel and joined it again (and a notice in the gap landed on nobody). It now subscribes per (port, table, client) and reads the page it judges a notice against at the moment the notice lands.
8
+ - **An inline `onError` no longer rebuilds the client.** `RecordsProvider` holds one stable error sink that calls whatever the host passed last; a host writing `onError={(e) => …}` inline no longer re-reads every hook on every render. Whether a sink is bound at all is still part of the client's identity.
9
+
3
10
  ## 0.58.27
4
11
 
5
12
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -41954,8 +41954,15 @@ function asWriteConflict(error, recordId) {
41954
41954
  // src/react/context.tsx
41955
41955
  var RecordsContext = (0, import_react.createContext)(null);
41956
41956
  function RecordsProvider({ config, children }) {
41957
+ const latestOnError = (0, import_react.useRef)(config.onError);
41958
+ latestOnError.current = config.onError;
41959
+ const hasOnError = config.onError !== void 0;
41960
+ const stableOnError = (0, import_react.useMemo)(
41961
+ () => hasOnError ? (error) => latestOnError.current?.(error) : void 0,
41962
+ [hasOnError]
41963
+ );
41957
41964
  const client = (0, import_react.useMemo)(
41958
- () => createRecordsClient(config),
41965
+ () => createRecordsClient(stableOnError === void 0 ? config : { ...config, onError: stableOnError }),
41959
41966
  // The identity of the organization and the actor is what makes a client a
41960
41967
  // different client. Rebuilding on those is the point, not a cost.
41961
41968
  [
@@ -41966,7 +41973,7 @@ function RecordsProvider({ config, children }) {
41966
41973
  config.actor.on_behalf_of,
41967
41974
  config.realtime,
41968
41975
  config.resolver,
41969
- config.onError,
41976
+ stableOnError,
41970
41977
  config.schema
41971
41978
  ]
41972
41979
  );
@@ -42127,6 +42134,8 @@ function useLiveRows(tableId, base) {
42127
42134
  const client = useRecordsClient();
42128
42135
  const realtime = client.config.realtime;
42129
42136
  const { reload, setData } = base;
42137
+ const heldRows = (0, import_react2.useRef)([]);
42138
+ heldRows.current = base.data?.rows ?? [];
42130
42139
  (0, import_react2.useEffect)(() => {
42131
42140
  if (!realtime || !tableId) return;
42132
42141
  return realtime.subscribeRecords(
@@ -42145,7 +42154,7 @@ function useLiveRows(tableId, base) {
42145
42154
  return;
42146
42155
  }
42147
42156
  const fresh = new Map(answered.data.map((row) => [row.id, row]));
42148
- const onThisPage = (id) => (base.data?.rows ?? []).some((row) => row.id === id);
42157
+ const onThisPage = (id) => heldRows.current.some((row) => row.id === id);
42149
42158
  const needsThePage = ids.some((id) => !onThisPage(id) && fresh.has(id));
42150
42159
  if (needsThePage) {
42151
42160
  void reload();
@@ -42168,7 +42177,7 @@ function useLiveRows(tableId, base) {
42168
42177
  }
42169
42178
  }
42170
42179
  );
42171
- }, [realtime, tableId, client, reload, setData, base.data]);
42180
+ }, [realtime, tableId, client, reload, setData]);
42172
42181
  const live = Boolean(realtime && tableId);
42173
42182
  const liveReason = realtime ? tableId ? "Live: changes to this table re-read through the read door." : "Not live: no table selected." : "Not live: this host bound no realtime port, so this list updates when you reload it.";
42174
42183
  return { live, liveReason };
@@ -42181,6 +42190,38 @@ function useKeptPage(tableId, base) {
42181
42190
  }
42182
42191
  return base;
42183
42192
  }
42193
+ function sameRowAnswer(a, b) {
42194
+ if (a === b) return true;
42195
+ if (a.id !== b.id) return false;
42196
+ try {
42197
+ return JSON.stringify(a) === JSON.stringify(b);
42198
+ } catch {
42199
+ return false;
42200
+ }
42201
+ }
42202
+ function useSharedRows(state) {
42203
+ const held = (0, import_react2.useRef)({
42204
+ source: null,
42205
+ shared: null,
42206
+ byId: /* @__PURE__ */ new Map()
42207
+ });
42208
+ const data = state.data;
42209
+ if (data === null) return state;
42210
+ const memo = held.current;
42211
+ if (memo.source === data && memo.shared !== null) return { ...state, data: memo.shared };
42212
+ let reused = 0;
42213
+ const rows = data.rows.map((row) => {
42214
+ const before = memo.byId.get(row.id);
42215
+ if (before && sameRowAnswer(before, row)) {
42216
+ reused += 1;
42217
+ return before;
42218
+ }
42219
+ return row;
42220
+ });
42221
+ const shared = reused === 0 ? data : { ...data, rows };
42222
+ held.current = { source: data, shared, byId: new Map(rows.map((row) => [row.id, row])) };
42223
+ return { ...state, data: shared };
42224
+ }
42184
42225
  function useRecords(tableId, options = {}) {
42185
42226
  const pageSize = options.pageSize ?? 50;
42186
42227
  const page = options.page ?? 0;
@@ -42201,7 +42242,7 @@ function useRecords(tableId, options = {}) {
42201
42242
  [tableId, pageSize, page, filterKey]
42202
42243
  );
42203
42244
  const { live, liveReason } = useLiveRows(tableId, base);
42204
- return { ...useKeptPage(tableId, base), live, liveReason };
42245
+ return { ...useSharedRows(useKeptPage(tableId, base)), live, liveReason };
42205
42246
  }
42206
42247
  function useRecordPage(tableId, options = {}) {
42207
42248
  const pageSize = options.pageSize ?? 50;
@@ -42227,7 +42268,7 @@ function useRecordPage(tableId, options = {}) {
42227
42268
  [tableId, pageSize, page, filterKey, search, sortKey, viewId]
42228
42269
  );
42229
42270
  const { live, liveReason } = useLiveRows(tableId, base);
42230
- return { ...useKeptPage(tableId, base), live, liveReason };
42271
+ return { ...useSharedRows(useKeptPage(tableId, base)), live, liveReason };
42231
42272
  }
42232
42273
  function useRecordChangeRevision(recordId) {
42233
42274
  const [revision, setRevision] = (0, import_react2.useState)(0);