strata-storage 2.6.1 → 2.7.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.
Files changed (143) hide show
  1. package/AI-INTEGRATION-GUIDE.md +129 -11
  2. package/README.md +42 -64
  3. package/android/AGENTS.md +18 -3
  4. package/android/CLAUDE.md +36 -3
  5. package/android/build.gradle +5 -1
  6. package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
  7. package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
  8. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
  9. package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
  10. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
  11. package/dist/README.md +42 -64
  12. package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
  13. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  14. package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
  15. package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
  16. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
  18. package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
  19. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  20. package/dist/adapters/capacitor/SecureAdapter.js +6 -2
  21. package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
  22. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  23. package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
  24. package/dist/adapters/capacitor/index.d.ts +4 -4
  25. package/dist/adapters/web/CacheAdapter.d.ts +2 -2
  26. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  27. package/dist/adapters/web/CacheAdapter.js +20 -5
  28. package/dist/adapters/web/CookieAdapter.d.ts +2 -2
  29. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  30. package/dist/adapters/web/CookieAdapter.js +27 -3
  31. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  32. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  33. package/dist/adapters/web/IndexedDBAdapter.js +19 -4
  34. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  35. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  36. package/dist/adapters/web/LocalStorageAdapter.js +15 -1
  37. package/dist/adapters/web/MemoryAdapter.d.ts +9 -2
  38. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  39. package/dist/adapters/web/MemoryAdapter.js +22 -5
  40. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  41. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  42. package/dist/adapters/web/SessionStorageAdapter.js +17 -3
  43. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  44. package/dist/adapters/web/index.d.ts +7 -7
  45. package/dist/capacitor.d.ts +6 -6
  46. package/dist/capacitor.d.ts.map +1 -1
  47. package/dist/capacitor.js +5 -4
  48. package/dist/core/AdapterRegistry.d.ts +1 -1
  49. package/dist/core/BaseAdapter.d.ts +21 -5
  50. package/dist/core/BaseAdapter.d.ts.map +1 -1
  51. package/dist/core/BaseAdapter.js +29 -1
  52. package/dist/core/Strata.d.ts +53 -7
  53. package/dist/core/Strata.d.ts.map +1 -1
  54. package/dist/core/Strata.js +581 -125
  55. package/dist/features/compression/index.d.ts +1 -1
  56. package/dist/features/compression.d.ts +28 -9
  57. package/dist/features/compression.d.ts.map +1 -1
  58. package/dist/features/compression.js +164 -77
  59. package/dist/features/encryption/index.d.ts +1 -1
  60. package/dist/features/encryption.d.ts +55 -2
  61. package/dist/features/encryption.d.ts.map +1 -1
  62. package/dist/features/encryption.js +139 -26
  63. package/dist/features/migration.d.ts +1 -1
  64. package/dist/features/migration.d.ts.map +1 -1
  65. package/dist/features/migration.js +7 -2
  66. package/dist/features/observer/index.d.ts +1 -1
  67. package/dist/features/observer.d.ts +1 -1
  68. package/dist/features/query/index.d.ts +1 -1
  69. package/dist/features/query.d.ts +2 -1
  70. package/dist/features/query.d.ts.map +1 -1
  71. package/dist/features/query.js +32 -7
  72. package/dist/features/sync/index.d.ts +1 -1
  73. package/dist/features/sync.d.ts +19 -3
  74. package/dist/features/sync.d.ts.map +1 -1
  75. package/dist/features/sync.js +75 -12
  76. package/dist/features/ttl/index.d.ts +1 -1
  77. package/dist/features/ttl.d.ts +2 -2
  78. package/dist/features/ttl.d.ts.map +1 -1
  79. package/dist/features/ttl.js +4 -2
  80. package/dist/firebase.d.ts +1 -1
  81. package/dist/firebase.d.ts.map +1 -1
  82. package/dist/firebase.js +72 -10
  83. package/dist/index.d.ts +24 -22
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +6 -1
  86. package/dist/integrations/angular/index.d.ts +2 -2
  87. package/dist/integrations/angular/index.d.ts.map +1 -1
  88. package/dist/integrations/angular/index.js +7 -8
  89. package/dist/integrations/react/index.d.ts +3 -3
  90. package/dist/integrations/react/index.d.ts.map +1 -1
  91. package/dist/integrations/react/index.js +16 -4
  92. package/dist/integrations/vue/index.d.ts +2 -2
  93. package/dist/integrations/vue/index.d.ts.map +1 -1
  94. package/dist/integrations/vue/index.js +5 -2
  95. package/dist/package.json +11 -2
  96. package/dist/plugin/definitions.d.ts +29 -3
  97. package/dist/plugin/definitions.d.ts.map +1 -1
  98. package/dist/plugin/index.d.ts +2 -2
  99. package/dist/plugin/index.d.ts.map +1 -1
  100. package/dist/plugin/index.js +3 -0
  101. package/dist/plugin/web.d.ts +11 -2
  102. package/dist/plugin/web.d.ts.map +1 -1
  103. package/dist/plugin/web.js +23 -4
  104. package/dist/types/index.d.ts +50 -20
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/types/index.js +1 -10
  107. package/dist/utils/index.d.ts +5 -0
  108. package/dist/utils/index.d.ts.map +1 -1
  109. package/dist/utils/index.js +7 -0
  110. package/ios/AGENTS.md +14 -3
  111. package/ios/CLAUDE.md +25 -3
  112. package/ios/Plugin/FilesystemStorage.swift +19 -0
  113. package/ios/Plugin/SQLiteStorage.swift +59 -9
  114. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  115. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  116. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  117. package/package.json +27 -24
  118. package/scripts/build.js +46 -27
  119. package/scripts/cli.js +1 -1
  120. package/scripts/configure.js +2 -2
  121. package/scripts/postinstall.js +3 -2
  122. package/dist/android/AGENTS.md +0 -51
  123. package/dist/android/CLAUDE.md +0 -89
  124. package/dist/android/build.gradle +0 -58
  125. package/dist/android/proguard-rules.pro +0 -11
  126. package/dist/android/settings.gradle +0 -2
  127. package/dist/android/src/main/AndroidManifest.xml +0 -2
  128. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  129. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  130. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  131. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  132. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  133. package/dist/core/StorageStrategy.d.ts +0 -55
  134. package/dist/core/StorageStrategy.d.ts.map +0 -1
  135. package/dist/core/StorageStrategy.js +0 -200
  136. package/dist/ios/AGENTS.md +0 -48
  137. package/dist/ios/CLAUDE.md +0 -84
  138. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  139. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  140. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  141. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  142. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  143. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -1,287 +0,0 @@
1
- package com.strata.storage;
2
-
3
- import android.content.Context;
4
- import android.util.Log;
5
- import com.getcapacitor.JSObject;
6
- import java.io.File;
7
- import java.io.FileOutputStream;
8
- import java.io.IOException;
9
- import java.nio.charset.StandardCharsets;
10
- import java.net.URLDecoder;
11
- import java.net.URLEncoder;
12
- import java.util.ArrayList;
13
- import java.util.List;
14
- import java.util.UUID;
15
- import org.json.JSONException;
16
-
17
- /**
18
- * Filesystem-backed storage. One file per key lives under
19
- * {@code <filesDir>/strata_storage}. The file name is a reversible
20
- * URL-encoding of the key (no path separators survive encoding) and the file
21
- * contents are the JSON-serialized FULL wrapper object ({@code value, created,
22
- * updated, expires?, tags?, metadata?}), mirroring the SQLite value-shape
23
- * contract.
24
- *
25
- * <p>Writes are atomic: a temp file is written then renamed over the target.
26
- * All access is synchronized on the instance so overlapping bridge calls are
27
- * safe.
28
- */
29
- public class FilesystemStorage {
30
- private static final String DIR_NAME = "strata_storage";
31
- private static final String ENCODING = "UTF-8";
32
- /**
33
- * Reserved subdirectory (inside the storage dir) that holds in-flight temp
34
- * files for atomic writes. Because it is a directory, the file-only
35
- * enumeration in keys()/size()/clear() skips it automatically, and temp
36
- * names therefore can never collide with an encoded key file in the storage
37
- * dir (e.g. a real key {@code "backup.tmp"}).
38
- */
39
- private static final String STAGING_DIR_NAME = ".strata-staging";
40
-
41
- private final File baseDir;
42
- private final File stagingDir;
43
-
44
- public FilesystemStorage(Context context) {
45
- this.baseDir = new File(context.getFilesDir(), DIR_NAME);
46
- this.stagingDir = new File(baseDir, STAGING_DIR_NAME);
47
- ensureDir();
48
- }
49
-
50
- private synchronized void ensureDir() {
51
- if (!baseDir.exists()) {
52
- // mkdirs() returns false if it already exists due to a race; the
53
- // subsequent exists() check below is the real guard.
54
- baseDir.mkdirs();
55
- }
56
- if (!stagingDir.exists()) {
57
- stagingDir.mkdirs();
58
- }
59
- }
60
-
61
- /** Reversibly encode a key into a path-separator-free file name. */
62
- private static String encodeKey(String key) {
63
- try {
64
- return URLEncoder.encode(key, ENCODING);
65
- } catch (Exception e) {
66
- // UTF-8 is always supported; fall back defensively.
67
- return key.replaceAll("[^A-Za-z0-9_-]", "_");
68
- }
69
- }
70
-
71
- /** Inverse of {@link #encodeKey(String)}. */
72
- private static String decodeKey(String fileName) {
73
- try {
74
- return URLDecoder.decode(fileName, ENCODING);
75
- } catch (Exception e) {
76
- return fileName;
77
- }
78
- }
79
-
80
- private File fileForKey(String key) {
81
- return new File(baseDir, encodeKey(key));
82
- }
83
-
84
- /**
85
- * Read + parse the full wrapper for {@code key}. Missing file → {@code null}.
86
- * Unparseable contents → {@code null} (treated as a miss; never throws).
87
- */
88
- public synchronized JSObject get(String key) {
89
- File file = fileForKey(key);
90
- if (!file.exists() || !file.isFile()) {
91
- return null;
92
- }
93
- try {
94
- String json = readFile(file);
95
- return new JSObject(json);
96
- } catch (JSONException parseError) {
97
- Log.w("StrataStorage", "Unparseable filesystem value for key " + key);
98
- return null;
99
- } catch (IOException ioError) {
100
- Log.e("StrataStorage", "Failed to read filesystem value for key " + key, ioError);
101
- return null;
102
- }
103
- }
104
-
105
- /**
106
- * Atomically persist the full wrapper for {@code key}: write a temp file
107
- * then rename it over the destination.
108
- */
109
- public synchronized boolean set(String key, JSObject wrapper) {
110
- if (wrapper == null) {
111
- return false;
112
- }
113
- ensureDir();
114
- File target = fileForKey(key);
115
- // Temp file lives in the staging subdir (same filesystem → atomic
116
- // rename) with a UUID name, so it can never collide with a key file.
117
- File tmp = new File(stagingDir, UUID.randomUUID().toString() + ".tmp");
118
- FileOutputStream fos = null;
119
- try {
120
- fos = new FileOutputStream(tmp);
121
- fos.write(wrapper.toString().getBytes(StandardCharsets.UTF_8));
122
- fos.flush();
123
- fos.getFD().sync();
124
- fos.close();
125
- fos = null;
126
-
127
- // Rename is atomic on the same filesystem. Remove a stale target
128
- // first since File.renameTo can fail when the destination exists.
129
- if (target.exists() && !target.delete()) {
130
- Log.w("StrataStorage", "Could not delete existing file before rename: " + key);
131
- }
132
- if (!tmp.renameTo(target)) {
133
- tmp.delete();
134
- return false;
135
- }
136
- return true;
137
- } catch (IOException e) {
138
- Log.e("StrataStorage", "Failed to set filesystem value for key " + key, e);
139
- if (tmp.exists()) {
140
- tmp.delete();
141
- }
142
- return false;
143
- } finally {
144
- if (fos != null) {
145
- try {
146
- fos.close();
147
- } catch (IOException ignored) {
148
- // best-effort close
149
- }
150
- }
151
- }
152
- }
153
-
154
- public synchronized boolean remove(String key) {
155
- File file = fileForKey(key);
156
- if (!file.exists()) {
157
- return false;
158
- }
159
- return file.delete();
160
- }
161
-
162
- /**
163
- * Delete all stored entries. {@code prefix}, when non-null, restricts the
164
- * delete to keys whose (decoded) name starts with the prefix.
165
- */
166
- public synchronized boolean clear(String prefix) {
167
- File[] files = baseDir.listFiles();
168
- if (files == null) {
169
- return true;
170
- }
171
- boolean ok = true;
172
- for (File file : files) {
173
- // Skips the staging subdirectory (and any other non-file entry).
174
- if (!file.isFile()) {
175
- continue;
176
- }
177
- if (prefix == null || decodeKey(file.getName()).startsWith(prefix)) {
178
- if (!file.delete()) {
179
- ok = false;
180
- }
181
- }
182
- }
183
- // On a full clear, also drop any orphaned in-flight temp files.
184
- if (prefix == null) {
185
- File[] temps = stagingDir.listFiles();
186
- if (temps != null) {
187
- for (File t : temps) {
188
- t.delete();
189
- }
190
- }
191
- }
192
- return ok;
193
- }
194
-
195
- public synchronized List<String> keys(String pattern) {
196
- List<String> keys = new ArrayList<>();
197
- File[] files = baseDir.listFiles();
198
- if (files == null) {
199
- return keys;
200
- }
201
- for (File file : files) {
202
- // Skips the staging subdirectory (and any other non-file entry).
203
- if (!file.isFile()) {
204
- continue;
205
- }
206
- String key = decodeKey(file.getName());
207
- if (pattern == null || key.contains(pattern)) {
208
- keys.add(key);
209
- }
210
- }
211
- return keys;
212
- }
213
-
214
- public synchronized boolean has(String key) {
215
- File file = fileForKey(key);
216
- return file.exists() && file.isFile();
217
- }
218
-
219
- /**
220
- * Size information. {@code total} = {@code keys} + {@code values} (matching
221
- * the web adapters' convention), {@code values} = Σ file byte sizes,
222
- * {@code keys} = Σ decoded-key byte lengths, {@code metadata} = Σ length of
223
- * the parsed {@code metadata} field (0 when absent or unparseable),
224
- * {@code count} = number of stored files. The detailed breakdown is only
225
- * surfaced to JS when {@code detailed} is true.
226
- */
227
- public synchronized SQLiteStorage.SizeInfo size(boolean detailed) {
228
- File[] files = baseDir.listFiles();
229
- long valuesBytes = 0;
230
- long keysBytes = 0;
231
- long metadataBytes = 0;
232
- int count = 0;
233
-
234
- if (files != null) {
235
- for (File file : files) {
236
- // Skips the staging subdirectory (and any other non-file entry).
237
- if (!file.isFile()) {
238
- continue;
239
- }
240
- count++;
241
- valuesBytes += file.length();
242
- keysBytes += decodeKey(file.getName()).getBytes(StandardCharsets.UTF_8).length;
243
- if (detailed) {
244
- metadataBytes += metadataByteLength(file);
245
- }
246
- }
247
- }
248
-
249
- if (!detailed) {
250
- return new SQLiteStorage.SizeInfo(keysBytes + valuesBytes, count);
251
- }
252
- return new SQLiteStorage.SizeInfo(keysBytes + valuesBytes, count, keysBytes, valuesBytes, metadataBytes);
253
- }
254
-
255
- /** Parse the file and return the byte length of its {@code metadata} field. */
256
- private long metadataByteLength(File file) {
257
- try {
258
- JSObject wrapper = new JSObject(readFile(file));
259
- Object metadata = wrapper.opt("metadata");
260
- if (metadata != null && metadata != org.json.JSONObject.NULL) {
261
- return metadata.toString().getBytes(StandardCharsets.UTF_8).length;
262
- }
263
- } catch (Exception ignored) {
264
- // Unparseable → contributes 0 metadata bytes.
265
- }
266
- return 0;
267
- }
268
-
269
- private String readFile(File file) throws IOException {
270
- byte[] data = new byte[(int) file.length()];
271
- java.io.FileInputStream fis = null;
272
- try {
273
- fis = new java.io.FileInputStream(file);
274
- int offset = 0;
275
- int read;
276
- while (offset < data.length
277
- && (read = fis.read(data, offset, data.length - offset)) != -1) {
278
- offset += read;
279
- }
280
- } finally {
281
- if (fis != null) {
282
- fis.close();
283
- }
284
- }
285
- return new String(data, StandardCharsets.UTF_8);
286
- }
287
- }
@@ -1,371 +0,0 @@
1
- package com.strata.storage;
2
-
3
- import android.content.Context;
4
- import android.content.ContentValues;
5
- import android.database.Cursor;
6
- import android.database.sqlite.SQLiteDatabase;
7
- import android.database.sqlite.SQLiteOpenHelper;
8
- import android.util.Log;
9
- import com.getcapacitor.JSObject;
10
- import java.nio.charset.StandardCharsets;
11
- import java.util.ArrayList;
12
- import java.util.HashMap;
13
- import java.util.HashSet;
14
- import java.util.List;
15
- import java.util.Map;
16
- import java.util.Set;
17
- import org.json.JSONException;
18
- import org.json.JSONObject;
19
-
20
- /**
21
- * SQLite-backed storage. One {@link SQLiteStorage} instance maps to a single
22
- * database file (one {@link SQLiteOpenHelper}). A single instance can serve
23
- * multiple logical tables: every public method takes a sanitized {@code table}
24
- * name and the table is created on first use via {@code CREATE TABLE IF NOT
25
- * EXISTS}.
26
- *
27
- * <p>Value-shape contract (matches the TS {@code SqliteAdapter}): {@code set}
28
- * receives the FULL wrapper object ({@code value, created, updated, expires?,
29
- * tags?, metadata?}). The entire wrapper is JSON-serialized into the
30
- * {@code value} column for round-trip fidelity, while {@code created},
31
- * {@code updated}, {@code expires}, {@code tags} and {@code metadata} are also
32
- * extracted into their own columns for TTL + query support. {@code get} parses
33
- * the {@code value} column back into a {@link JSObject} wrapper.
34
- *
35
- * <p>All database access is synchronized on the instance so overlapping bridge
36
- * calls cannot corrupt the connection.
37
- */
38
- public class SQLiteStorage extends SQLiteOpenHelper {
39
- private static final int DATABASE_VERSION = 1;
40
- private static final String DEFAULT_TABLE = "storage";
41
-
42
- private static final String KEY_ID = "key";
43
- private static final String KEY_VALUE = "value";
44
- private static final String KEY_CREATED = "created";
45
- private static final String KEY_UPDATED = "updated";
46
- private static final String KEY_EXPIRES = "expires";
47
- private static final String KEY_TAGS = "tags";
48
- private static final String KEY_METADATA = "metadata";
49
-
50
- /** Tables already created in this DB during the process lifetime. */
51
- private final Set<String> ensuredTables = new HashSet<>();
52
-
53
- /**
54
- * @param context Android context
55
- * @param dbName fully-resolved database file name (already sanitized +
56
- * suffixed with ".db" by the plugin)
57
- */
58
- public SQLiteStorage(Context context, String dbName) {
59
- super(context, dbName, null, DATABASE_VERSION);
60
- }
61
-
62
- /**
63
- * Sanitize a table name to a safe SQL identifier ({@code ^[A-Za-z0-9_]+$}).
64
- * Table names cannot be passed as bound parameters in SQLite, so they MUST
65
- * be validated before being interpolated into DDL/DML.
66
- */
67
- public static String sanitizeTable(String table) {
68
- if (table == null || table.isEmpty()) {
69
- return DEFAULT_TABLE;
70
- }
71
- String cleaned = table.replaceAll("[^A-Za-z0-9_]", "");
72
- return cleaned.isEmpty() ? DEFAULT_TABLE : cleaned;
73
- }
74
-
75
- @Override
76
- public void onCreate(SQLiteDatabase db) {
77
- // Tables are created lazily per (database, table) via ensureTable().
78
- }
79
-
80
- @Override
81
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
82
- // No destructive migrations: tables are created on demand and persist.
83
- }
84
-
85
- /**
86
- * Create the table for {@code safeTable} if it does not yet exist. Caller
87
- * MUST pass an already-sanitized identifier.
88
- */
89
- private void ensureTable(SQLiteDatabase db, String safeTable) {
90
- if (ensuredTables.contains(safeTable)) {
91
- return;
92
- }
93
- String createTable = "CREATE TABLE IF NOT EXISTS " + safeTable + "("
94
- + KEY_ID + " TEXT PRIMARY KEY,"
95
- + KEY_VALUE + " TEXT NOT NULL,"
96
- + KEY_CREATED + " INTEGER NOT NULL,"
97
- + KEY_UPDATED + " INTEGER NOT NULL,"
98
- + KEY_EXPIRES + " INTEGER,"
99
- + KEY_TAGS + " TEXT,"
100
- + KEY_METADATA + " TEXT" + ")";
101
- db.execSQL(createTable);
102
- ensuredTables.add(safeTable);
103
- }
104
-
105
- /**
106
- * Persist the full wrapper for {@code key}. The wrapper is stored verbatim
107
- * as JSON in the {@code value} column; {@code created}/{@code updated}/
108
- * {@code expires}/{@code tags}/{@code metadata} are mirrored into columns
109
- * for TTL + query.
110
- *
111
- * @param wrapper the full {@link StorageValue} wrapper as a {@link JSObject}
112
- */
113
- public synchronized boolean set(String table, String key, JSObject wrapper) {
114
- if (wrapper == null) {
115
- return false;
116
- }
117
- String safeTable = sanitizeTable(table);
118
- long now = System.currentTimeMillis();
119
-
120
- // Column extraction for TTL + query. Defaults keep legacy fidelity.
121
- long created = wrapper.optLong(KEY_CREATED, now);
122
- long updated = wrapper.optLong(KEY_UPDATED, now);
123
-
124
- ContentValues values = new ContentValues();
125
- values.put(KEY_ID, key);
126
- values.put(KEY_VALUE, wrapper.toString());
127
- values.put(KEY_CREATED, created);
128
- values.put(KEY_UPDATED, updated);
129
-
130
- if (wrapper.has(KEY_EXPIRES) && !wrapper.isNull(KEY_EXPIRES)) {
131
- values.put(KEY_EXPIRES, wrapper.optLong(KEY_EXPIRES));
132
- }
133
- // tags / metadata are stored as their JSON text for query use.
134
- Object tags = wrapper.opt(KEY_TAGS);
135
- if (tags != null && tags != JSONObject.NULL) {
136
- values.put(KEY_TAGS, tags.toString());
137
- }
138
- Object metadata = wrapper.opt(KEY_METADATA);
139
- if (metadata != null && metadata != JSONObject.NULL) {
140
- values.put(KEY_METADATA, metadata.toString());
141
- }
142
-
143
- try {
144
- SQLiteDatabase db = getWritableDatabase();
145
- ensureTable(db, safeTable);
146
- long result = db.insertWithOnConflict(
147
- safeTable, null, values, SQLiteDatabase.CONFLICT_REPLACE);
148
- return result != -1;
149
- } catch (Exception e) {
150
- Log.e("StrataStorage", "Failed to set value in SQLite", e);
151
- return false;
152
- }
153
- }
154
-
155
- /**
156
- * Read the full wrapper for {@code key}, parsed back from the JSON stored in
157
- * the {@code value} column. Missing row → {@code null}. Unparseable legacy
158
- * bytes → {@code null} (treated as a miss; never throws).
159
- */
160
- public synchronized JSObject get(String table, String key) {
161
- String safeTable = sanitizeTable(table);
162
- SQLiteDatabase db = getReadableDatabase();
163
- ensureTable(db, safeTable);
164
- Cursor cursor = null;
165
- try {
166
- cursor = db.query(safeTable, new String[]{KEY_VALUE}, KEY_ID + "=?",
167
- new String[]{key}, null, null, null, null);
168
- if (cursor != null && cursor.moveToFirst()) {
169
- String stored = cursor.getString(0);
170
- if (stored == null) {
171
- return null;
172
- }
173
- try {
174
- return new JSObject(stored);
175
- } catch (JSONException parseError) {
176
- // Legacy / corrupted payload: treat as a miss rather than throw.
177
- Log.w("StrataStorage", "Unparseable SQLite value for key " + key);
178
- return null;
179
- }
180
- }
181
- return null;
182
- } finally {
183
- if (cursor != null) {
184
- cursor.close();
185
- }
186
- }
187
- }
188
-
189
- public synchronized boolean remove(String table, String key) {
190
- String safeTable = sanitizeTable(table);
191
- SQLiteDatabase db = getWritableDatabase();
192
- ensureTable(db, safeTable);
193
- int result = db.delete(safeTable, KEY_ID + " = ?", new String[]{key});
194
- return result > 0;
195
- }
196
-
197
- public synchronized boolean clear(String table, String prefix) {
198
- String safeTable = sanitizeTable(table);
199
- SQLiteDatabase db = getWritableDatabase();
200
- ensureTable(db, safeTable);
201
- int result;
202
- if (prefix != null) {
203
- result = db.delete(safeTable, KEY_ID + " LIKE ?", new String[]{prefix + "%"});
204
- } else {
205
- result = db.delete(safeTable, null, null);
206
- }
207
- return result >= 0;
208
- }
209
-
210
- public synchronized List<String> keys(String table, String pattern) {
211
- String safeTable = sanitizeTable(table);
212
- List<String> keys = new ArrayList<>();
213
- SQLiteDatabase db = getReadableDatabase();
214
- ensureTable(db, safeTable);
215
- Cursor cursor = null;
216
- try {
217
- if (pattern != null) {
218
- cursor = db.query(safeTable, new String[]{KEY_ID}, KEY_ID + " LIKE ?",
219
- new String[]{"%" + pattern + "%"}, null, null, null, null);
220
- } else {
221
- cursor = db.query(safeTable, new String[]{KEY_ID},
222
- null, null, null, null, null);
223
- }
224
- if (cursor != null && cursor.moveToFirst()) {
225
- do {
226
- keys.add(cursor.getString(0));
227
- } while (cursor.moveToNext());
228
- }
229
- return keys;
230
- } finally {
231
- if (cursor != null) {
232
- cursor.close();
233
- }
234
- }
235
- }
236
-
237
- public synchronized boolean has(String table, String key) {
238
- String safeTable = sanitizeTable(table);
239
- SQLiteDatabase db = getReadableDatabase();
240
- ensureTable(db, safeTable);
241
- Cursor cursor = null;
242
- try {
243
- cursor = db.query(safeTable, new String[]{KEY_ID}, KEY_ID + "=?",
244
- new String[]{key}, null, null, null, null);
245
- return cursor != null && cursor.getCount() > 0;
246
- } finally {
247
- if (cursor != null) {
248
- cursor.close();
249
- }
250
- }
251
- }
252
-
253
- /**
254
- * Enumerate every key in the table. The TS {@code SqliteAdapter} applies the
255
- * real query condition by re-fetching/filtering each key, so this only needs
256
- * to surface candidate keys.
257
- */
258
- public synchronized List<Map<String, Object>> query(String table) {
259
- String safeTable = sanitizeTable(table);
260
- List<Map<String, Object>> rows = new ArrayList<>();
261
- SQLiteDatabase db = getReadableDatabase();
262
- ensureTable(db, safeTable);
263
- Cursor cursor = null;
264
- try {
265
- cursor = db.query(safeTable, new String[]{KEY_ID},
266
- null, null, null, null, null);
267
- if (cursor != null && cursor.moveToFirst()) {
268
- do {
269
- Map<String, Object> row = new HashMap<>();
270
- row.put("key", cursor.getString(0));
271
- rows.add(row);
272
- } while (cursor.moveToNext());
273
- }
274
- return rows;
275
- } finally {
276
- if (cursor != null) {
277
- cursor.close();
278
- }
279
- }
280
- }
281
-
282
- /**
283
- * Size information. {@code total} = Σ(length(key) + length(value)), matching
284
- * the web adapters' convention (key bytes are included). When {@code
285
- * detailed} is true the byte breakdown is also populated: {@code keys} = Σ
286
- * length(key), {@code values} = Σ length(value), {@code metadata} = Σ
287
- * length(metadata) (0 for null); {@code count} is the row count.
288
- */
289
- public synchronized SizeInfo size(String table, boolean detailed) {
290
- String safeTable = sanitizeTable(table);
291
- SQLiteDatabase db = getReadableDatabase();
292
- ensureTable(db, safeTable);
293
- Cursor cursor = null;
294
- try {
295
- if (!detailed) {
296
- cursor = db.rawQuery("SELECT COUNT(*), SUM(LENGTH(" + KEY_ID + ") + LENGTH("
297
- + KEY_VALUE + ")) FROM " + safeTable, null);
298
- long totalSize = 0;
299
- int count = 0;
300
- if (cursor != null && cursor.moveToFirst()) {
301
- count = cursor.getInt(0);
302
- if (!cursor.isNull(1)) {
303
- totalSize = cursor.getLong(1);
304
- }
305
- }
306
- return new SizeInfo(totalSize, count);
307
- }
308
-
309
- cursor = db.query(safeTable,
310
- new String[]{KEY_ID, KEY_VALUE, KEY_METADATA},
311
- null, null, null, null, null);
312
- long keysBytes = 0;
313
- long valuesBytes = 0;
314
- long metadataBytes = 0;
315
- int count = 0;
316
- if (cursor != null && cursor.moveToFirst()) {
317
- do {
318
- count++;
319
- String key = cursor.getString(0);
320
- String value = cursor.getString(1);
321
- String metadata = cursor.getString(2);
322
- if (key != null) {
323
- keysBytes += key.getBytes(StandardCharsets.UTF_8).length;
324
- }
325
- if (value != null) {
326
- valuesBytes += value.getBytes(StandardCharsets.UTF_8).length;
327
- }
328
- if (metadata != null) {
329
- metadataBytes += metadata.getBytes(StandardCharsets.UTF_8).length;
330
- }
331
- } while (cursor.moveToNext());
332
- }
333
- return new SizeInfo(keysBytes + valuesBytes, count, keysBytes, valuesBytes, metadataBytes);
334
- } finally {
335
- if (cursor != null) {
336
- cursor.close();
337
- }
338
- }
339
- }
340
-
341
- /**
342
- * Size information. The detailed byte breakdown is only meaningful when
343
- * {@link #detailed} is true.
344
- */
345
- public static class SizeInfo {
346
- public final long total;
347
- public final int count;
348
- public final boolean detailed;
349
- public final long keysBytes;
350
- public final long valuesBytes;
351
- public final long metadataBytes;
352
-
353
- public SizeInfo(long total, int count) {
354
- this.total = total;
355
- this.count = count;
356
- this.detailed = false;
357
- this.keysBytes = 0;
358
- this.valuesBytes = 0;
359
- this.metadataBytes = 0;
360
- }
361
-
362
- public SizeInfo(long total, int count, long keysBytes, long valuesBytes, long metadataBytes) {
363
- this.total = total;
364
- this.count = count;
365
- this.detailed = true;
366
- this.keysBytes = keysBytes;
367
- this.valuesBytes = valuesBytes;
368
- this.metadataBytes = metadataBytes;
369
- }
370
- }
371
- }