@warkypublic/svelix 0.1.36 → 0.1.38

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.
@@ -1,5 +1,6 @@
1
1
  export { default as Boxer } from './Boxer.svelte';
2
2
  export { default as BoxerTarget } from './BoxerTarget.svelte';
3
+ export { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
3
4
  export { BoxerResolveSpecAdapter } from './BoxerResolveSpecAdapter.js';
4
5
  export { BoxerRestHeaderSpecAdapter } from './BoxerRestHeaderSpecAdapter.js';
5
6
  export { createBoxerStore } from './store';
@@ -1,5 +1,6 @@
1
1
  export { default as Boxer } from './Boxer.svelte';
2
2
  export { default as BoxerTarget } from './BoxerTarget.svelte';
3
+ export { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
3
4
  export { BoxerResolveSpecAdapter } from './BoxerResolveSpecAdapter.js';
4
5
  export { BoxerRestHeaderSpecAdapter } from './BoxerRestHeaderSpecAdapter.js';
5
6
  export { createBoxerStore } from './store';
@@ -1,23 +1,32 @@
1
1
  import type { BoxerItem, BoxerProps } from './types';
2
- export interface BoxerStoreState extends BoxerProps {
2
+ export interface BoxerStoreState extends Omit<BoxerProps, 'error'> {
3
3
  boxerData: Array<BoxerItem>;
4
+ /** Runtime fetch error, distinct from the `error` prop used for form validation. */
5
+ error: string | null;
4
6
  hasMore: boolean;
5
7
  input: string;
6
8
  isFetching: boolean;
7
9
  opened: boolean;
8
10
  page: number;
9
11
  search: string;
12
+ selectedItems: Array<BoxerItem>;
10
13
  selectedOptionIndex: number;
11
14
  total: number;
12
15
  }
13
16
  export declare function createBoxerStore(initialProps: BoxerProps): {
17
+ addSelectedItem: (item: BoxerItem) => void;
18
+ cancel: () => void;
19
+ clearSelectedItems: () => void;
14
20
  fetchData: (search?: string, reset?: boolean) => Promise<void>;
15
21
  fetchMoreOnBottomReached: (target: HTMLDivElement) => void;
16
22
  loadMore: () => Promise<void>;
23
+ refetch: () => Promise<void>;
24
+ removeSelectedItem: (value: unknown) => void;
17
25
  set: (this: void, value: BoxerStoreState) => void;
18
26
  setInput: (input: string) => void;
19
27
  setOpened: (opened: boolean) => void;
20
28
  setSearch: (search: string) => void;
29
+ setSelectedItems: (items: Array<BoxerItem>) => void;
21
30
  setSelectedOptionIndex: (index: number) => void;
22
31
  subscribe: (this: void, run: import("svelte/store").Subscriber<BoxerStoreState>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
23
32
  update: (this: void, updater: import("svelte/store").Updater<BoxerStoreState>) => void;
@@ -1,11 +1,20 @@
1
- import { writable } from 'svelte/store';
1
+ import { get, writable } from 'svelte/store';
2
+ function valueKey(v) {
3
+ return typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
4
+ }
5
+ function matchesLocal(item, needle, columns) {
6
+ const lower = needle.toLowerCase();
7
+ return columns.some((col) => String(item[col] ?? '').toLowerCase().includes(lower));
8
+ }
2
9
  export function createBoxerStore(initialProps) {
3
- const { data = [], dataSource = 'local', pageSize = 50, ...rest } = initialProps;
10
+ const { data = [], dataSource = 'local', error: _propError, pageSize = 50, ...rest } = initialProps;
11
+ void _propError;
4
12
  const initial = {
5
13
  ...rest,
6
14
  boxerData: data,
7
15
  data,
8
16
  dataSource,
17
+ error: null,
9
18
  hasMore: dataSource === 'server',
10
19
  input: '',
11
20
  isFetching: false,
@@ -13,74 +22,117 @@ export function createBoxerStore(initialProps) {
13
22
  page: 0,
14
23
  pageSize,
15
24
  search: '',
25
+ selectedItems: [],
16
26
  selectedOptionIndex: -1,
17
27
  total: data.length,
18
28
  };
19
- const { subscribe, update, set } = writable(initial);
20
- function getState() {
21
- let current;
22
- subscribe((s) => (current = s))();
23
- return current;
24
- }
25
- let lastResetSearch;
29
+ const store = writable(initial);
30
+ const { subscribe, update, set } = store;
31
+ let requestCounter = 0;
32
+ let activeController = null;
26
33
  async function fetchData(search, reset) {
27
- const state = getState();
34
+ const state = get(store);
35
+ // Local mode — synchronous filter
28
36
  if (state.dataSource === 'local' || !state.onAPICall) {
29
37
  const localData = state.data ?? [];
30
38
  if (!search) {
31
- update((s) => ({ ...s, boxerData: localData, hasMore: false, total: localData.length }));
39
+ update((s) => ({
40
+ ...s,
41
+ boxerData: localData,
42
+ error: null,
43
+ hasMore: false,
44
+ total: localData.length,
45
+ }));
32
46
  return;
33
47
  }
34
- const filtered = localData.filter((item) => item.label.toLowerCase().includes(search.toLowerCase()));
35
- update((s) => ({ ...s, boxerData: filtered, hasMore: false, total: filtered.length }));
48
+ const cols = state.searchColumns && state.searchColumns.length > 0
49
+ ? state.searchColumns
50
+ : ['label'];
51
+ const filtered = localData.filter((item) => matchesLocal(item, search, cols));
52
+ update((s) => ({
53
+ ...s,
54
+ boxerData: filtered,
55
+ error: null,
56
+ hasMore: false,
57
+ total: filtered.length,
58
+ }));
36
59
  return;
37
60
  }
38
- if (state.onAPICall) {
39
- // Prevent concurrent fetches
40
- if (state.isFetching)
61
+ // Server mode — cancel in-flight, issue new request
62
+ const onAPICall = state.onAPICall;
63
+ activeController?.abort();
64
+ const controller = new AbortController();
65
+ activeController = controller;
66
+ const thisRequestId = ++requestCounter;
67
+ const currentPage = reset ? 0 : state.page;
68
+ update((s) => ({ ...s, error: null, isFetching: true }));
69
+ try {
70
+ const result = await onAPICall({
71
+ page: currentPage,
72
+ pageSize: state.pageSize ?? 50,
73
+ search,
74
+ signal: controller.signal,
75
+ });
76
+ if (thisRequestId !== requestCounter)
77
+ return;
78
+ update((s) => {
79
+ const boxerData = reset ? result.data : [...(s.boxerData ?? []), ...result.data];
80
+ return {
81
+ ...s,
82
+ boxerData,
83
+ error: null,
84
+ hasMore: boxerData.length < result.total,
85
+ isFetching: false,
86
+ page: reset ? 0 : s.page,
87
+ total: result.total,
88
+ };
89
+ });
90
+ }
91
+ catch (err) {
92
+ // Aborted by a newer fetch — newer fetch owns the state now.
93
+ if (controller.signal.aborted)
41
94
  return;
42
- // Prevent duplicate reset fetches for the same search term
43
- if (reset && search === lastResetSearch && state.boxerData.length > 0)
95
+ if (thisRequestId !== requestCounter)
44
96
  return;
45
- try {
46
- if (reset)
47
- lastResetSearch = search;
48
- update((s) => ({ ...s, isFetching: true }));
49
- const currentPage = reset ? 0 : state.page;
50
- const result = await state.onAPICall({ page: currentPage, pageSize: state.pageSize ?? 50, search });
51
- update((s) => {
52
- const boxerData = reset ? result.data : [...(s.boxerData ?? []), ...result.data];
53
- return {
54
- ...s,
55
- boxerData,
56
- hasMore: boxerData.length < result.total,
57
- isFetching: false,
58
- page: reset ? 0 : s.page,
59
- total: result.total,
60
- };
61
- });
62
- }
63
- catch (error) {
64
- console.error('Boxer fetchData error:', error);
65
- update((s) => ({ ...s, isFetching: false }));
66
- }
97
+ const message = err instanceof Error ? err.message : String(err);
98
+ console.error('Boxer fetchData error:', err);
99
+ update((s) => ({ ...s, error: message, isFetching: false }));
100
+ }
101
+ finally {
102
+ if (activeController === controller)
103
+ activeController = null;
67
104
  }
68
105
  }
69
106
  async function loadMore() {
70
- const state = getState();
107
+ const state = get(store);
71
108
  if (!state.hasMore || state.isFetching)
72
109
  return;
110
+ const prevPage = state.page;
73
111
  update((s) => ({ ...s, page: s.page + 1 }));
74
112
  await fetchData(state.search);
113
+ // Revert the page bump if the fetch errored (so next retry fetches the same page).
114
+ const after = get(store);
115
+ if (after.error && after.page === prevPage + 1) {
116
+ update((s) => ({ ...s, page: prevPage }));
117
+ }
75
118
  }
76
119
  function fetchMoreOnBottomReached(target) {
77
- const state = getState();
120
+ const state = get(store);
78
121
  if (!state.hasMore || state.isFetching)
79
122
  return;
80
123
  const pct = (target.scrollTop + target.clientHeight) / target.scrollHeight;
81
124
  if (pct > 0.8)
82
125
  loadMore();
83
126
  }
127
+ function refetch() {
128
+ const state = get(store);
129
+ return fetchData(state.search, true);
130
+ }
131
+ function cancel() {
132
+ activeController?.abort();
133
+ activeController = null;
134
+ update((s) => (s.isFetching ? { ...s, isFetching: false } : s));
135
+ }
84
136
  function setOpened(opened) {
85
137
  update((s) => ({ ...s, opened }));
86
138
  }
@@ -93,17 +145,57 @@ export function createBoxerStore(initialProps) {
93
145
  function setSelectedOptionIndex(index) {
94
146
  update((s) => ({ ...s, selectedOptionIndex: index }));
95
147
  }
148
+ function addSelectedItem(item) {
149
+ update((s) => {
150
+ const key = valueKey(item.value);
151
+ if (s.selectedItems.some((i) => valueKey(i.value) === key))
152
+ return s;
153
+ return { ...s, selectedItems: [...s.selectedItems, item] };
154
+ });
155
+ }
156
+ function removeSelectedItem(value) {
157
+ update((s) => {
158
+ const key = valueKey(value);
159
+ const next = s.selectedItems.filter((i) => valueKey(i.value) !== key);
160
+ if (next.length === s.selectedItems.length)
161
+ return s;
162
+ return { ...s, selectedItems: next };
163
+ });
164
+ }
165
+ function setSelectedItems(items) {
166
+ update((s) => ({ ...s, selectedItems: items }));
167
+ }
168
+ function clearSelectedItems() {
169
+ update((s) => (s.selectedItems.length === 0 ? s : { ...s, selectedItems: [] }));
170
+ }
96
171
  function updateProps(props) {
97
- update((s) => ({ ...s, ...props }));
172
+ update((s) => {
173
+ const merged = { ...s, ...props };
174
+ // Keep boxerData consistent with data in local mode.
175
+ if ((props.data !== undefined || props.dataSource !== undefined) &&
176
+ merged.dataSource === 'local') {
177
+ const localData = merged.data ?? [];
178
+ merged.boxerData = localData;
179
+ merged.total = localData.length;
180
+ merged.hasMore = false;
181
+ }
182
+ return merged;
183
+ });
98
184
  }
99
185
  return {
186
+ addSelectedItem,
187
+ cancel,
188
+ clearSelectedItems,
100
189
  fetchData,
101
190
  fetchMoreOnBottomReached,
102
191
  loadMore,
192
+ refetch,
193
+ removeSelectedItem,
103
194
  set,
104
195
  setInput,
105
196
  setOpened,
106
197
  setSearch,
198
+ setSelectedItems,
107
199
  setSelectedOptionIndex,
108
200
  subscribe,
109
201
  update,
@@ -24,15 +24,19 @@ export interface BoxerAdapterConfig {
24
24
  /** Custom row-to-BoxerItem mapping. When omitted, `labelField` and `valueField` are mapped automatically. */
25
25
  mapItem?: (row: any) => BoxerItem;
26
26
  }
27
+ export interface BoxerFetchParams {
28
+ page: number;
29
+ pageSize: number;
30
+ search?: string;
31
+ signal?: AbortSignal;
32
+ }
27
33
  export interface BoxerServerAdapter {
28
- fetch(params: {
29
- page: number;
30
- pageSize: number;
31
- search?: string;
32
- }): Promise<{
34
+ fetch(params: BoxerFetchParams): Promise<{
33
35
  data: BoxerItem[];
34
36
  total: number;
35
37
  }>;
38
+ /** Resolve BoxerItems for a list of selected values. Used to display labels for preloaded values not in the current page. */
39
+ resolveByValue?(values: any[], signal?: AbortSignal): Promise<BoxerItem[]>;
36
40
  }
37
41
  export type BoxerItem = {
38
42
  [key: string]: any;
@@ -45,6 +49,8 @@ export interface BoxerProps {
45
49
  clearable?: boolean;
46
50
  data?: Array<BoxerItem>;
47
51
  dataSource?: BoxerDataSource;
52
+ /** Debounce interval in ms for search-triggered fetches. Defaults to `300`. */
53
+ debounceMs?: number;
48
54
  disablePortal?: boolean;
49
55
  disabled?: boolean;
50
56
  error?: string;
@@ -54,21 +60,21 @@ export interface BoxerProps {
54
60
  mah?: number;
55
61
  multiSelect?: boolean;
56
62
  name?: string;
57
- onAPICall?: (params: {
58
- page: number;
59
- pageSize: number;
60
- search?: string;
61
- }) => Promise<{
63
+ onAPICall?: (params: BoxerFetchParams) => Promise<{
62
64
  data: Array<BoxerItem>;
63
65
  total: number;
64
66
  }>;
65
67
  onBufferChange?: (buffer: Array<BoxerItem> | BoxerItem | null) => void;
66
68
  onChange?: (value: any | Array<any>) => void;
69
+ /** Resolve labels for preloaded values that may not be in the current page (server-mode initial values). */
70
+ onResolveValues?: (values: any[], signal?: AbortSignal) => Promise<Array<BoxerItem>>;
67
71
  openOnClear?: boolean;
68
72
  pageSize?: number;
69
73
  placeholder?: string;
70
74
  rightSection?: Snippet;
71
75
  searchable?: boolean;
76
+ /** Columns to search against in `local` mode. Falls back to `['label']`. */
77
+ searchColumns?: string[];
72
78
  selectFirst?: boolean;
73
79
  showAll?: boolean;
74
80
  value?: any | Array<any>;
@@ -79,5 +85,6 @@ export interface BoxerRef {
79
85
  focus: () => void;
80
86
  getValue: () => any | Array<any>;
81
87
  open: () => void;
88
+ refetch: () => void;
82
89
  setValue: (value: any | Array<any>) => void;
83
90
  }
@@ -35,7 +35,8 @@
35
35
  isDark = nextIsDark;
36
36
  });
37
37
 
38
- editor = monaco.editor.create(container!, {
38
+ if (!container) return;
39
+ editor = monaco.editor.create(container, {
39
40
  value: '',
40
41
  language: lang,
41
42
  readOnly: readonly,
@@ -43,15 +44,16 @@
43
44
  automaticLayout: true,
44
45
  });
45
46
 
46
- editor.onDidChangeModelContent(() => {
47
+ const createdEditor = editor;
48
+ createdEditor.onDidChangeModelContent(() => {
47
49
  if (changeGuard) return;
48
- const content = editor!.getValue();
50
+ const content = createdEditor.getValue();
49
51
  onChange?.(new Blob([content]));
50
52
  });
51
53
 
52
- editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
54
+ createdEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
53
55
  if (!onSave) return;
54
- onSave(new Blob([editor!.getValue()]));
56
+ onSave(new Blob([createdEditor.getValue()]));
55
57
  });
56
58
 
57
59
  function doInsertText(text: string): void {
@@ -74,10 +76,11 @@
74
76
  // Sync incoming Blob → editor text
75
77
  $effect(() => {
76
78
  if (!editor || !value) return;
79
+ const currentEditor = editor;
77
80
  blobToString(value).then((text) => {
78
- if (editor!.getValue() !== text) {
81
+ if (currentEditor.getValue() !== text) {
79
82
  changeGuard = true;
80
- editor!.setValue(text);
83
+ currentEditor.setValue(text);
81
84
  changeGuard = false;
82
85
  }
83
86
  });
@@ -108,6 +108,7 @@ const computeIsLoggedIn = (session) => {
108
108
  }
109
109
  return !isSessionExpired(session);
110
110
  };
111
+ const VALIDATION_FAILURE_BACKOFF_MS = 5000;
111
112
  const createGlobalStateStore = () => {
112
113
  const initialState = createInitialState();
113
114
  let isStorageInitialized = false;
@@ -115,9 +116,14 @@ const createGlobalStateStore = () => {
115
116
  let operationLock = Promise.resolve();
116
117
  let hasAutoValidated = false;
117
118
  let validationScheduled = false;
119
+ let lastValidationFailureAt = 0;
118
120
  const scheduleValidation = () => {
119
121
  if (validationScheduled)
120
122
  return;
123
+ if (lastValidationFailureAt &&
124
+ Date.now() - lastValidationFailureAt < VALIDATION_FAILURE_BACKOFF_MS) {
125
+ return;
126
+ }
121
127
  validationScheduled = true;
122
128
  waitForInitialization()
123
129
  .then(() => withOperationLock(() => fetchDataInternal()))
@@ -131,9 +137,10 @@ const createGlobalStateStore = () => {
131
137
  const before = get(store);
132
138
  store.update((current) => {
133
139
  const nextPartial = typeof partial === "function" ? partial(current) : partial;
134
- return replace
135
- ? nextPartial
136
- : { ...current, ...nextPartial };
140
+ if (replace) {
141
+ return { ...actions, ...nextPartial };
142
+ }
143
+ return { ...current, ...nextPartial };
137
144
  });
138
145
  const after = get(store);
139
146
  const handlerJustRegistered = !hasAutoValidated &&
@@ -260,6 +267,7 @@ const createGlobalStateStore = () => {
260
267
  }
261
268
  catch (e) {
262
269
  const error = `Load Exception: ${String(e)}`;
270
+ lastValidationFailureAt = Date.now();
263
271
  setGlobalState((state) => ({
264
272
  session: {
265
273
  ...state.session,
@@ -268,7 +276,9 @@ const createGlobalStateStore = () => {
268
276
  loading: false,
269
277
  },
270
278
  }));
279
+ return;
271
280
  }
281
+ lastValidationFailureAt = 0;
272
282
  };
273
283
  const actions = {
274
284
  fetchData: async (url) => {
@@ -345,10 +355,12 @@ const createGlobalStateStore = () => {
345
355
  return withOperationLock(async () => {
346
356
  const previousState = getState();
347
357
  try {
358
+ const blank = createInitialState();
348
359
  setGlobalState((state) => ({
349
- ...createInitialState(),
360
+ ...blank,
361
+ initialized: true,
350
362
  session: {
351
- ...createInitialState().session,
363
+ ...blank.session,
352
364
  apiURL: state.session.apiURL,
353
365
  expiryDate: undefined,
354
366
  loading: true,
@@ -485,6 +497,7 @@ const createGlobalStateStore = () => {
485
497
  isStorageInitialized = true;
486
498
  initializationPromise = null;
487
499
  });
500
+ let lastPersistedState;
488
501
  store.subscribe((state) => {
489
502
  if (!isStorageInitialized) {
490
503
  return;
@@ -495,9 +508,11 @@ const createGlobalStateStore = () => {
495
508
  typeof state.onFetchSession === "function") {
496
509
  scheduleValidation();
497
510
  }
498
- saveStorage(toPersistedState(state)).catch((e) => {
511
+ const nextPersisted = toPersistedState(state);
512
+ saveStorage(nextPersisted, lastPersistedState).catch((e) => {
499
513
  console.error("Error saving storage:", e);
500
514
  });
515
+ lastPersistedState = nextPersisted;
501
516
  });
502
517
  return {
503
518
  getState,
@@ -1,4 +1,4 @@
1
1
  import type { PersistedGlobalState } from './GlobalStateStore.types';
2
2
  declare function loadStorage(): Promise<Partial<PersistedGlobalState>>;
3
- declare function saveStorage(state: PersistedGlobalState): Promise<void>;
3
+ declare function saveStorage(state: PersistedGlobalState, previous?: Partial<PersistedGlobalState>): Promise<void>;
4
4
  export { loadStorage, saveStorage };
@@ -75,8 +75,10 @@ async function loadStorage() {
75
75
  }
76
76
  return result;
77
77
  }
78
- async function saveStorage(state) {
78
+ async function saveStorage(state, previous) {
79
79
  for (const key of PERSIST_KEYS) {
80
+ if (previous && previous[key] === state[key])
81
+ continue;
80
82
  const storageKey = `${STORAGE_KEY}:${key}`;
81
83
  const filtered = filterState(state[key], key);
82
84
  const serialized = JSON.stringify(filtered);
@@ -40,6 +40,12 @@
40
40
  let lastFetchTime = 0;
41
41
  let fetchInProgress = false;
42
42
  let mounted = false;
43
+ let storeInitialized = $state(GetGlobalState().initialized);
44
+ let lastHandlers: {
45
+ onFetchSession?: Props['onFetchSession'];
46
+ onLogin?: Props['onLogin'];
47
+ onLogout?: Props['onLogout'];
48
+ } = {};
43
49
 
44
50
  const throttledFetch = async (url?: string): Promise<void> => {
45
51
  const now = Date.now();
@@ -67,30 +73,40 @@
67
73
  };
68
74
 
69
75
  $effect(() => {
76
+ const unsubscribe = GlobalStateStore.subscribe((state) => {
77
+ storeInitialized = state.initialized;
78
+ });
79
+ return unsubscribe;
80
+ });
81
+
82
+ $effect(() => {
83
+ if (!storeInitialized) return;
70
84
  if (apiURL) {
71
85
  GlobalStateStore.getState().setApiURL(apiURL);
72
86
  }
73
87
  });
74
88
 
75
89
  $effect(() => {
90
+ if (!storeInitialized) return;
76
91
  if (program) {
77
92
  GlobalStateStore.getState().setProgram(program);
78
93
  }
79
94
  });
80
95
 
81
96
  $effect(() => {
82
- GlobalStateStore.setState({ onFetchSession });
83
- });
84
-
85
- $effect(() => {
86
- GlobalStateStore.setState({ onLogin });
87
- });
88
-
89
- $effect(() => {
90
- GlobalStateStore.setState({ onLogout });
97
+ if (
98
+ onFetchSession === lastHandlers.onFetchSession &&
99
+ onLogin === lastHandlers.onLogin &&
100
+ onLogout === lastHandlers.onLogout
101
+ ) {
102
+ return;
103
+ }
104
+ lastHandlers = { onFetchSession, onLogin, onLogout };
105
+ GlobalStateStore.setState({ onFetchSession, onLogin, onLogout });
91
106
  });
92
107
 
93
108
  $effect(() => {
109
+ if (!storeInitialized) return;
94
110
  if (!mounted) {
95
111
  mounted = true;
96
112
  if (autoFetch && fetchOnMount) {
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  // @ts-nocheck
3
3
  import type { Snippet } from "svelte";
4
- import { untrack } from "svelte";
5
4
  import type {
6
5
  GridlerProps,
7
6
  GridlerColumn,
@@ -74,7 +73,7 @@
74
73
  sortOrder,
75
74
  onFilterChange: _onFilterChange,
76
75
  filters: _filters,
77
- selectedItems,
76
+ selectedItems: _selectedItems,
78
77
  onSelectedItemsChange: _onSelectedItemsChange,
79
78
  getRowData,
80
79
  settings,
@@ -526,8 +526,19 @@
526
526
 
527
527
  type Adapter = GridlerResolveSpecAdapter | GridlerRestHeaderSpecAdapter;
528
528
 
529
+ const serverAdapterKey = $derived(
530
+ `${dataSource}|${resolvedUrl}|${resolvedToken}|${resolvedSchema}|${resolvedEntity}|${resolvedUniqueID}`,
531
+ );
532
+
533
+ let _serverAdapterInstance = $state<Adapter | null>(null);
534
+ let _serverAdapterKey = $state<string>("");
535
+
529
536
  const serverAdapter = $derived.by((): Adapter | null => {
530
537
  if (!resolvedUrl || !resolvedSchema || !resolvedEntity) return null;
538
+ const key = serverAdapterKey;
539
+ if (key === untrack(() => _serverAdapterKey) && untrack(() => _serverAdapterInstance)) {
540
+ return untrack(() => _serverAdapterInstance);
541
+ }
531
542
  const config: GridlerAdapterConfig = {
532
543
  url: resolvedUrl,
533
544
  token: resolvedToken,
@@ -535,9 +546,13 @@
535
546
  entity: resolvedEntity,
536
547
  uniqueID: resolvedUniqueID,
537
548
  };
538
- return dataSource === "headerspec"
539
- ? new GridlerRestHeaderSpecAdapter(config)
540
- : new GridlerResolveSpecAdapter(config);
549
+ const instance =
550
+ dataSource === "headerspec"
551
+ ? new GridlerRestHeaderSpecAdapter(config)
552
+ : new GridlerResolveSpecAdapter(config);
553
+ _serverAdapterInstance = instance;
554
+ _serverAdapterKey = key;
555
+ return instance;
541
556
  });
542
557
 
543
558
  function buildAllFilters(search: string | null): FilterOption[] {
@@ -661,7 +676,10 @@
661
676
  if (serverFetchVersion !== versionSnapshot) return;
662
677
  serverData = [...serverData, ...result.data];
663
678
  serverCursor = result.nextCursor;
664
- serverAllLoaded = result.data.length < pageSize || !result.nextCursor;
679
+ serverAllLoaded =
680
+ result.data.length < pageSize ||
681
+ !result.nextCursor ||
682
+ result.nextCursor === cursorSnapshot;
665
683
  onGridEvent?.("page_loaded", undefined, undefined, undefined, { data: result.data, total: serverData.length });
666
684
  } catch (e) {
667
685
  const msg = e instanceof Error ? e.message : "Failed to load page";
@@ -11,11 +11,12 @@ export const CaptureScreen = async () => {
11
11
  displaySurface: "browser",
12
12
  },
13
13
  });
14
- video = document.createElement("video");
15
- video.srcObject = stream;
14
+ const createdVideo = document.createElement("video");
15
+ video = createdVideo;
16
+ createdVideo.srcObject = stream;
16
17
  await new Promise((resolve) => {
17
- video.onloadedmetadata = () => {
18
- void video.play();
18
+ createdVideo.onloadedmetadata = () => {
19
+ void createdVideo.play();
19
20
  resolve();
20
21
  };
21
22
  });