@rozenite/storage-plugin 1.13.0 → 2.1.0

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
  4. package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/async-storage.require.cjs +1 -1
  7. package/dist/react-native/chunks/async-storage.require.js +8 -7
  8. package/dist/react-native/chunks/index.require.cjs +1 -1
  9. package/dist/react-native/chunks/index.require.js +70 -66
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +17 -13
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
  14. package/dist/react-native/index.d.ts +166 -18
  15. package/dist/rozenite.json +1 -1
  16. package/dist/sdk/index.cjs +1 -1
  17. package/dist/sdk/index.d.ts +13 -5
  18. package/dist/sdk/index.js +21 -12
  19. package/package.json +35 -37
  20. package/react-native.ts +12 -22
  21. package/rozenite.config.ts +74 -0
  22. package/sdk.ts +1 -5
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
  26. package/src/react-native/__tests__/import.test.ts +174 -11
  27. package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +140 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
  31. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  32. package/src/react-native/adapters/async-storage.ts +8 -9
  33. package/src/react-native/adapters/index.ts +2 -8
  34. package/src/react-native/adapters/mmkv.ts +12 -24
  35. package/src/react-native/adapters/secure-storage.ts +4 -0
  36. package/src/react-native/entry-preview-pagination.ts +261 -0
  37. package/src/react-native/export-snapshot.ts +88 -0
  38. package/src/react-native/full-entry-request.ts +91 -0
  39. package/src/react-native/import.ts +179 -16
  40. package/src/react-native/storage-discovery.ts +92 -0
  41. package/src/react-native/storage-view.ts +47 -109
  42. package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
  43. package/src/react-native/useStorageAgentTools.ts +196 -91
  44. package/src/shared/__tests__/entry-preview.test.ts +99 -0
  45. package/src/shared/__tests__/snapshot.test.ts +31 -32
  46. package/src/shared/agent-tools.ts +33 -38
  47. package/src/shared/entry-preview.ts +54 -0
  48. package/src/shared/messaging.ts +140 -21
  49. package/src/shared/snapshot.ts +12 -18
  50. package/src/shared/types.ts +21 -4
  51. package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
  52. package/src/ui/__tests__/binary.test.ts +31 -33
  53. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  54. package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
  55. package/src/ui/__tests__/panel.test.tsx +382 -0
  56. package/src/ui/__tests__/storage-groups.test.ts +88 -0
  57. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  58. package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
  59. package/src/ui/add-entry-dialog.tsx +66 -117
  60. package/src/ui/binary-value-editor-state.ts +16 -25
  61. package/src/ui/binary-value-editor.tsx +59 -47
  62. package/src/ui/binary.ts +54 -15
  63. package/src/ui/edit-entry-dialog.tsx +68 -104
  64. package/src/ui/editor-switcher.tsx +9 -16
  65. package/src/ui/entry-detail-dialog.tsx +62 -181
  66. package/src/ui/format-value.tsx +25 -0
  67. package/src/ui/globals.css +1 -123
  68. package/src/ui/import-dialog.tsx +72 -109
  69. package/src/ui/large-value-viewer-state.ts +25 -0
  70. package/src/ui/large-value-viewer.tsx +65 -0
  71. package/src/ui/panel.tsx +649 -509
  72. package/src/ui/query-client.tsx +23 -0
  73. package/src/ui/storage-groups.ts +66 -0
  74. package/src/ui/type-conversion.ts +2 -5
  75. package/src/ui/typed-value-editor.tsx +15 -20
  76. package/src/ui/use-storage-entries.ts +192 -0
  77. package/src/ui/utils.ts +1 -4
  78. package/tsconfig.json +5 -1
  79. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  80. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  81. package/postcss.config.js +0 -6
  82. package/src/ui/confirm-dialog.tsx +0 -100
  83. package/src/ui/editable-table.tsx +0 -300
  84. package/tailwind.config.ts +0 -94
@@ -0,0 +1,23 @@
1
+ import { QueryClient, QueryClientProvider, type QueryClientConfig } from '@tanstack/react-query';
2
+ import { type ReactNode, useEffect, useState } from 'react';
3
+
4
+ const storageQueryClientConfig: QueryClientConfig = {
5
+ defaultOptions: {
6
+ queries: {
7
+ gcTime: 0,
8
+ refetchOnReconnect: false,
9
+ refetchOnWindowFocus: false,
10
+ retry: false,
11
+ },
12
+ },
13
+ };
14
+
15
+ export const createStorageQueryClient = () => new QueryClient(storageQueryClientConfig);
16
+
17
+ export const StorageQueryClientProvider = ({ children }: { children: ReactNode }) => {
18
+ const [queryClient] = useState(createStorageQueryClient);
19
+
20
+ useEffect(() => () => queryClient.clear(), [queryClient]);
21
+
22
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
23
+ };
@@ -0,0 +1,66 @@
1
+ import type { StorageTarget } from '../shared/types';
2
+
3
+ export type StorageSnapshotEntry = {
4
+ target: StorageTarget;
5
+ adapterName: string;
6
+ storageName: string;
7
+ entryCount: number;
8
+ };
9
+
10
+ export type StorageSidebarItem = {
11
+ viewId: string;
12
+ storageName: string;
13
+ entryCount: number;
14
+ };
15
+
16
+ export type StorageSidebarGroup = {
17
+ adapterId: string;
18
+ adapterName: string;
19
+ items: StorageSidebarItem[];
20
+ };
21
+
22
+ /**
23
+ * Groups storage snapshots by `target.adapterId` for the sidebar, preserving
24
+ * the order in which each adapter and storage first appeared.
25
+ */
26
+ export const buildStorageSidebarGroups = (
27
+ snapshots: Map<string, StorageSnapshotEntry>,
28
+ ): StorageSidebarGroup[] => {
29
+ const groups = new Map<string, StorageSidebarGroup>();
30
+
31
+ for (const [viewId, snapshot] of snapshots) {
32
+ const { adapterId } = snapshot.target;
33
+ let group = groups.get(adapterId);
34
+
35
+ if (!group) {
36
+ group = { adapterId, adapterName: snapshot.adapterName, items: [] };
37
+ groups.set(adapterId, group);
38
+ }
39
+
40
+ group.items.push({
41
+ viewId,
42
+ storageName: snapshot.storageName,
43
+ entryCount: snapshot.entryCount,
44
+ });
45
+ }
46
+
47
+ return [...groups.values()];
48
+ };
49
+
50
+ /**
51
+ * Splits a storage view id (`${adapterId}:${storageId}`) back into its
52
+ * target, using the first `:` as the separator since `storageId` may itself
53
+ * contain colons. Returns `null` for a malformed id.
54
+ */
55
+ export const parseStorageViewId = (viewId: string): StorageTarget | null => {
56
+ const separatorIndex = viewId.indexOf(':');
57
+
58
+ if (separatorIndex < 0) {
59
+ return null;
60
+ }
61
+
62
+ return {
63
+ adapterId: viewId.slice(0, separatorIndex),
64
+ storageId: viewId.slice(separatorIndex + 1),
65
+ };
66
+ };
@@ -15,8 +15,7 @@ const tryDecode = (bytes: readonly number[]): string => {
15
15
  }
16
16
  };
17
17
 
18
- const encode = (value: string): number[] =>
19
- Array.from(textEncoder.encode(value));
18
+ const encode = (value: string): number[] => Array.from(textEncoder.encode(value));
20
19
 
21
20
  // Convert a value between storage primitive types for the editor
22
21
  // switcher. The headline transition is `string` ↔ `buffer`, which
@@ -89,9 +88,7 @@ export const convertValue = (
89
88
 
90
89
  // Zero value for a type — used when seeding the add-entry dialog and
91
90
  // when conversion has no meaningful starting point.
92
- export const defaultValueForType = (
93
- type: StorageEntryType,
94
- ): StorageEntryValue => {
91
+ export const defaultValueForType = (type: StorageEntryType): StorageEntryValue => {
95
92
  switch (type) {
96
93
  case 'string':
97
94
  return '';
@@ -1,3 +1,4 @@
1
+ import { Input, Select } from '@rozenite/ui';
1
2
  import type { StorageEntryType, StorageEntryValue } from '../shared/types';
2
3
  import { BinaryValueEditor } from './binary-value-editor';
3
4
  import { EditorSwitcher } from './editor-switcher';
@@ -41,12 +42,8 @@ export const TypedValueEditor = ({
41
42
  };
42
43
 
43
44
  return (
44
- <div className="space-y-2">
45
- <EditorSwitcher
46
- supportedTypes={supportedTypes}
47
- value={type}
48
- onChange={handleTypeChange}
49
- />
45
+ <div className="flex flex-col gap-2">
46
+ <EditorSwitcher supportedTypes={supportedTypes} value={type} onChange={handleTypeChange} />
50
47
 
51
48
  {type === 'buffer' ? (
52
49
  <BinaryValueEditor
@@ -54,20 +51,20 @@ export const TypedValueEditor = ({
54
51
  onChange={(bytes) => onChange('buffer', bytes)}
55
52
  />
56
53
  ) : type === 'boolean' ? (
57
- <select
58
- id={inputId}
54
+ <Select
59
55
  value={String(value ?? false)}
60
- onChange={(event) =>
61
- onChange('boolean', event.target.value === 'true')
62
- }
63
- className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
64
- autoFocus={autoFocus}
56
+ onValueChange={(next) => onChange('boolean', next === 'true')}
65
57
  >
66
- <option value="true">true</option>
67
- <option value="false">false</option>
68
- </select>
58
+ <Select.Trigger id={inputId} autoFocus={autoFocus}>
59
+ <Select.Value />
60
+ </Select.Trigger>
61
+ <Select.Content>
62
+ <Select.Item value="true">true</Select.Item>
63
+ <Select.Item value="false">false</Select.Item>
64
+ </Select.Content>
65
+ </Select>
69
66
  ) : type === 'number' ? (
70
- <input
67
+ <Input
71
68
  id={inputId}
72
69
  type="number"
73
70
  value={String(value ?? '')}
@@ -77,17 +74,15 @@ export const TypedValueEditor = ({
77
74
  onChange('number', Number.isNaN(parsed) ? 0 : parsed);
78
75
  }}
79
76
  placeholder="Enter number value"
80
- className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
81
77
  autoFocus={autoFocus}
82
78
  />
83
79
  ) : (
84
- <input
80
+ <Input
85
81
  id={inputId}
86
82
  type="text"
87
83
  value={String(value ?? '')}
88
84
  onChange={(event) => onChange('string', event.target.value)}
89
85
  placeholder="Enter string value"
90
- className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
91
86
  autoFocus={autoFocus}
92
87
  />
93
88
  )}
@@ -0,0 +1,192 @@
1
+ import {
2
+ keepPreviousData,
3
+ type InfiniteData,
4
+ type QueryClient,
5
+ useInfiniteQuery,
6
+ useQuery,
7
+ useQueryClient,
8
+ } from '@tanstack/react-query';
9
+ import type { RozeniteDevToolsRequestClient } from '@rozenite/plugin-bridge';
10
+ import { useCallback, useEffect, useMemo } from 'react';
11
+ import type { StorageEventMap, StorageListEntryPreviewsResponseEvent } from '../shared/messaging';
12
+ import type { StorageEntryPreview, StorageTarget } from '../shared/types';
13
+
14
+ export const STORAGE_ENTRY_PREVIEW_PAGE_SIZE = 100;
15
+ export const STORAGE_ENTRY_PREVIEW_MAX_PAGES = 5;
16
+
17
+ type StorageRequestClient = Pick<RozeniteDevToolsRequestClient<StorageEventMap>, 'request'>;
18
+
19
+ export const normalizeStorageKeySearch = (search: string) => search.trim().toLowerCase();
20
+
21
+ export const storageEntryPreviewQueryKeyPrefix = (target: StorageTarget) =>
22
+ ['storage-entry-previews', target.adapterId, target.storageId] as const;
23
+
24
+ export const storageEntryPreviewQueryKey = (
25
+ target: StorageTarget,
26
+ search: string,
27
+ keySortDirection: 'ascending' | 'descending',
28
+ ) =>
29
+ [
30
+ ...storageEntryPreviewQueryKeyPrefix(target),
31
+ normalizeStorageKeySearch(search),
32
+ keySortDirection,
33
+ ] as const;
34
+
35
+ const storageFullEntryQueryKeyPrefix = (target: StorageTarget) =>
36
+ ['storage-full-entry', target.adapterId, target.storageId] as const;
37
+
38
+ const storageFullEntryQueryKey = (target: StorageTarget, key: string) =>
39
+ [...storageFullEntryQueryKeyPrefix(target), key] as const;
40
+
41
+ export const resetSelectedStorageQueries = async (
42
+ queryClient: QueryClient,
43
+ target: StorageTarget,
44
+ ) => {
45
+ const previewQueryKey = storageEntryPreviewQueryKeyPrefix(target);
46
+ const fullEntryQueryKey = storageFullEntryQueryKeyPrefix(target);
47
+
48
+ await queryClient.cancelQueries({ queryKey: previewQueryKey });
49
+ await queryClient.cancelQueries({ queryKey: fullEntryQueryKey });
50
+ queryClient.removeQueries({ queryKey: fullEntryQueryKey });
51
+ await queryClient.resetQueries({ queryKey: previewQueryKey });
52
+ };
53
+
54
+ export const invalidateStorageEntryPreviews = async (
55
+ queryClient: QueryClient,
56
+ target: StorageTarget,
57
+ ) =>
58
+ queryClient.invalidateQueries({
59
+ queryKey: storageEntryPreviewQueryKeyPrefix(target),
60
+ });
61
+
62
+ export const dropStorageFullEntries = async (
63
+ queryClient: QueryClient,
64
+ target: StorageTarget,
65
+ key?: string,
66
+ ) => {
67
+ const queryKey =
68
+ key == null ? storageFullEntryQueryKeyPrefix(target) : storageFullEntryQueryKey(target, key);
69
+
70
+ await queryClient.cancelQueries({ queryKey, exact: key != null });
71
+ queryClient.removeQueries({ queryKey, exact: key != null });
72
+ };
73
+
74
+ export type UseStorageEntryPreviewsOptions = {
75
+ client: StorageRequestClient | null | undefined;
76
+ target: StorageTarget | null | undefined;
77
+ search: string;
78
+ keySortDirection: 'ascending' | 'descending';
79
+ pageSize?: number;
80
+ maxPages?: number;
81
+ };
82
+
83
+ export const useStorageEntryPreviews = ({
84
+ client,
85
+ target,
86
+ search,
87
+ keySortDirection,
88
+ pageSize = STORAGE_ENTRY_PREVIEW_PAGE_SIZE,
89
+ maxPages = STORAGE_ENTRY_PREVIEW_MAX_PAGES,
90
+ }: UseStorageEntryPreviewsOptions) => {
91
+ const normalizedSearch = normalizeStorageKeySearch(search);
92
+ const queryKey = useMemo(
93
+ () =>
94
+ target
95
+ ? storageEntryPreviewQueryKey(target, normalizedSearch, keySortDirection)
96
+ : (['storage-entry-previews', 'no-target'] as const),
97
+ [keySortDirection, normalizedSearch, target?.adapterId, target?.storageId],
98
+ );
99
+
100
+ return useInfiniteQuery({
101
+ queryKey,
102
+ enabled: client != null && target != null,
103
+ initialPageParam: undefined as string | undefined,
104
+ maxPages: Math.max(1, maxPages),
105
+ placeholderData: keepPreviousData,
106
+ queryFn: ({ pageParam, signal }) => {
107
+ if (!client || !target) {
108
+ throw new Error('A storage target and request client are required.');
109
+ }
110
+
111
+ return client.request({
112
+ requestType: 'list-entry-previews',
113
+ responseType: 'entry-previews',
114
+ errorType: 'storage-request-error',
115
+ payload: {
116
+ type: 'list-entry-previews',
117
+ target,
118
+ search: normalizedSearch || undefined,
119
+ keySortDirection,
120
+ cursor: pageParam,
121
+ limit: pageSize,
122
+ },
123
+ signal,
124
+ });
125
+ },
126
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
127
+ getPreviousPageParam: (firstPage) => firstPage.previousCursor,
128
+ refetchOnWindowFocus: false,
129
+ });
130
+ };
131
+
132
+ export const flattenStorageEntryPreviewPages = (
133
+ data: InfiniteData<StorageListEntryPreviewsResponseEvent> | undefined,
134
+ ): StorageEntryPreview[] => data?.pages.flatMap((page) => page.items) ?? [];
135
+
136
+ export type UseStorageFullEntryOptions = {
137
+ client: StorageRequestClient | null | undefined;
138
+ target: StorageTarget | null | undefined;
139
+ key: string | null | undefined;
140
+ enabled?: boolean;
141
+ };
142
+
143
+ export const useStorageFullEntry = ({
144
+ client,
145
+ target,
146
+ key,
147
+ enabled = true,
148
+ }: UseStorageFullEntryOptions) => {
149
+ const queryClient = useQueryClient();
150
+ const queryKey = useMemo(
151
+ () =>
152
+ target && key != null
153
+ ? storageFullEntryQueryKey(target, key)
154
+ : (['storage-full-entry', 'no-entry'] as const),
155
+ [key, target?.adapterId, target?.storageId],
156
+ );
157
+
158
+ const result = useQuery({
159
+ queryKey,
160
+ enabled: enabled && client != null && target != null && key != null,
161
+ gcTime: 0,
162
+ queryFn: ({ signal }) => {
163
+ if (!client || !target || key == null) {
164
+ throw new Error('A storage target, key, and request client are required.');
165
+ }
166
+
167
+ return client.request({
168
+ requestType: 'get-entry',
169
+ responseType: 'entry',
170
+ errorType: 'storage-request-error',
171
+ payload: { type: 'get-entry', target, key },
172
+ signal,
173
+ });
174
+ },
175
+ refetchOnWindowFocus: false,
176
+ });
177
+
178
+ useEffect(
179
+ () => () => {
180
+ void queryClient.cancelQueries({ queryKey });
181
+ queryClient.removeQueries({ queryKey });
182
+ },
183
+ [queryClient, queryKey],
184
+ );
185
+
186
+ const reset = useCallback(() => {
187
+ void queryClient.cancelQueries({ queryKey });
188
+ queryClient.removeQueries({ queryKey });
189
+ }, [queryClient, queryKey]);
190
+
191
+ return { ...result, entry: result.data?.entry, reset };
192
+ };
package/src/ui/utils.ts CHANGED
@@ -15,10 +15,7 @@ export const downloadJson = (data: unknown, filename: string): void => {
15
15
 
16
16
  const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, '-');
17
17
 
18
- export const buildExportFilename = (
19
- target: StorageTarget,
20
- now: Date = new Date(),
21
- ): string => {
18
+ export const buildExportFilename = (target: StorageTarget, now: Date = new Date()): string => {
22
19
  const yyyy = now.getFullYear().toString().padStart(4, '0');
23
20
  const mm = (now.getMonth() + 1).toString().padStart(2, '0');
24
21
  const dd = now.getDate().toString().padStart(2, '0');
package/tsconfig.json CHANGED
@@ -15,7 +15,8 @@
15
15
  "paths": {
16
16
  "@rozenite/agent-bridge": ["../agent-bridge/src/index.ts"],
17
17
  "@rozenite/agent-shared": ["../agent-shared/src/index.ts"],
18
- "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"]
18
+ "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"],
19
+ "@rozenite/ui": ["../ui/src/index.ts"]
19
20
  },
20
21
  "resolveJsonModule": true,
21
22
  "isolatedModules": true,
@@ -33,6 +34,9 @@
33
34
  },
34
35
  {
35
36
  "path": "../vite-plugin"
37
+ },
38
+ {
39
+ "path": "../ui"
36
40
  }
37
41
  ]
38
42
  }