@vailix/mask 0.2.2 → 0.2.4
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.
- package/CHANGELOG.md +18 -0
- package/README.md +10 -0
- package/__tests__/singleton.test.ts +235 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +47 -2
- package/dist/index.mjs +47 -2
- package/package.json +3 -2
- package/src/index.ts +61 -2
- package/vitest.config.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @vailix/mask
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dfda662: fix: make singleton initialization truly atomic
|
|
8
|
+
- Fix race condition where check-and-set wasn't atomic by using IIFE pattern
|
|
9
|
+
- Add comprehensive tests for singleton pattern (12 tests including stress test with 100 concurrent calls)
|
|
10
|
+
|
|
11
|
+
## 0.2.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- c1ca263: fix: prevent race condition in SDK initialization
|
|
16
|
+
- Implement singleton pattern for `VailixSDK.create()` to prevent concurrent database connections
|
|
17
|
+
- Ensure database is properly closed before deletion on key mismatch errors
|
|
18
|
+
- Add missing internal BLE types (`InternalNearbyUser`, `PendingPairRequest`, `BleServiceConfig`)
|
|
19
|
+
- Add `VailixSDK.destroy()` for cleanup and `VailixSDK.isInitialized()` for status checking
|
|
20
|
+
|
|
3
21
|
## 0.2.2
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -62,6 +62,16 @@ const unsubscribe = sdk.onNearbyUsersChanged((users) => {
|
|
|
62
62
|
});
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
> **Note:** `VailixSDK.create()` returns a singleton instance. Multiple calls return the same instance, and the config is only used on the first call. Use `VailixSDK.destroy()` to reset the SDK if needed.
|
|
66
|
+
|
|
67
|
+
### Static Methods
|
|
68
|
+
|
|
69
|
+
| Method | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `VailixSDK.create(config)` | Create or return the singleton SDK instance |
|
|
72
|
+
| `VailixSDK.destroy()` | Release resources and reset the singleton |
|
|
73
|
+
| `VailixSDK.isInitialized()` | Check if the SDK has been initialized |
|
|
74
|
+
|
|
65
75
|
## Compatibility
|
|
66
76
|
|
|
67
77
|
- **Expo**: SDK 52+ (Development Build required, **Expo Go not supported**)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for VailixSDK singleton pattern.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the race condition fix works correctly:
|
|
5
|
+
* - Multiple concurrent calls return the same instance
|
|
6
|
+
* - The promise is set atomically (no gap between check and set)
|
|
7
|
+
* - destroy() properly resets the singleton
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
11
|
+
|
|
12
|
+
// We need to test the singleton pattern in isolation.
|
|
13
|
+
// Since VailixSDK.doCreate has many dependencies, we'll mock them.
|
|
14
|
+
|
|
15
|
+
// Mock all the heavy dependencies using proper class constructors
|
|
16
|
+
vi.mock('../src/identity', () => {
|
|
17
|
+
return {
|
|
18
|
+
IdentityManager: class MockIdentityManager {
|
|
19
|
+
initialize = vi.fn().mockResolvedValue(undefined);
|
|
20
|
+
getMasterKey = vi.fn().mockReturnValue('0123456789abcdef0123456789abcdef');
|
|
21
|
+
getCurrentRPI = vi.fn().mockReturnValue('testrpi');
|
|
22
|
+
getMetadataKey = vi.fn().mockReturnValue('testkey');
|
|
23
|
+
getHistory = vi.fn().mockReturnValue([]);
|
|
24
|
+
getDisplayName = vi.fn().mockReturnValue('Test-123');
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
vi.mock('../src/db', () => ({
|
|
30
|
+
initializeDatabase: vi.fn().mockResolvedValue({
|
|
31
|
+
run: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock('../src/storage', () => {
|
|
36
|
+
return {
|
|
37
|
+
StorageService: class MockStorageService {
|
|
38
|
+
initialize = vi.fn().mockResolvedValue(undefined);
|
|
39
|
+
cleanupOldScans = vi.fn().mockResolvedValue(undefined);
|
|
40
|
+
canScan = vi.fn().mockReturnValue(true);
|
|
41
|
+
logScan = vi.fn().mockResolvedValue(undefined);
|
|
42
|
+
getRecentPairs = vi.fn().mockResolvedValue([]);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
vi.mock('../src/matcher', () => {
|
|
48
|
+
return {
|
|
49
|
+
MatcherService: class MockMatcherService {
|
|
50
|
+
on = vi.fn();
|
|
51
|
+
off = vi.fn();
|
|
52
|
+
emit = vi.fn();
|
|
53
|
+
getMatchById = vi.fn().mockResolvedValue(null);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
vi.mock('../src/ble', () => {
|
|
59
|
+
return {
|
|
60
|
+
BleService: class MockBleService {
|
|
61
|
+
setStorage = vi.fn();
|
|
62
|
+
destroy = vi.fn();
|
|
63
|
+
startDiscovery = vi.fn().mockResolvedValue(undefined);
|
|
64
|
+
stopDiscovery = vi.fn().mockResolvedValue(undefined);
|
|
65
|
+
getNearbyUsers = vi.fn().mockReturnValue([]);
|
|
66
|
+
onNearbyUsersChanged = vi.fn().mockReturnValue(() => { });
|
|
67
|
+
pairWithUser = vi.fn().mockResolvedValue({ success: true });
|
|
68
|
+
unpairUser = vi.fn().mockResolvedValue(undefined);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
vi.mock('react-native-quick-crypto', () => ({
|
|
74
|
+
createCipheriv: vi.fn(),
|
|
75
|
+
randomBytes: vi.fn().mockReturnValue(Buffer.alloc(12)),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// Import after mocks are set up
|
|
79
|
+
import { VailixSDK } from '../src/index';
|
|
80
|
+
|
|
81
|
+
const TEST_CONFIG = {
|
|
82
|
+
appSecret: 'test-secret',
|
|
83
|
+
reportUrl: 'https://test.example.com',
|
|
84
|
+
downloadUrl: 'https://test.example.com',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
describe('VailixSDK Singleton Pattern', () => {
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
// Reset singleton state before each test
|
|
90
|
+
await VailixSDK.destroy();
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('create()', () => {
|
|
95
|
+
it('should return the same instance on multiple sequential calls', async () => {
|
|
96
|
+
const sdk1 = await VailixSDK.create(TEST_CONFIG);
|
|
97
|
+
const sdk2 = await VailixSDK.create(TEST_CONFIG);
|
|
98
|
+
const sdk3 = await VailixSDK.create(TEST_CONFIG);
|
|
99
|
+
|
|
100
|
+
expect(sdk1).toBe(sdk2);
|
|
101
|
+
expect(sdk2).toBe(sdk3);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return the same instance on concurrent calls (race condition test)', async () => {
|
|
105
|
+
// Launch multiple concurrent create() calls
|
|
106
|
+
const promises = [
|
|
107
|
+
VailixSDK.create(TEST_CONFIG),
|
|
108
|
+
VailixSDK.create(TEST_CONFIG),
|
|
109
|
+
VailixSDK.create(TEST_CONFIG),
|
|
110
|
+
VailixSDK.create(TEST_CONFIG),
|
|
111
|
+
VailixSDK.create(TEST_CONFIG),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const results = await Promise.all(promises);
|
|
115
|
+
|
|
116
|
+
// All should be the exact same instance
|
|
117
|
+
const firstInstance = results[0];
|
|
118
|
+
results.forEach((sdk) => {
|
|
119
|
+
expect(sdk).toBe(firstInstance);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should only initialize once even with concurrent calls', async () => {
|
|
124
|
+
const { initializeDatabase } = await import('../src/db');
|
|
125
|
+
|
|
126
|
+
// Launch concurrent calls
|
|
127
|
+
await Promise.all([
|
|
128
|
+
VailixSDK.create(TEST_CONFIG),
|
|
129
|
+
VailixSDK.create(TEST_CONFIG),
|
|
130
|
+
VailixSDK.create(TEST_CONFIG),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
// initializeDatabase should only be called once
|
|
134
|
+
expect(initializeDatabase).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should use config from first call only', async () => {
|
|
138
|
+
const config1 = { ...TEST_CONFIG, reportUrl: 'https://first.com' };
|
|
139
|
+
const config2 = { ...TEST_CONFIG, reportUrl: 'https://second.com' };
|
|
140
|
+
|
|
141
|
+
const sdk1 = await VailixSDK.create(config1);
|
|
142
|
+
const sdk2 = await VailixSDK.create(config2);
|
|
143
|
+
|
|
144
|
+
// Both should be the same instance
|
|
145
|
+
expect(sdk1).toBe(sdk2);
|
|
146
|
+
// The instance should use the first config (verified indirectly by same instance)
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('isInitialized()', () => {
|
|
151
|
+
it('should return false before create() is called', () => {
|
|
152
|
+
expect(VailixSDK.isInitialized()).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return true after create() completes', async () => {
|
|
156
|
+
await VailixSDK.create(TEST_CONFIG);
|
|
157
|
+
expect(VailixSDK.isInitialized()).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return false after destroy() is called', async () => {
|
|
161
|
+
await VailixSDK.create(TEST_CONFIG);
|
|
162
|
+
expect(VailixSDK.isInitialized()).toBe(true);
|
|
163
|
+
|
|
164
|
+
await VailixSDK.destroy();
|
|
165
|
+
expect(VailixSDK.isInitialized()).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('destroy()', () => {
|
|
170
|
+
it('should allow creating a new instance after destroy', async () => {
|
|
171
|
+
const sdk1 = await VailixSDK.create(TEST_CONFIG);
|
|
172
|
+
await VailixSDK.destroy();
|
|
173
|
+
|
|
174
|
+
const sdk2 = await VailixSDK.create(TEST_CONFIG);
|
|
175
|
+
|
|
176
|
+
// Should be different instances
|
|
177
|
+
expect(sdk1).not.toBe(sdk2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should be safe to call destroy() multiple times', async () => {
|
|
181
|
+
await VailixSDK.create(TEST_CONFIG);
|
|
182
|
+
|
|
183
|
+
// Multiple destroy calls should not throw
|
|
184
|
+
await VailixSDK.destroy();
|
|
185
|
+
await VailixSDK.destroy();
|
|
186
|
+
await VailixSDK.destroy();
|
|
187
|
+
|
|
188
|
+
expect(VailixSDK.isInitialized()).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should be safe to call destroy() without prior create()', async () => {
|
|
192
|
+
// Should not throw
|
|
193
|
+
await VailixSDK.destroy();
|
|
194
|
+
expect(VailixSDK.isInitialized()).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('Race condition stress test', () => {
|
|
199
|
+
it('should handle many concurrent calls correctly', async () => {
|
|
200
|
+
const CONCURRENT_CALLS = 100;
|
|
201
|
+
|
|
202
|
+
const promises = Array.from({ length: CONCURRENT_CALLS }, () =>
|
|
203
|
+
VailixSDK.create(TEST_CONFIG)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const results = await Promise.all(promises);
|
|
207
|
+
|
|
208
|
+
// All should be the exact same instance
|
|
209
|
+
const firstInstance = results[0];
|
|
210
|
+
expect(results.every(sdk => sdk === firstInstance)).toBe(true);
|
|
211
|
+
|
|
212
|
+
// Verify initialization only happened once
|
|
213
|
+
const { initializeDatabase } = await import('../src/db');
|
|
214
|
+
expect(initializeDatabase).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle interleaved create and destroy calls', async () => {
|
|
218
|
+
// First creation
|
|
219
|
+
const sdk1 = await VailixSDK.create(TEST_CONFIG);
|
|
220
|
+
|
|
221
|
+
// Destroy and recreate
|
|
222
|
+
await VailixSDK.destroy();
|
|
223
|
+
const sdk2 = await VailixSDK.create(TEST_CONFIG);
|
|
224
|
+
|
|
225
|
+
// Destroy and recreate again
|
|
226
|
+
await VailixSDK.destroy();
|
|
227
|
+
const sdk3 = await VailixSDK.create(TEST_CONFIG);
|
|
228
|
+
|
|
229
|
+
// All should be different instances
|
|
230
|
+
expect(sdk1).not.toBe(sdk2);
|
|
231
|
+
expect(sdk2).not.toBe(sdk3);
|
|
232
|
+
expect(sdk1).not.toBe(sdk3);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
package/dist/index.d.mts
CHANGED
|
@@ -227,6 +227,8 @@ interface ParsedQR {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
declare class VailixSDK {
|
|
230
|
+
private static instance;
|
|
231
|
+
private static initPromise;
|
|
230
232
|
identity: IdentityManager;
|
|
231
233
|
storage: StorageService;
|
|
232
234
|
matcher: MatcherService;
|
|
@@ -237,11 +239,27 @@ declare class VailixSDK {
|
|
|
237
239
|
private rpiDurationMs;
|
|
238
240
|
private constructor();
|
|
239
241
|
/**
|
|
240
|
-
* Create
|
|
242
|
+
* Create or return the singleton SDK instance.
|
|
241
243
|
*
|
|
242
|
-
*
|
|
244
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
245
|
+
* and return the same instance. Config is only used on first initialization.
|
|
246
|
+
*
|
|
247
|
+
* @param config - Unified configuration object (used only on first call)
|
|
243
248
|
*/
|
|
244
249
|
static create(config: VailixConfig): Promise<VailixSDK>;
|
|
250
|
+
/**
|
|
251
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
252
|
+
*/
|
|
253
|
+
private static doCreate;
|
|
254
|
+
/**
|
|
255
|
+
* Destroy the singleton instance and release all resources.
|
|
256
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
257
|
+
*/
|
|
258
|
+
static destroy(): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Check if the SDK has been initialized.
|
|
261
|
+
*/
|
|
262
|
+
static isInitialized(): boolean;
|
|
245
263
|
/** Get current QR code data */
|
|
246
264
|
getQRCode(): string;
|
|
247
265
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -227,6 +227,8 @@ interface ParsedQR {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
declare class VailixSDK {
|
|
230
|
+
private static instance;
|
|
231
|
+
private static initPromise;
|
|
230
232
|
identity: IdentityManager;
|
|
231
233
|
storage: StorageService;
|
|
232
234
|
matcher: MatcherService;
|
|
@@ -237,11 +239,27 @@ declare class VailixSDK {
|
|
|
237
239
|
private rpiDurationMs;
|
|
238
240
|
private constructor();
|
|
239
241
|
/**
|
|
240
|
-
* Create
|
|
242
|
+
* Create or return the singleton SDK instance.
|
|
241
243
|
*
|
|
242
|
-
*
|
|
244
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
245
|
+
* and return the same instance. Config is only used on first initialization.
|
|
246
|
+
*
|
|
247
|
+
* @param config - Unified configuration object (used only on first call)
|
|
243
248
|
*/
|
|
244
249
|
static create(config: VailixConfig): Promise<VailixSDK>;
|
|
250
|
+
/**
|
|
251
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
252
|
+
*/
|
|
253
|
+
private static doCreate;
|
|
254
|
+
/**
|
|
255
|
+
* Destroy the singleton instance and release all resources.
|
|
256
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
257
|
+
*/
|
|
258
|
+
static destroy(): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Check if the SDK has been initialized.
|
|
261
|
+
*/
|
|
262
|
+
static isInitialized(): boolean;
|
|
245
263
|
/** Get current QR code data */
|
|
246
264
|
getQRCode(): string;
|
|
247
265
|
/**
|
package/dist/index.js
CHANGED
|
@@ -826,6 +826,9 @@ async function tryOpenEncryptedDatabase(masterKey) {
|
|
|
826
826
|
|
|
827
827
|
// src/index.ts
|
|
828
828
|
var VailixSDK = class _VailixSDK {
|
|
829
|
+
// Singleton instance and initialization promise for thread-safety
|
|
830
|
+
static instance = null;
|
|
831
|
+
static initPromise = null;
|
|
829
832
|
identity;
|
|
830
833
|
storage;
|
|
831
834
|
matcher;
|
|
@@ -845,11 +848,36 @@ var VailixSDK = class _VailixSDK {
|
|
|
845
848
|
this.rpiDurationMs = rpiDurationMs;
|
|
846
849
|
}
|
|
847
850
|
/**
|
|
848
|
-
* Create
|
|
851
|
+
* Create or return the singleton SDK instance.
|
|
849
852
|
*
|
|
850
|
-
*
|
|
853
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
854
|
+
* and return the same instance. Config is only used on first initialization.
|
|
855
|
+
*
|
|
856
|
+
* @param config - Unified configuration object (used only on first call)
|
|
851
857
|
*/
|
|
852
858
|
static async create(config) {
|
|
859
|
+
if (_VailixSDK.instance) {
|
|
860
|
+
return _VailixSDK.instance;
|
|
861
|
+
}
|
|
862
|
+
if (_VailixSDK.initPromise) {
|
|
863
|
+
return _VailixSDK.initPromise;
|
|
864
|
+
}
|
|
865
|
+
_VailixSDK.initPromise = (async () => {
|
|
866
|
+
try {
|
|
867
|
+
const sdk = await _VailixSDK.doCreate(config);
|
|
868
|
+
_VailixSDK.instance = sdk;
|
|
869
|
+
return sdk;
|
|
870
|
+
} catch (error) {
|
|
871
|
+
_VailixSDK.initPromise = null;
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
})();
|
|
875
|
+
return _VailixSDK.initPromise;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
879
|
+
*/
|
|
880
|
+
static async doCreate(config) {
|
|
853
881
|
const rpiDuration = config.rpiDurationMs ?? 15 * 60 * 1e3;
|
|
854
882
|
if (config.rescanIntervalMs && config.rescanIntervalMs > rpiDuration) {
|
|
855
883
|
throw new Error(`rescanIntervalMs (${config.rescanIntervalMs}) cannot exceed rpiDurationMs (${rpiDuration})`);
|
|
@@ -885,6 +913,23 @@ var VailixSDK = class _VailixSDK {
|
|
|
885
913
|
rpiDuration
|
|
886
914
|
);
|
|
887
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Destroy the singleton instance and release all resources.
|
|
918
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
919
|
+
*/
|
|
920
|
+
static async destroy() {
|
|
921
|
+
if (_VailixSDK.instance) {
|
|
922
|
+
_VailixSDK.instance.ble.destroy();
|
|
923
|
+
_VailixSDK.instance = null;
|
|
924
|
+
}
|
|
925
|
+
_VailixSDK.initPromise = null;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Check if the SDK has been initialized.
|
|
929
|
+
*/
|
|
930
|
+
static isInitialized() {
|
|
931
|
+
return _VailixSDK.instance !== null;
|
|
932
|
+
}
|
|
888
933
|
// ========================================================================
|
|
889
934
|
// QR Code Methods
|
|
890
935
|
// ========================================================================
|
package/dist/index.mjs
CHANGED
|
@@ -790,6 +790,9 @@ async function tryOpenEncryptedDatabase(masterKey) {
|
|
|
790
790
|
|
|
791
791
|
// src/index.ts
|
|
792
792
|
var VailixSDK = class _VailixSDK {
|
|
793
|
+
// Singleton instance and initialization promise for thread-safety
|
|
794
|
+
static instance = null;
|
|
795
|
+
static initPromise = null;
|
|
793
796
|
identity;
|
|
794
797
|
storage;
|
|
795
798
|
matcher;
|
|
@@ -809,11 +812,36 @@ var VailixSDK = class _VailixSDK {
|
|
|
809
812
|
this.rpiDurationMs = rpiDurationMs;
|
|
810
813
|
}
|
|
811
814
|
/**
|
|
812
|
-
* Create
|
|
815
|
+
* Create or return the singleton SDK instance.
|
|
813
816
|
*
|
|
814
|
-
*
|
|
817
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
818
|
+
* and return the same instance. Config is only used on first initialization.
|
|
819
|
+
*
|
|
820
|
+
* @param config - Unified configuration object (used only on first call)
|
|
815
821
|
*/
|
|
816
822
|
static async create(config) {
|
|
823
|
+
if (_VailixSDK.instance) {
|
|
824
|
+
return _VailixSDK.instance;
|
|
825
|
+
}
|
|
826
|
+
if (_VailixSDK.initPromise) {
|
|
827
|
+
return _VailixSDK.initPromise;
|
|
828
|
+
}
|
|
829
|
+
_VailixSDK.initPromise = (async () => {
|
|
830
|
+
try {
|
|
831
|
+
const sdk = await _VailixSDK.doCreate(config);
|
|
832
|
+
_VailixSDK.instance = sdk;
|
|
833
|
+
return sdk;
|
|
834
|
+
} catch (error) {
|
|
835
|
+
_VailixSDK.initPromise = null;
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
})();
|
|
839
|
+
return _VailixSDK.initPromise;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
843
|
+
*/
|
|
844
|
+
static async doCreate(config) {
|
|
817
845
|
const rpiDuration = config.rpiDurationMs ?? 15 * 60 * 1e3;
|
|
818
846
|
if (config.rescanIntervalMs && config.rescanIntervalMs > rpiDuration) {
|
|
819
847
|
throw new Error(`rescanIntervalMs (${config.rescanIntervalMs}) cannot exceed rpiDurationMs (${rpiDuration})`);
|
|
@@ -849,6 +877,23 @@ var VailixSDK = class _VailixSDK {
|
|
|
849
877
|
rpiDuration
|
|
850
878
|
);
|
|
851
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Destroy the singleton instance and release all resources.
|
|
882
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
883
|
+
*/
|
|
884
|
+
static async destroy() {
|
|
885
|
+
if (_VailixSDK.instance) {
|
|
886
|
+
_VailixSDK.instance.ble.destroy();
|
|
887
|
+
_VailixSDK.instance = null;
|
|
888
|
+
}
|
|
889
|
+
_VailixSDK.initPromise = null;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Check if the SDK has been initialized.
|
|
893
|
+
*/
|
|
894
|
+
static isInitialized() {
|
|
895
|
+
return _VailixSDK.instance !== null;
|
|
896
|
+
}
|
|
852
897
|
// ========================================================================
|
|
853
898
|
// QR Code Methods
|
|
854
899
|
// ========================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vailix/mask",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Privacy-preserving proximity tracing SDK for React Native & Expo",
|
|
5
5
|
"author": "Gil Eyni",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"drizzle-kit": "^0.30.0",
|
|
54
54
|
"typescript": "^5.7.0",
|
|
55
|
-
"tsup": "^8.0.0"
|
|
55
|
+
"tsup": "^8.0.0",
|
|
56
|
+
"vitest": "^4.0.0"
|
|
56
57
|
},
|
|
57
58
|
"scripts": {
|
|
58
59
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,10 @@ import type {
|
|
|
16
16
|
} from './types';
|
|
17
17
|
|
|
18
18
|
export class VailixSDK {
|
|
19
|
+
// Singleton instance and initialization promise for thread-safety
|
|
20
|
+
private static instance: VailixSDK | null = null;
|
|
21
|
+
private static initPromise: Promise<VailixSDK> | null = null;
|
|
22
|
+
|
|
19
23
|
public identity: IdentityManager;
|
|
20
24
|
public storage: StorageService;
|
|
21
25
|
public matcher: MatcherService;
|
|
@@ -46,11 +50,45 @@ export class VailixSDK {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
|
-
* Create
|
|
53
|
+
* Create or return the singleton SDK instance.
|
|
54
|
+
*
|
|
55
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
56
|
+
* and return the same instance. Config is only used on first initialization.
|
|
50
57
|
*
|
|
51
|
-
* @param config - Unified configuration object
|
|
58
|
+
* @param config - Unified configuration object (used only on first call)
|
|
52
59
|
*/
|
|
53
60
|
static async create(config: VailixConfig): Promise<VailixSDK> {
|
|
61
|
+
// Already initialized - return existing instance
|
|
62
|
+
if (VailixSDK.instance) {
|
|
63
|
+
return VailixSDK.instance;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialization in progress - wait for it
|
|
67
|
+
if (VailixSDK.initPromise) {
|
|
68
|
+
return VailixSDK.initPromise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// CRITICAL: Set promise SYNCHRONOUSLY before any await
|
|
72
|
+
// This makes the check-and-set atomic within the same microtask
|
|
73
|
+
VailixSDK.initPromise = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const sdk = await VailixSDK.doCreate(config);
|
|
76
|
+
VailixSDK.instance = sdk;
|
|
77
|
+
return sdk;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Clear promise on failure so retry is possible
|
|
80
|
+
VailixSDK.initPromise = null;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
return VailixSDK.initPromise;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
90
|
+
*/
|
|
91
|
+
private static async doCreate(config: VailixConfig): Promise<VailixSDK> {
|
|
54
92
|
// Validate: rescanInterval cannot exceed rpiDuration
|
|
55
93
|
const rpiDuration = config.rpiDurationMs ?? 15 * 60 * 1000; // Default 15 min
|
|
56
94
|
if (config.rescanIntervalMs && config.rescanIntervalMs > rpiDuration) {
|
|
@@ -99,6 +137,27 @@ export class VailixSDK {
|
|
|
99
137
|
);
|
|
100
138
|
}
|
|
101
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Destroy the singleton instance and release all resources.
|
|
142
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
143
|
+
*/
|
|
144
|
+
static async destroy(): Promise<void> {
|
|
145
|
+
if (VailixSDK.instance) {
|
|
146
|
+
// Cleanup BLE resources
|
|
147
|
+
VailixSDK.instance.ble.destroy();
|
|
148
|
+
// Note: Database connection cleanup is handled by expo-sqlite
|
|
149
|
+
VailixSDK.instance = null;
|
|
150
|
+
}
|
|
151
|
+
VailixSDK.initPromise = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if the SDK has been initialized.
|
|
156
|
+
*/
|
|
157
|
+
static isInitialized(): boolean {
|
|
158
|
+
return VailixSDK.instance !== null;
|
|
159
|
+
}
|
|
160
|
+
|
|
102
161
|
// ========================================================================
|
|
103
162
|
// QR Code Methods
|
|
104
163
|
// ========================================================================
|