strata-storage 2.6.1 → 2.7.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 (130) hide show
  1. package/AI-INTEGRATION-GUIDE.md +88 -10
  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/CookieAdapter.d.ts +2 -2
  27. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  28. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  29. package/dist/adapters/web/IndexedDBAdapter.js +14 -3
  30. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  31. package/dist/adapters/web/MemoryAdapter.d.ts +2 -2
  32. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  33. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  34. package/dist/adapters/web/SessionStorageAdapter.js +3 -2
  35. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  36. package/dist/adapters/web/index.d.ts +7 -7
  37. package/dist/capacitor.d.ts +6 -6
  38. package/dist/capacitor.d.ts.map +1 -1
  39. package/dist/capacitor.js +5 -4
  40. package/dist/core/AdapterRegistry.d.ts +1 -1
  41. package/dist/core/BaseAdapter.d.ts +21 -5
  42. package/dist/core/BaseAdapter.d.ts.map +1 -1
  43. package/dist/core/BaseAdapter.js +29 -1
  44. package/dist/core/Strata.d.ts +41 -7
  45. package/dist/core/Strata.d.ts.map +1 -1
  46. package/dist/core/Strata.js +476 -118
  47. package/dist/features/compression/index.d.ts +1 -1
  48. package/dist/features/compression.d.ts +5 -1
  49. package/dist/features/compression.d.ts.map +1 -1
  50. package/dist/features/encryption/index.d.ts +1 -1
  51. package/dist/features/encryption.d.ts +55 -2
  52. package/dist/features/encryption.d.ts.map +1 -1
  53. package/dist/features/encryption.js +139 -26
  54. package/dist/features/migration.d.ts +1 -1
  55. package/dist/features/observer/index.d.ts +1 -1
  56. package/dist/features/observer.d.ts +1 -1
  57. package/dist/features/query/index.d.ts +1 -1
  58. package/dist/features/query.d.ts +1 -1
  59. package/dist/features/query.d.ts.map +1 -1
  60. package/dist/features/query.js +3 -1
  61. package/dist/features/sync/index.d.ts +1 -1
  62. package/dist/features/sync.d.ts +19 -3
  63. package/dist/features/sync.d.ts.map +1 -1
  64. package/dist/features/sync.js +57 -9
  65. package/dist/features/ttl/index.d.ts +1 -1
  66. package/dist/features/ttl.d.ts +2 -2
  67. package/dist/firebase.d.ts +1 -1
  68. package/dist/firebase.d.ts.map +1 -1
  69. package/dist/firebase.js +34 -9
  70. package/dist/index.d.ts +24 -22
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +6 -1
  73. package/dist/integrations/angular/index.d.ts +2 -2
  74. package/dist/integrations/angular/index.d.ts.map +1 -1
  75. package/dist/integrations/angular/index.js +7 -8
  76. package/dist/integrations/react/index.d.ts +3 -3
  77. package/dist/integrations/react/index.d.ts.map +1 -1
  78. package/dist/integrations/react/index.js +16 -4
  79. package/dist/integrations/vue/index.d.ts +2 -2
  80. package/dist/integrations/vue/index.d.ts.map +1 -1
  81. package/dist/integrations/vue/index.js +5 -2
  82. package/dist/package.json +11 -2
  83. package/dist/plugin/definitions.d.ts +29 -3
  84. package/dist/plugin/definitions.d.ts.map +1 -1
  85. package/dist/plugin/index.d.ts +2 -2
  86. package/dist/plugin/index.d.ts.map +1 -1
  87. package/dist/plugin/index.js +3 -0
  88. package/dist/plugin/web.d.ts +11 -2
  89. package/dist/plugin/web.d.ts.map +1 -1
  90. package/dist/plugin/web.js +23 -4
  91. package/dist/types/index.d.ts +50 -20
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +1 -10
  94. package/dist/utils/index.d.ts +5 -0
  95. package/dist/utils/index.d.ts.map +1 -1
  96. package/dist/utils/index.js +7 -0
  97. package/ios/AGENTS.md +14 -3
  98. package/ios/CLAUDE.md +25 -3
  99. package/ios/Plugin/FilesystemStorage.swift +19 -0
  100. package/ios/Plugin/SQLiteStorage.swift +59 -9
  101. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  102. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  103. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  104. package/package.json +27 -24
  105. package/scripts/build.js +16 -14
  106. package/scripts/cli.js +1 -1
  107. package/scripts/configure.js +2 -2
  108. package/scripts/postinstall.js +3 -2
  109. package/dist/android/AGENTS.md +0 -51
  110. package/dist/android/CLAUDE.md +0 -89
  111. package/dist/android/build.gradle +0 -58
  112. package/dist/android/proguard-rules.pro +0 -11
  113. package/dist/android/settings.gradle +0 -2
  114. package/dist/android/src/main/AndroidManifest.xml +0 -2
  115. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  116. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  117. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  118. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  119. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  120. package/dist/core/StorageStrategy.d.ts +0 -55
  121. package/dist/core/StorageStrategy.d.ts.map +0 -1
  122. package/dist/core/StorageStrategy.js +0 -200
  123. package/dist/ios/AGENTS.md +0 -48
  124. package/dist/ios/CLAUDE.md +0 -84
  125. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  126. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  127. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  128. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  129. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  130. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -1,411 +0,0 @@
1
- import Foundation
2
- import Capacitor
3
-
4
- /**
5
- * Main Capacitor plugin for Strata Storage
6
- * Coordinates between different storage types on iOS.
7
- *
8
- * NOTE: Method registration with the Capacitor bridge lives in
9
- * StrataStoragePlugin.m (CAP_PLUGIN / CAP_PLUGIN_METHOD). Keep the two in
10
- * sync when adding or renaming methods.
11
- */
12
- @objc(StrataStoragePlugin)
13
- public class StrataStoragePlugin: CAPPlugin {
14
- private let userDefaultsStorage = UserDefaultsStorage()
15
- private let keychainStorage = KeychainStorage()
16
- private let filesystemStorage = FilesystemStorage()
17
-
18
- /// Storage types that have a real native backend on iOS.
19
- private let supportedStorageTypes: Set<String> = ["preferences", "secure", "sqlite", "filesystem"]
20
-
21
- // MARK: - SQLite multi-store helpers
22
-
23
- /// Default database name (→ file `strata_storage.db`) and table.
24
- private static let defaultDatabase = "strata_storage"
25
- private static let defaultTable = "storage"
26
-
27
- /// Sanitizes a database name into a safe `.db` filename. Strips everything
28
- /// outside `[A-Za-z0-9_]` (which also blocks path separators / traversal),
29
- /// then appends `.db`. Empty input falls back to the default. The filename
30
- /// CANNOT be a bound parameter, so it must be sanitized before use.
31
- private func sqliteFileName(from database: String?) -> String {
32
- let raw = database ?? StrataStoragePlugin.defaultDatabase
33
- let cleaned = String(raw.unicodeScalars.filter { CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_").contains($0) })
34
- let safe = cleaned.isEmpty ? StrataStoragePlugin.defaultDatabase : cleaned
35
- return "\(safe).db"
36
- }
37
-
38
- /// Sanitizes a table name to a safe SQL identifier matching `^[A-Za-z0-9_]+$`.
39
- /// Disallowed characters are removed; empty input falls back to the default.
40
- /// The table name is interpolated into SQL (it cannot be bound), so this is
41
- /// the single point that guarantees injection-safety for the identifier.
42
- private func sqliteTableName(from table: String?) -> String {
43
- let raw = table ?? StrataStoragePlugin.defaultTable
44
- let cleaned = String(raw.unicodeScalars.filter { CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_").contains($0) })
45
- return cleaned.isEmpty ? StrataStoragePlugin.defaultTable : cleaned
46
- }
47
-
48
- /// Returns the cached SQLite store for the call's `database` option.
49
- private func sqliteStore(for call: CAPPluginCall) -> SQLiteStorage {
50
- return SQLiteStorageManager.shared.store(forFile: sqliteFileName(from: call.getString("database")))
51
- }
52
-
53
- /**
54
- * Check if a specific storage type is available.
55
- * Matches the JS contract: resolves `{ available: boolean }`.
56
- */
57
- @objc func isAvailable(_ call: CAPPluginCall) {
58
- let storage = call.getString("storage") ?? "preferences"
59
- call.resolve([
60
- "available": supportedStorageTypes.contains(storage)
61
- ])
62
- }
63
-
64
- /**
65
- * Get value from storage.
66
- * For sqlite/filesystem the resolved `value` is the full StorageValue
67
- * wrapper object (or NSNull on a miss).
68
- */
69
- @objc func get(_ call: CAPPluginCall) {
70
- guard let key = call.getString("key") else {
71
- call.reject("Key is required")
72
- return
73
- }
74
-
75
- let storage = call.getString("storage") ?? "preferences"
76
-
77
- do {
78
- let value: Any?
79
-
80
- switch storage {
81
- case "secure":
82
- value = try keychainStorage.get(key: key)
83
- case "sqlite":
84
- value = sqliteStore(for: call).get(table: sqliteTableName(from: call.getString("table")), key: key)
85
- case "filesystem":
86
- value = filesystemStorage.get(key: key)
87
- case "preferences":
88
- fallthrough
89
- default:
90
- value = userDefaultsStorage.get(key: key)
91
- }
92
-
93
- call.resolve([
94
- "value": value ?? NSNull()
95
- ])
96
- } catch {
97
- call.reject("Failed to get value", nil, error)
98
- }
99
- }
100
-
101
- /**
102
- * Set value in storage.
103
- * For sqlite/filesystem the `value` option is the full StorageValue
104
- * wrapper object and is stored verbatim (JSON) for a perfect round-trip.
105
- */
106
- @objc func set(_ call: CAPPluginCall) {
107
- guard let key = call.getString("key") else {
108
- call.reject("Key is required")
109
- return
110
- }
111
-
112
- let storage = call.getString("storage") ?? "preferences"
113
-
114
- do {
115
- switch storage {
116
- case "secure":
117
- let value = call.getValue("value") ?? NSNull()
118
- _ = try keychainStorage.set(key: key, value: value)
119
- case "sqlite":
120
- // getObject returns JSObject ([String: JSValue]); upcast each
121
- // value to Any (the canonical Capacitor pattern) so the bridged
122
- // Foundation values are JSONSerialization-compatible.
123
- guard let jsObject = call.getObject("value") else {
124
- call.reject("SQLite value must be a StorageValue object")
125
- return
126
- }
127
- let ok = try sqliteStore(for: call).set(
128
- table: sqliteTableName(from: call.getString("table")),
129
- key: key,
130
- wrapper: jsObject as [String: Any]
131
- )
132
- if !ok {
133
- call.reject("Failed to write value to SQLite")
134
- return
135
- }
136
- case "filesystem":
137
- guard let jsObject = call.getObject("value") else {
138
- call.reject("Filesystem value must be a StorageValue object")
139
- return
140
- }
141
- let ok = try filesystemStorage.set(key: key, wrapper: jsObject as [String: Any])
142
- if !ok {
143
- call.reject("Failed to write value to filesystem")
144
- return
145
- }
146
- case "preferences":
147
- fallthrough
148
- default:
149
- let value = call.getValue("value") ?? NSNull()
150
- userDefaultsStorage.set(key: key, value: value)
151
- }
152
-
153
- call.resolve()
154
- } catch {
155
- call.reject("Failed to set value", nil, error)
156
- }
157
- }
158
-
159
- /**
160
- * Remove value from storage
161
- */
162
- @objc func remove(_ call: CAPPluginCall) {
163
- guard let key = call.getString("key") else {
164
- call.reject("Key is required")
165
- return
166
- }
167
-
168
- let storage = call.getString("storage") ?? "preferences"
169
-
170
- do {
171
- switch storage {
172
- case "secure":
173
- _ = try keychainStorage.remove(key: key)
174
- case "sqlite":
175
- _ = sqliteStore(for: call).remove(table: sqliteTableName(from: call.getString("table")), key: key)
176
- case "filesystem":
177
- _ = filesystemStorage.remove(key: key)
178
- case "preferences":
179
- fallthrough
180
- default:
181
- userDefaultsStorage.remove(key: key)
182
- }
183
-
184
- call.resolve()
185
- } catch {
186
- call.reject("Failed to remove value", nil, error)
187
- }
188
- }
189
-
190
- /**
191
- * Clear storage.
192
- * The JS contract sends an optional `pattern`. The native backends accept
193
- * a prefix; we use `pattern` as that prefix (prefix/contains matching).
194
- */
195
- @objc func clear(_ call: CAPPluginCall) {
196
- let storage = call.getString("storage") ?? "preferences"
197
- // Accept both `pattern` (JS contract) and legacy `prefix`.
198
- let prefix = call.getString("pattern") ?? call.getString("prefix")
199
-
200
- do {
201
- switch storage {
202
- case "secure":
203
- _ = try keychainStorage.clear(prefix: prefix)
204
- case "sqlite":
205
- _ = sqliteStore(for: call).clear(table: sqliteTableName(from: call.getString("table")), prefix: prefix)
206
- case "filesystem":
207
- _ = filesystemStorage.clear(prefix: prefix)
208
- case "preferences":
209
- fallthrough
210
- default:
211
- userDefaultsStorage.clear(prefix: prefix)
212
- }
213
-
214
- call.resolve()
215
- } catch {
216
- call.reject("Failed to clear storage", nil, error)
217
- }
218
- }
219
-
220
- /**
221
- * Get all keys from storage
222
- */
223
- @objc func keys(_ call: CAPPluginCall) {
224
- let storage = call.getString("storage") ?? "preferences"
225
- let pattern = call.getString("pattern")
226
-
227
- do {
228
- let keys: [String]
229
-
230
- switch storage {
231
- case "secure":
232
- keys = try keychainStorage.keys(pattern: pattern)
233
- case "sqlite":
234
- keys = sqliteStore(for: call).keys(table: sqliteTableName(from: call.getString("table")), pattern: pattern)
235
- case "filesystem":
236
- keys = filesystemStorage.keys(pattern: pattern)
237
- case "preferences":
238
- fallthrough
239
- default:
240
- keys = userDefaultsStorage.keys(pattern: pattern)
241
- }
242
-
243
- call.resolve([
244
- "keys": keys
245
- ])
246
- } catch {
247
- call.reject("Failed to get keys", nil, error)
248
- }
249
- }
250
-
251
- /**
252
- * Get storage size information.
253
- * When `detailed` is true, resolves `{ total, count, detailed: { keys,
254
- * values, metadata } }`; otherwise `{ total, count }`.
255
- */
256
- @objc func size(_ call: CAPPluginCall) {
257
- let storage = call.getString("storage") ?? "preferences"
258
- let detailed = call.getBool("detailed") ?? false
259
-
260
- do {
261
- // segments carries total/count/keys/values/metadata in bytes.
262
- let segments: [String: Int]
263
-
264
- switch storage {
265
- case "secure":
266
- let info = try keychainStorage.size()
267
- segments = ["total": info.total, "count": info.count, "keys": 0, "values": info.total, "metadata": 0]
268
- case "sqlite":
269
- segments = try sqliteStore(for: call).size(table: sqliteTableName(from: call.getString("table")))
270
- case "filesystem":
271
- segments = filesystemStorage.size()
272
- case "preferences":
273
- fallthrough
274
- default:
275
- let info = userDefaultsStorage.size()
276
- segments = ["total": info.total, "count": info.count, "keys": 0, "values": info.total, "metadata": 0]
277
- }
278
-
279
- var result: [String: Any] = [
280
- "total": segments["total"] ?? 0,
281
- "count": segments["count"] ?? 0
282
- ]
283
- if detailed {
284
- result["detailed"] = [
285
- "keys": segments["keys"] ?? 0,
286
- "values": segments["values"] ?? 0,
287
- "metadata": segments["metadata"] ?? 0
288
- ]
289
- }
290
- call.resolve(result)
291
- } catch {
292
- call.reject("Failed to get size", nil, error)
293
- }
294
- }
295
-
296
- /**
297
- * Query SQLite-backed storage.
298
- * Matches the optional `query` method in the JS contract:
299
- * resolves `{ results: [{ key }] }` (JS re-fetches each value via get).
300
- */
301
- @objc func query(_ call: CAPPluginCall) {
302
- let storage = call.getString("storage") ?? "sqlite"
303
-
304
- guard storage == "sqlite" else {
305
- call.reject("Query is only supported for the 'sqlite' storage type")
306
- return
307
- }
308
-
309
- let condition = call.getObject("condition") ?? [:]
310
- let results = sqliteStore(for: call).query(
311
- table: sqliteTableName(from: call.getString("table")),
312
- condition: condition
313
- )
314
- call.resolve([
315
- "results": results
316
- ])
317
- }
318
-
319
- /**
320
- * iOS-specific: read a UserDefaults value.
321
- * Resolves `{ value: unknown }`.
322
- */
323
- @objc func getUserDefaults(_ call: CAPPluginCall) {
324
- guard let key = call.getString("key") else {
325
- call.reject("Key is required")
326
- return
327
- }
328
- let suiteName = call.getString("suiteName")
329
- let store = suiteName != nil ? UserDefaultsStorage(suiteName: suiteName) : userDefaultsStorage
330
- call.resolve([
331
- "value": store.get(key: key) ?? NSNull()
332
- ])
333
- }
334
-
335
- /**
336
- * iOS-specific: write a UserDefaults value.
337
- */
338
- @objc func setUserDefaults(_ call: CAPPluginCall) {
339
- guard let key = call.getString("key") else {
340
- call.reject("Key is required")
341
- return
342
- }
343
- let value = call.getValue("value") ?? NSNull()
344
- let suiteName = call.getString("suiteName")
345
- let store = suiteName != nil ? UserDefaultsStorage(suiteName: suiteName) : userDefaultsStorage
346
- _ = store.set(key: key, value: value)
347
- call.resolve()
348
- }
349
-
350
- /**
351
- * iOS-specific: read a Keychain value. Resolves `{ value: string | null }`.
352
- */
353
- @objc func getKeychain(_ call: CAPPluginCall) {
354
- guard let key = call.getString("key") else {
355
- call.reject("Key is required")
356
- return
357
- }
358
- let service = call.getString("service")
359
- let accessGroup = call.getString("accessGroup")
360
- let store = (service != nil || accessGroup != nil)
361
- ? KeychainStorage(service: service, accessGroup: accessGroup)
362
- : keychainStorage
363
-
364
- do {
365
- let value = try store.get(key: key)
366
- // The JS contract types this as `string | null`. Serialize
367
- // non-string values to a JSON string so the bridge stays typed.
368
- if let value = value {
369
- if let str = value as? String {
370
- call.resolve(["value": str])
371
- } else if let data = try? JSONSerialization.data(withJSONObject: value, options: []),
372
- let str = String(data: data, encoding: .utf8) {
373
- call.resolve(["value": str])
374
- } else {
375
- call.resolve(["value": NSNull()])
376
- }
377
- } else {
378
- call.resolve(["value": NSNull()])
379
- }
380
- } catch {
381
- call.reject("Failed to read keychain item", nil, error)
382
- }
383
- }
384
-
385
- /**
386
- * iOS-specific: write a Keychain value, honoring the `accessible` option.
387
- */
388
- @objc func setKeychain(_ call: CAPPluginCall) {
389
- guard let key = call.getString("key") else {
390
- call.reject("Key is required")
391
- return
392
- }
393
- guard let value = call.getString("value") else {
394
- call.reject("Value is required and must be a string")
395
- return
396
- }
397
- let service = call.getString("service")
398
- let accessGroup = call.getString("accessGroup")
399
- let accessible = call.getString("accessible")
400
- let store = (service != nil || accessGroup != nil)
401
- ? KeychainStorage(service: service, accessGroup: accessGroup)
402
- : keychainStorage
403
-
404
- do {
405
- _ = try store.set(key: key, value: value, accessible: accessible)
406
- call.resolve()
407
- } catch {
408
- call.reject("Failed to write keychain item", nil, error)
409
- }
410
- }
411
- }
@@ -1,87 +0,0 @@
1
- import Foundation
2
-
3
- @objc public class UserDefaultsStorage: NSObject {
4
- private let suiteName: String?
5
- private var userDefaults: UserDefaults
6
-
7
- @objc public init(suiteName: String? = nil) {
8
- self.suiteName = suiteName
9
- if let suiteName = suiteName, let customDefaults = UserDefaults(suiteName: suiteName) {
10
- self.userDefaults = customDefaults
11
- } else {
12
- self.userDefaults = UserDefaults.standard
13
- }
14
- super.init()
15
- }
16
-
17
- @objc public func set(key: String, value: Any) -> Bool {
18
- userDefaults.set(value, forKey: key)
19
- return userDefaults.synchronize()
20
- }
21
-
22
- @objc public func get(key: String) -> Any? {
23
- return userDefaults.object(forKey: key)
24
- }
25
-
26
- @objc public func remove(key: String) -> Bool {
27
- userDefaults.removeObject(forKey: key)
28
- return userDefaults.synchronize()
29
- }
30
-
31
- @objc public func clear(prefix: String? = nil) -> Bool {
32
- if let prefix = prefix {
33
- // Clear only keys with the given prefix
34
- let keysToRemove = keys(pattern: prefix)
35
- for key in keysToRemove {
36
- userDefaults.removeObject(forKey: key)
37
- }
38
- } else {
39
- // Clear all keys
40
- if let suiteName = suiteName {
41
- UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
42
- } else {
43
- guard let domain = Bundle.main.bundleIdentifier else {
44
- // Fallback: manually clear all keys
45
- let keys = Array(userDefaults.dictionaryRepresentation().keys)
46
- for key in keys {
47
- userDefaults.removeObject(forKey: key)
48
- }
49
- return userDefaults.synchronize()
50
- }
51
- userDefaults.removePersistentDomain(forName: domain)
52
- }
53
- }
54
- return userDefaults.synchronize()
55
- }
56
-
57
- @objc public func keys(pattern: String? = nil) -> [String] {
58
- let allKeys = Array(userDefaults.dictionaryRepresentation().keys)
59
-
60
- guard let pattern = pattern else {
61
- return allKeys
62
- }
63
-
64
- // Filter keys by pattern (simple prefix matching)
65
- return allKeys.filter { key in
66
- key.hasPrefix(pattern) || key.contains(pattern)
67
- }
68
- }
69
-
70
- @objc public func has(key: String) -> Bool {
71
- return userDefaults.object(forKey: key) != nil
72
- }
73
-
74
- @objc public func size() -> (total: Int, count: Int) {
75
- let all = userDefaults.dictionaryRepresentation()
76
- var totalSize = 0
77
- let count = all.count
78
-
79
- for (key, value) in all {
80
- // Estimate size (key + value in bytes)
81
- totalSize += key.data(using: .utf8)?.count ?? 0
82
- totalSize += "\(value)".data(using: .utf8)?.count ?? 0
83
- }
84
-
85
- return (total: totalSize, count: count)
86
- }
87
- }