@rozenite/storage-plugin 2.2.0 → 2.3.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 (90) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/devtools/assets/panel-CcThj-0Z.js +32 -0
  3. package/dist/devtools/assets/panel-DTYbHqnH.css +1 -0
  4. package/dist/devtools/panel.html +2 -2
  5. package/dist/react-native/cjs/package.json +3 -0
  6. package/dist/react-native/cjs/react-native.js +38 -0
  7. package/dist/react-native/cjs/src/react-native/adapters/async-storage.js +60 -0
  8. package/dist/react-native/cjs/src/react-native/adapters/index.js +10 -0
  9. package/dist/react-native/cjs/src/react-native/adapters/mmkv.js +143 -0
  10. package/dist/react-native/cjs/src/react-native/adapters/secure-storage.js +51 -0
  11. package/dist/react-native/cjs/src/react-native/entry-preview-pagination.js +198 -0
  12. package/dist/react-native/cjs/src/react-native/export-snapshot.js +70 -0
  13. package/dist/react-native/cjs/src/react-native/full-entry-request.js +72 -0
  14. package/dist/react-native/cjs/src/react-native/import.js +185 -0
  15. package/dist/react-native/cjs/src/react-native/storage-discovery.js +69 -0
  16. package/dist/react-native/cjs/src/react-native/storage-view.js +128 -0
  17. package/dist/react-native/cjs/src/react-native/useRozeniteStoragePlugin.js +145 -0
  18. package/dist/react-native/cjs/src/react-native/useStorageAgentTools.js +228 -0
  19. package/dist/react-native/cjs/src/shared/agent-tools.js +119 -0
  20. package/dist/react-native/cjs/src/shared/entry-preview.js +48 -0
  21. package/dist/react-native/cjs/src/shared/messaging.js +2 -0
  22. package/dist/react-native/cjs/src/shared/snapshot.js +185 -0
  23. package/dist/react-native/cjs/src/shared/types.js +13 -0
  24. package/dist/react-native/package.json +3 -0
  25. package/dist/react-native/react-native.d.ts +7 -0
  26. package/dist/react-native/react-native.js +40 -0
  27. package/dist/react-native/src/react-native/adapters/async-storage.d.ts +34 -0
  28. package/dist/react-native/src/react-native/adapters/async-storage.js +56 -0
  29. package/dist/react-native/src/react-native/adapters/index.d.ts +6 -0
  30. package/dist/react-native/src/react-native/adapters/index.js +3 -0
  31. package/dist/react-native/src/react-native/adapters/mmkv.d.ts +13 -0
  32. package/dist/react-native/src/react-native/adapters/mmkv.js +139 -0
  33. package/dist/react-native/src/react-native/adapters/secure-storage.d.ts +18 -0
  34. package/dist/react-native/src/react-native/adapters/secure-storage.js +47 -0
  35. package/dist/react-native/src/react-native/entry-preview-pagination.d.ts +7 -0
  36. package/dist/react-native/src/react-native/entry-preview-pagination.js +194 -0
  37. package/dist/react-native/src/react-native/export-snapshot.d.ts +5 -0
  38. package/dist/react-native/src/react-native/export-snapshot.js +66 -0
  39. package/dist/react-native/src/react-native/full-entry-request.d.ts +5 -0
  40. package/dist/react-native/src/react-native/full-entry-request.js +68 -0
  41. package/dist/react-native/src/react-native/import.d.ts +7 -0
  42. package/dist/react-native/src/react-native/import.js +180 -0
  43. package/dist/react-native/src/react-native/storage-discovery.d.ts +5 -0
  44. package/dist/react-native/src/react-native/storage-discovery.js +65 -0
  45. package/dist/react-native/src/react-native/storage-view.d.ts +19 -0
  46. package/dist/react-native/src/react-native/storage-view.js +123 -0
  47. package/dist/react-native/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
  48. package/dist/react-native/src/react-native/useRozeniteStoragePlugin.js +141 -0
  49. package/dist/react-native/src/react-native/useStorageAgentTools.d.ts +37 -0
  50. package/dist/react-native/src/react-native/useStorageAgentTools.js +223 -0
  51. package/dist/react-native/src/shared/agent-tools.d.ts +76 -0
  52. package/dist/react-native/src/shared/agent-tools.js +116 -0
  53. package/dist/react-native/src/shared/entry-preview.d.ts +4 -0
  54. package/dist/react-native/src/shared/entry-preview.js +44 -0
  55. package/dist/react-native/src/shared/messaging.d.ts +134 -0
  56. package/dist/react-native/src/shared/messaging.js +1 -0
  57. package/dist/react-native/src/shared/snapshot.d.ts +53 -0
  58. package/dist/react-native/src/shared/snapshot.js +179 -0
  59. package/dist/react-native/src/shared/types.d.ts +78 -0
  60. package/dist/react-native/src/shared/types.js +8 -0
  61. package/dist/rozenite.json +1 -1
  62. package/dist/sdk/package.json +3 -0
  63. package/dist/sdk/sdk.d.ts +44 -0
  64. package/dist/sdk/sdk.js +9 -0
  65. package/dist/sdk/src/shared/agent-tools.d.ts +76 -0
  66. package/dist/sdk/src/shared/agent-tools.js +119 -0
  67. package/dist/sdk/src/shared/types.d.ts +78 -0
  68. package/dist/sdk/src/shared/types.js +13 -0
  69. package/package.json +16 -16
  70. package/react-native.ts +8 -1
  71. package/rozenite.config.ts +1 -0
  72. package/src/__tests__/release-bundle.test.ts +32 -0
  73. package/dist/devtools/assets/panel-D5z1BY8U.js +0 -32
  74. package/dist/devtools/assets/panel-DVlWRg6d.css +0 -1
  75. package/dist/react-native/chunks/async-storage.require.cjs +0 -1
  76. package/dist/react-native/chunks/async-storage.require.js +0 -53
  77. package/dist/react-native/chunks/index.require.cjs +0 -1
  78. package/dist/react-native/chunks/index.require.js +0 -103
  79. package/dist/react-native/chunks/secure-storage.require.cjs +0 -1
  80. package/dist/react-native/chunks/secure-storage.require.js +0 -46
  81. package/dist/react-native/chunks/types.cjs +0 -1
  82. package/dist/react-native/chunks/types.js +0 -11
  83. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +0 -1
  84. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +0 -1037
  85. package/dist/react-native/index.cjs +0 -1
  86. package/dist/react-native/index.d.ts +0 -365
  87. package/dist/react-native/index.js +0 -28
  88. package/dist/sdk/index.cjs +0 -1
  89. package/dist/sdk/index.d.ts +0 -206
  90. package/dist/sdk/index.js +0 -122
@@ -0,0 +1,34 @@
1
+ import type { StorageAdapter } from '../../shared/types';
2
+ export type AsyncStorageLike = {
3
+ getAllKeys: () => Promise<readonly string[]>;
4
+ getItem: (key: string) => Promise<string | null>;
5
+ setItem: (key: string, value: string) => Promise<void>;
6
+ removeItem: (key: string) => Promise<void>;
7
+ clear: () => Promise<void>;
8
+ };
9
+ export type AsyncStorageInstanceConfig = {
10
+ storage: AsyncStorageLike;
11
+ name?: string;
12
+ blacklist?: RegExp;
13
+ };
14
+ type SingleStorageOptions = {
15
+ storage: AsyncStorageLike;
16
+ adapterId?: string;
17
+ adapterName?: string;
18
+ storageId?: string;
19
+ storageName?: string;
20
+ blacklist?: RegExp;
21
+ };
22
+ type MultiStorageOptions = {
23
+ storages: Record<string, AsyncStorageLike | AsyncStorageInstanceConfig>;
24
+ adapterId?: string;
25
+ adapterName?: string;
26
+ };
27
+ export type CreateAsyncStorageAdapterOptions = SingleStorageOptions | MultiStorageOptions;
28
+ export declare const createAsyncStorageAdapter: (options: CreateAsyncStorageAdapterOptions) => StorageAdapter;
29
+ /**
30
+ * @deprecated Use createAsyncStorageAdapter instead.
31
+ */
32
+ export declare const createExpoAsyncStorageAdapter: (options: CreateAsyncStorageAdapterOptions) => StorageAdapter;
33
+ export type CreateExpoAsyncStorageAdapterOptions = CreateAsyncStorageAdapterOptions;
34
+ export {};
@@ -0,0 +1,56 @@
1
+ const toStorageNode = (storageId, config, fallbackName, fallbackBlacklist) => {
2
+ const resolved = 'storage' in config
3
+ ? config
4
+ : {
5
+ storage: config,
6
+ };
7
+ const { storage, name, blacklist } = resolved;
8
+ return {
9
+ id: storageId,
10
+ name: name ?? fallbackName ?? storageId,
11
+ blacklist: blacklist ?? fallbackBlacklist,
12
+ capabilities: {
13
+ supportedTypes: ['string'],
14
+ },
15
+ storage: {
16
+ kind: 'async',
17
+ getAllKeys: () => storage.getAllKeys(),
18
+ get: async (key) => {
19
+ const value = await storage.getItem(key);
20
+ if (value === null) {
21
+ return undefined;
22
+ }
23
+ return {
24
+ key,
25
+ type: 'string',
26
+ value,
27
+ };
28
+ },
29
+ set: async (entry) => {
30
+ if (entry.type !== 'string') {
31
+ throw new Error('AsyncStorage adapter supports only string values.');
32
+ }
33
+ await storage.setItem(entry.key, entry.value);
34
+ },
35
+ delete: (key) => storage.removeItem(key),
36
+ clear: () => storage.clear(),
37
+ },
38
+ };
39
+ };
40
+ export const createAsyncStorageAdapter = (options) => {
41
+ const { adapterId = 'async-storage', adapterName = 'AsyncStorage' } = options;
42
+ const storageNodes = 'storages' in options
43
+ ? Object.entries(options.storages).map(([storageId, config]) => toStorageNode(storageId, config))
44
+ : [
45
+ toStorageNode(options.storageId ?? 'default', options.storage, options.storageName ?? 'Default Storage', options.blacklist),
46
+ ];
47
+ return {
48
+ id: adapterId,
49
+ name: adapterName,
50
+ storages: storageNodes,
51
+ };
52
+ };
53
+ /**
54
+ * @deprecated Use createAsyncStorageAdapter instead.
55
+ */
56
+ export const createExpoAsyncStorageAdapter = createAsyncStorageAdapter;
@@ -0,0 +1,6 @@
1
+ export { createMMKVStorageAdapter } from './mmkv';
2
+ export type { CreateMMKVStorageAdapterOptions } from './mmkv';
3
+ export { createAsyncStorageAdapter, createExpoAsyncStorageAdapter } from './async-storage';
4
+ export type { AsyncStorageInstanceConfig, AsyncStorageLike, CreateAsyncStorageAdapterOptions, CreateExpoAsyncStorageAdapterOptions, } from './async-storage';
5
+ export { createExpoSecureStorageAdapter } from './secure-storage';
6
+ export type { CreateExpoSecureStorageAdapterOptions, SecureStorageLike } from './secure-storage';
@@ -0,0 +1,3 @@
1
+ export { createMMKVStorageAdapter } from './mmkv';
2
+ export { createAsyncStorageAdapter, createExpoAsyncStorageAdapter } from './async-storage';
3
+ export { createExpoSecureStorageAdapter } from './secure-storage';
@@ -0,0 +1,13 @@
1
+ import type { MMKV as MMKVV3 } from 'react-native-mmkv-v3';
2
+ import type { MMKV as MMKVV4 } from 'react-native-mmkv-v4';
3
+ import type { StorageAdapter } from '../../shared/types';
4
+ type MMKV = MMKVV3 | MMKVV4;
5
+ export type MMKVBlacklistConfig = RegExp | Record<string, RegExp | undefined>;
6
+ export type CreateMMKVStorageAdapterOptions = {
7
+ adapterId?: string;
8
+ adapterName?: string;
9
+ storages: MMKV[] | Record<string, MMKV>;
10
+ blacklist?: MMKVBlacklistConfig;
11
+ };
12
+ export declare const createMMKVStorageAdapter: ({ adapterId, adapterName, storages, blacklist, }: CreateMMKVStorageAdapterOptions) => StorageAdapter;
13
+ export {};
@@ -0,0 +1,139 @@
1
+ import { DEFAULT_SUPPORTED_TYPES } from '../../shared/types';
2
+ const isMMKVV4 = (mmkv) => 'remove' in mmkv;
3
+ const normalizeStorages = (storages) => {
4
+ if (Array.isArray(storages)) {
5
+ const isAnyStorageV4 = storages.some(isMMKVV4);
6
+ if (isAnyStorageV4) {
7
+ throw new Error('[Rozenite] Storage Plugin: MMKV arrays are not supported for v4 storages. Pass a record of storage IDs and MMKV instances.');
8
+ }
9
+ return Object.fromEntries(storages.map((storage) => [storage['id'], storage]));
10
+ }
11
+ return storages;
12
+ };
13
+ const getMMKVAdapter = (mmkv) => {
14
+ const clear = () => mmkv.clearAll();
15
+ if (isMMKVV4(mmkv)) {
16
+ return {
17
+ set: (key, value) => mmkv.set(key, value),
18
+ getBoolean: (key) => mmkv.getBoolean(key),
19
+ getString: (key) => mmkv.getString(key),
20
+ getNumber: (key) => mmkv.getNumber(key),
21
+ getBuffer: (key) => mmkv.getBuffer(key),
22
+ delete: (key) => mmkv.remove(key),
23
+ getAllKeys: () => mmkv.getAllKeys(),
24
+ clear,
25
+ addOnValueChangedListener: (callback) => mmkv.addOnValueChangedListener(callback),
26
+ };
27
+ }
28
+ return {
29
+ set: (key, value) => mmkv.set(key, value),
30
+ getBoolean: (key) => mmkv.getBoolean(key),
31
+ getString: (key) => mmkv.getString(key),
32
+ getNumber: (key) => mmkv.getNumber(key),
33
+ getBuffer: (key) => mmkv.getBuffer(key),
34
+ delete: (key) => mmkv.delete(key),
35
+ getAllKeys: () => mmkv.getAllKeys(),
36
+ clear,
37
+ addOnValueChangedListener: (callback) => mmkv.addOnValueChangedListener(callback),
38
+ };
39
+ };
40
+ // MMKV's typed getters can disagree on the same key — on some platforms
41
+ // `getString` lenient-decodes invalid-UTF-8 buffer bytes to `""`, and
42
+ // `getNumber` reinterprets 8-byte buffers as IEEE 754 doubles without
43
+ // honoring the original `setBuffer` write. `getBuffer` is more reliably
44
+ // strict (only returns for keys actually written via `setBuffer`), so
45
+ // we consult it ahead of the numeric getters to break ties in favour of
46
+ // the bytes that are actually on disk.
47
+ //
48
+ // 1. Non-empty string — the common case. Buffer bytes that happen to
49
+ // be valid UTF-8 surface here too (documented trade-off); users
50
+ // disambiguate at edit time via the Hex editor.
51
+ // 2. Non-empty buffer — catches `setBuffer` payloads regardless of
52
+ // byte count and regardless of whether the other typed getters
53
+ // also return spurious values.
54
+ // 3. Number, then boolean — for keys actually written via `setNumber`
55
+ // / `setBoolean`. `getBuffer` will have returned `undefined` for
56
+ // those keys, so the chain falls through.
57
+ // 4. Empty string — intentional `setString(key, "")` lands here once
58
+ // we've ruled out a non-empty buffer payload at the same key.
59
+ const getEntry = (adapter, key) => {
60
+ const stringValue = adapter.getString(key);
61
+ if (stringValue !== undefined && stringValue.length > 0) {
62
+ return { key, type: 'string', value: stringValue };
63
+ }
64
+ const bufferValue = adapter.getBuffer(key);
65
+ if (bufferValue !== undefined && bufferValue.byteLength > 0) {
66
+ return {
67
+ key,
68
+ type: 'buffer',
69
+ value: Array.from(new Uint8Array(bufferValue)),
70
+ };
71
+ }
72
+ const numberValue = adapter.getNumber(key);
73
+ if (numberValue !== undefined) {
74
+ return { key, type: 'number', value: numberValue };
75
+ }
76
+ const booleanValue = adapter.getBoolean(key);
77
+ if (booleanValue !== undefined) {
78
+ return { key, type: 'boolean', value: booleanValue };
79
+ }
80
+ if (stringValue !== undefined) {
81
+ return { key, type: 'string', value: stringValue };
82
+ }
83
+ return undefined;
84
+ };
85
+ const setEntry = (adapter, entry) => {
86
+ if (entry.type === 'buffer') {
87
+ adapter.set(entry.key, new Uint8Array(entry.value).buffer);
88
+ }
89
+ else {
90
+ adapter.set(entry.key, entry.value);
91
+ }
92
+ };
93
+ const getStorageBlacklist = (config, storageId) => {
94
+ if (!config) {
95
+ return undefined;
96
+ }
97
+ if (config instanceof RegExp) {
98
+ return config;
99
+ }
100
+ return config[storageId];
101
+ };
102
+ const createStorageBlacklistMatcher = (config, storageId) => {
103
+ if (!(config instanceof RegExp)) {
104
+ return undefined;
105
+ }
106
+ return (key) => {
107
+ config.lastIndex = 0;
108
+ return config.test(`${storageId}:${key}`);
109
+ };
110
+ };
111
+ export const createMMKVStorageAdapter = ({ adapterId = 'mmkv', adapterName = 'MMKV', storages, blacklist, }) => {
112
+ const normalizedStorages = normalizeStorages(storages);
113
+ const storageNodes = Object.entries(normalizedStorages).map(([storageId, storage]) => {
114
+ const mmkv = getMMKVAdapter(storage);
115
+ return {
116
+ id: storageId,
117
+ name: storageId,
118
+ blacklist: getStorageBlacklist(blacklist, storageId),
119
+ shouldFilterKey: createStorageBlacklistMatcher(blacklist, storageId),
120
+ capabilities: {
121
+ supportedTypes: DEFAULT_SUPPORTED_TYPES,
122
+ },
123
+ storage: {
124
+ kind: 'sync',
125
+ getAllKeys: () => mmkv.getAllKeys(),
126
+ get: (key) => getEntry(mmkv, key),
127
+ set: (entry) => setEntry(mmkv, entry),
128
+ delete: (key) => mmkv.delete(key),
129
+ clear: mmkv.clear,
130
+ subscribe: (callback) => mmkv.addOnValueChangedListener(callback),
131
+ },
132
+ };
133
+ });
134
+ return {
135
+ id: adapterId,
136
+ name: adapterName,
137
+ storages: storageNodes,
138
+ };
139
+ };
@@ -0,0 +1,18 @@
1
+ import type { StorageAdapter } from '../../shared/types';
2
+ export type SecureStorageLike = {
3
+ getItemAsync: (key: string) => Promise<string | null>;
4
+ setItemAsync: (key: string, value: string) => Promise<void>;
5
+ deleteItemAsync: (key: string) => Promise<void>;
6
+ };
7
+ type KeySource = string[] | (() => Promise<string[]>);
8
+ export type CreateExpoSecureStorageAdapterOptions = {
9
+ storage: SecureStorageLike;
10
+ keys: KeySource;
11
+ adapterId?: string;
12
+ adapterName?: string;
13
+ storageId?: string;
14
+ storageName?: string;
15
+ blacklist?: RegExp;
16
+ };
17
+ export declare const createExpoSecureStorageAdapter: ({ storage, keys, adapterId, adapterName, storageId, storageName, blacklist, }: CreateExpoSecureStorageAdapterOptions) => StorageAdapter;
18
+ export {};
@@ -0,0 +1,47 @@
1
+ const resolveKeys = async (source) => {
2
+ if (Array.isArray(source)) {
3
+ return source;
4
+ }
5
+ return source();
6
+ };
7
+ export const createExpoSecureStorageAdapter = ({ storage, keys, adapterId = 'expo-secure-store', adapterName = 'Expo SecureStore', storageId = 'default', storageName = 'Default Secure Storage', blacklist, }) => {
8
+ const storageNode = {
9
+ id: storageId,
10
+ name: storageName,
11
+ blacklist,
12
+ capabilities: {
13
+ supportedTypes: ['string'],
14
+ },
15
+ storage: {
16
+ kind: 'async',
17
+ getAllKeys: () => resolveKeys(keys),
18
+ get: async (key) => {
19
+ const value = await storage.getItemAsync(key);
20
+ if (value === null) {
21
+ return undefined;
22
+ }
23
+ return {
24
+ key,
25
+ type: 'string',
26
+ value,
27
+ };
28
+ },
29
+ set: async (entry) => {
30
+ if (entry.type !== 'string') {
31
+ throw new Error('Expo SecureStore adapter supports only string values.');
32
+ }
33
+ await storage.setItemAsync(entry.key, entry.value);
34
+ },
35
+ delete: (key) => storage.deleteItemAsync(key),
36
+ clear: async () => {
37
+ const storageKeys = await resolveKeys(keys);
38
+ await Promise.all(storageKeys.map((key) => storage.deleteItemAsync(key)));
39
+ },
40
+ },
41
+ };
42
+ return {
43
+ id: adapterId,
44
+ name: adapterName,
45
+ storages: [storageNode],
46
+ };
47
+ };
@@ -0,0 +1,7 @@
1
+ import type { StorageListEntryPreviewsResponseEvent, StorageRequestErrorEvent } from '../shared/messaging';
2
+ import type { StorageView } from './storage-view';
3
+ export declare const MAX_ENTRY_PREVIEW_PAGE_SIZE = 100;
4
+ export declare const ENTRY_PREVIEW_READ_CONCURRENCY = 4;
5
+ type PaginationResult = StorageListEntryPreviewsResponseEvent | StorageRequestErrorEvent;
6
+ export declare const handleListEntryPreviewsRequest: (views: readonly StorageView[], payload: unknown) => Promise<PaginationResult>;
7
+ export {};
@@ -0,0 +1,194 @@
1
+ import { toStorageEntryPreview } from '../shared/entry-preview';
2
+ export const MAX_ENTRY_PREVIEW_PAGE_SIZE = 100;
3
+ export const ENTRY_PREVIEW_READ_CONCURRENCY = 4;
4
+ const getRequestId = (payload) => typeof payload === 'object' &&
5
+ payload != null &&
6
+ !Array.isArray(payload) &&
7
+ typeof payload.requestId === 'string'
8
+ ? payload.requestId
9
+ : '';
10
+ const isTarget = (value) => typeof value === 'object' &&
11
+ value != null &&
12
+ !Array.isArray(value) &&
13
+ typeof value.adapterId === 'string' &&
14
+ value.adapterId.trim().length > 0 &&
15
+ typeof value.storageId === 'string' &&
16
+ value.storageId.trim().length > 0;
17
+ const normalizeSearch = (search) => (search ?? '').trim().toLowerCase();
18
+ const validateRequest = (payload) => {
19
+ if (typeof payload !== 'object' ||
20
+ payload == null ||
21
+ Array.isArray(payload) ||
22
+ payload.type !== 'list-entry-previews' ||
23
+ typeof payload.requestId !== 'string' ||
24
+ payload.requestId.trim().length === 0 ||
25
+ !isTarget(payload.target) ||
26
+ (payload.search !== undefined &&
27
+ typeof payload.search !== 'string') ||
28
+ (payload.cursor !== undefined &&
29
+ typeof payload.cursor !== 'string') ||
30
+ !Number.isFinite(payload.limit) ||
31
+ !Number.isInteger(payload.limit) ||
32
+ payload.limit < 1) {
33
+ return undefined;
34
+ }
35
+ const keySortDirection = payload.keySortDirection;
36
+ if (keySortDirection !== undefined &&
37
+ keySortDirection !== 'ascending' &&
38
+ keySortDirection !== 'descending') {
39
+ return undefined;
40
+ }
41
+ const request = payload;
42
+ return {
43
+ ...request,
44
+ search: normalizeSearch(request.search),
45
+ keySortDirection: keySortDirection ?? 'ascending',
46
+ limit: Math.min(request.limit, MAX_ENTRY_PREVIEW_PAGE_SIZE),
47
+ };
48
+ };
49
+ // Cursor contents are intentionally private to the device protocol. Callers only
50
+ // receive and return this opaque token; its representation may change freely.
51
+ const encodeCursor = (cursor) => btoa(encodeURIComponent(JSON.stringify(cursor)));
52
+ const decodeCursor = (value) => {
53
+ try {
54
+ const parsed = JSON.parse(decodeURIComponent(atob(value)));
55
+ if (typeof parsed !== 'object' ||
56
+ parsed == null ||
57
+ Array.isArray(parsed) ||
58
+ parsed.version !== 1 ||
59
+ !isTarget(parsed.target) ||
60
+ typeof parsed.search !== 'string' ||
61
+ (parsed.keySortDirection !== 'ascending' &&
62
+ parsed.keySortDirection !== 'descending') ||
63
+ typeof parsed.boundaryKey !== 'string' ||
64
+ (parsed.direction !== 'next' &&
65
+ parsed.direction !== 'previous')) {
66
+ return undefined;
67
+ }
68
+ return parsed;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ };
74
+ const isSameTarget = (left, right) => left.adapterId === right.adapterId && left.storageId === right.storageId;
75
+ const compareKeys = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
76
+ const readPreviews = async (view, keys) => {
77
+ const previews = new Array(keys.length);
78
+ let nextIndex = 0;
79
+ const worker = async () => {
80
+ while (nextIndex < keys.length) {
81
+ const index = nextIndex;
82
+ nextIndex += 1;
83
+ const entry = await view.get(keys[index]);
84
+ if (entry) {
85
+ previews[index] = toStorageEntryPreview(entry);
86
+ }
87
+ }
88
+ };
89
+ await Promise.all(Array.from({ length: Math.min(ENTRY_PREVIEW_READ_CONCURRENCY, keys.length) }, worker));
90
+ return previews.filter((preview) => preview != null);
91
+ };
92
+ export const handleListEntryPreviewsRequest = async (views, payload) => {
93
+ const requestId = getRequestId(payload);
94
+ const request = validateRequest(payload);
95
+ if (!request) {
96
+ return {
97
+ type: 'storage-request-error',
98
+ requestId,
99
+ code: 'INVALID_REQUEST',
100
+ message: 'Entry preview requests require a valid target, request ID, and positive integer limit.',
101
+ };
102
+ }
103
+ const view = views.find((candidate) => isSameTarget(candidate.target, request.target));
104
+ if (!view) {
105
+ return {
106
+ type: 'storage-request-error',
107
+ requestId,
108
+ code: 'TARGET_NOT_FOUND',
109
+ message: 'The requested storage target was not found.',
110
+ };
111
+ }
112
+ let cursor;
113
+ if (request.cursor !== undefined) {
114
+ cursor = decodeCursor(request.cursor);
115
+ if (!cursor ||
116
+ !isSameTarget(cursor.target, request.target) ||
117
+ cursor.search !== request.search ||
118
+ cursor.keySortDirection !== request.keySortDirection) {
119
+ return {
120
+ type: 'storage-request-error',
121
+ requestId,
122
+ code: 'INVALID_CURSOR',
123
+ message: 'The entry preview cursor is invalid or belongs to a different query.',
124
+ resetPagination: true,
125
+ };
126
+ }
127
+ }
128
+ try {
129
+ const keys = (await view.getAllKeys())
130
+ .filter((key) => key.toLowerCase().includes(request.search))
131
+ .sort(compareKeys);
132
+ if (request.keySortDirection === 'descending') {
133
+ keys.reverse();
134
+ }
135
+ let start = 0;
136
+ if (cursor) {
137
+ const boundaryIndex = keys.indexOf(cursor.boundaryKey);
138
+ if (boundaryIndex === -1) {
139
+ return {
140
+ type: 'storage-request-error',
141
+ requestId,
142
+ code: 'INVALID_CURSOR',
143
+ message: 'The entry preview cursor is stale after storage changes.',
144
+ resetPagination: true,
145
+ };
146
+ }
147
+ start =
148
+ cursor.direction === 'next'
149
+ ? boundaryIndex + 1
150
+ : Math.max(0, boundaryIndex - request.limit);
151
+ }
152
+ const pageKeys = keys.slice(start, start + request.limit);
153
+ const items = await readPreviews(view, pageKeys);
154
+ const end = start + pageKeys.length;
155
+ const cursorBase = {
156
+ version: 1,
157
+ target: request.target,
158
+ search: request.search,
159
+ keySortDirection: request.keySortDirection,
160
+ };
161
+ return {
162
+ type: 'entry-previews',
163
+ requestId,
164
+ target: request.target,
165
+ items,
166
+ ...(end < keys.length && pageKeys.length > 0
167
+ ? {
168
+ nextCursor: encodeCursor({
169
+ ...cursorBase,
170
+ boundaryKey: pageKeys.at(-1),
171
+ direction: 'next',
172
+ }),
173
+ }
174
+ : {}),
175
+ ...(start > 0 && pageKeys.length > 0
176
+ ? {
177
+ previousCursor: encodeCursor({
178
+ ...cursorBase,
179
+ boundaryKey: pageKeys[0],
180
+ direction: 'previous',
181
+ }),
182
+ }
183
+ : {}),
184
+ };
185
+ }
186
+ catch {
187
+ return {
188
+ type: 'storage-request-error',
189
+ requestId,
190
+ code: 'READ_FAILED',
191
+ message: 'Failed to read entry previews from the requested storage.',
192
+ };
193
+ }
194
+ };
@@ -0,0 +1,5 @@
1
+ import type { StorageExportSnapshotResponseEvent, StorageRequestErrorEvent } from '../shared/messaging';
2
+ import type { StorageView } from './storage-view';
3
+ type ExportSnapshotResult = StorageExportSnapshotResponseEvent | StorageRequestErrorEvent;
4
+ export declare const handleExportSnapshotRequest: (views: readonly StorageView[], payload: unknown) => Promise<ExportSnapshotResult>;
5
+ export {};
@@ -0,0 +1,66 @@
1
+ import { buildSnapshot } from '../shared/snapshot';
2
+ const getRequestId = (payload) => typeof payload === 'object' &&
3
+ payload != null &&
4
+ !Array.isArray(payload) &&
5
+ typeof payload.requestId === 'string'
6
+ ? payload.requestId
7
+ : '';
8
+ const isTarget = (value) => typeof value === 'object' &&
9
+ value != null &&
10
+ !Array.isArray(value) &&
11
+ typeof value.adapterId === 'string' &&
12
+ value.adapterId.trim().length > 0 &&
13
+ typeof value.storageId === 'string' &&
14
+ value.storageId.trim().length > 0;
15
+ const isSameTarget = (left, right) => left.adapterId === right.adapterId && left.storageId === right.storageId;
16
+ const validateRequest = (payload) => typeof payload === 'object' &&
17
+ payload != null &&
18
+ !Array.isArray(payload) &&
19
+ payload.type === 'export-snapshot' &&
20
+ typeof payload.requestId === 'string' &&
21
+ payload.requestId.trim().length > 0 &&
22
+ isTarget(payload.target);
23
+ export const handleExportSnapshotRequest = async (views, payload) => {
24
+ const requestId = getRequestId(payload);
25
+ if (!validateRequest(payload)) {
26
+ return {
27
+ type: 'storage-request-error',
28
+ requestId,
29
+ code: 'INVALID_REQUEST',
30
+ message: 'Export requests require a valid target and request ID.',
31
+ };
32
+ }
33
+ const view = views.find((candidate) => isSameTarget(candidate.target, payload.target));
34
+ if (!view) {
35
+ return {
36
+ type: 'storage-request-error',
37
+ requestId,
38
+ code: 'TARGET_NOT_FOUND',
39
+ message: 'The requested storage target was not found.',
40
+ };
41
+ }
42
+ try {
43
+ // getAllEntries applies the storage view's blacklist before reading values.
44
+ const entries = await view.getAllEntries();
45
+ return {
46
+ type: 'export-snapshot-result',
47
+ requestId,
48
+ target: view.target,
49
+ snapshot: buildSnapshot({
50
+ target: view.target,
51
+ adapterName: view.adapterName,
52
+ storageName: view.storageName,
53
+ capabilities: view.capabilities,
54
+ entries,
55
+ }),
56
+ };
57
+ }
58
+ catch {
59
+ return {
60
+ type: 'storage-request-error',
61
+ requestId,
62
+ code: 'READ_FAILED',
63
+ message: 'Failed to export the requested storage.',
64
+ };
65
+ }
66
+ };
@@ -0,0 +1,5 @@
1
+ import type { StorageGetEntryResponseEvent, StorageRequestErrorEvent } from '../shared/messaging';
2
+ import type { StorageView } from './storage-view';
3
+ type FullEntryResult = StorageGetEntryResponseEvent | StorageRequestErrorEvent;
4
+ export declare const handleFullEntryRequest: (views: readonly StorageView[], payload: unknown) => Promise<FullEntryResult>;
5
+ export {};
@@ -0,0 +1,68 @@
1
+ const getRequestId = (payload) => typeof payload === 'object' &&
2
+ payload != null &&
3
+ !Array.isArray(payload) &&
4
+ typeof payload.requestId === 'string'
5
+ ? payload.requestId
6
+ : '';
7
+ const isTarget = (value) => typeof value === 'object' &&
8
+ value != null &&
9
+ !Array.isArray(value) &&
10
+ typeof value.adapterId === 'string' &&
11
+ value.adapterId.trim().length > 0 &&
12
+ typeof value.storageId === 'string' &&
13
+ value.storageId.trim().length > 0;
14
+ const isSameTarget = (left, right) => left.adapterId === right.adapterId && left.storageId === right.storageId;
15
+ const validateRequest = (payload) => typeof payload === 'object' &&
16
+ payload != null &&
17
+ !Array.isArray(payload) &&
18
+ payload.type === 'get-entry' &&
19
+ typeof payload.requestId === 'string' &&
20
+ payload.requestId.trim().length > 0 &&
21
+ isTarget(payload.target) &&
22
+ typeof payload.key === 'string';
23
+ export const handleFullEntryRequest = async (views, payload) => {
24
+ const requestId = getRequestId(payload);
25
+ if (!validateRequest(payload)) {
26
+ return {
27
+ type: 'storage-request-error',
28
+ requestId,
29
+ code: 'INVALID_REQUEST',
30
+ message: 'Full entry requests require a valid target, request ID, and string key.',
31
+ };
32
+ }
33
+ const view = views.find((candidate) => isSameTarget(candidate.target, payload.target));
34
+ if (!view) {
35
+ return {
36
+ type: 'storage-request-error',
37
+ requestId,
38
+ code: 'TARGET_NOT_FOUND',
39
+ message: 'The requested storage target was not found.',
40
+ };
41
+ }
42
+ try {
43
+ // StorageView applies the same blacklist policy used by preview requests.
44
+ const entry = await view.get(payload.key);
45
+ if (!entry) {
46
+ return {
47
+ type: 'storage-request-error',
48
+ requestId,
49
+ code: 'ENTRY_NOT_FOUND',
50
+ message: 'The requested storage entry was not found.',
51
+ };
52
+ }
53
+ return {
54
+ type: 'entry',
55
+ requestId,
56
+ target: view.target,
57
+ entry,
58
+ };
59
+ }
60
+ catch {
61
+ return {
62
+ type: 'storage-request-error',
63
+ requestId,
64
+ code: 'READ_FAILED',
65
+ message: 'Failed to read the requested storage entry.',
66
+ };
67
+ }
68
+ };