@logg/signals 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +514 -0
- package/dist/index.mjs +489 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AsyncStorageAdapter: () => AsyncStorageAdapter,
|
|
24
|
+
EventQueue: () => EventQueue,
|
|
25
|
+
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
26
|
+
MemoryStorageAdapter: () => MemoryStorageAdapter,
|
|
27
|
+
Signals: () => Signals,
|
|
28
|
+
getDefaultStorageAdapter: () => getDefaultStorageAdapter
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/utils/helpers.ts
|
|
33
|
+
function uuid() {
|
|
34
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
35
|
+
return crypto.randomUUID();
|
|
36
|
+
}
|
|
37
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
38
|
+
const r = Math.random() * 16 | 0;
|
|
39
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
40
|
+
return v.toString(16);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function getTimestamp() {
|
|
44
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
45
|
+
}
|
|
46
|
+
function sleep(ms) {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
}
|
|
49
|
+
async function retry(fn, options) {
|
|
50
|
+
const { maxRetries, initialDelay, onRetry } = options;
|
|
51
|
+
let lastError;
|
|
52
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
return await fn();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
57
|
+
if (attempt < maxRetries) {
|
|
58
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
59
|
+
onRetry?.(attempt + 1, lastError);
|
|
60
|
+
await sleep(delay);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastError;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/EventQueue.ts
|
|
68
|
+
var STORAGE_KEY = "session_signals_queue";
|
|
69
|
+
var EventQueue = class {
|
|
70
|
+
constructor(storage, sessionId) {
|
|
71
|
+
this.queue = [];
|
|
72
|
+
this.storage = storage;
|
|
73
|
+
this.sessionId = sessionId;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Initialize queue by loading persisted events
|
|
77
|
+
*/
|
|
78
|
+
async init() {
|
|
79
|
+
try {
|
|
80
|
+
const stored = await this.storage.getItem(STORAGE_KEY);
|
|
81
|
+
if (stored) {
|
|
82
|
+
const parsed = JSON.parse(stored);
|
|
83
|
+
this.queue = Array.isArray(parsed) ? parsed : [];
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn("Failed to load persisted events:", error);
|
|
87
|
+
this.queue = [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Add event to queue
|
|
92
|
+
*/
|
|
93
|
+
async add(eventData) {
|
|
94
|
+
const event = {
|
|
95
|
+
event_id: uuid(),
|
|
96
|
+
timestamp: getTimestamp(),
|
|
97
|
+
session_id: this.sessionId,
|
|
98
|
+
...eventData
|
|
99
|
+
};
|
|
100
|
+
this.queue.push({
|
|
101
|
+
event,
|
|
102
|
+
timestamp: Date.now()
|
|
103
|
+
});
|
|
104
|
+
await this.persist();
|
|
105
|
+
return event;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get all events in queue
|
|
109
|
+
*/
|
|
110
|
+
getAll() {
|
|
111
|
+
return this.queue.map((item) => item.event);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get events up to a certain size
|
|
115
|
+
*/
|
|
116
|
+
getBatch(size) {
|
|
117
|
+
return this.queue.slice(0, size).map((item) => item.event);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Remove events from queue
|
|
121
|
+
*/
|
|
122
|
+
async remove(count) {
|
|
123
|
+
this.queue.splice(0, count);
|
|
124
|
+
await this.persist();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Clear all events from queue
|
|
128
|
+
*/
|
|
129
|
+
async clear() {
|
|
130
|
+
this.queue = [];
|
|
131
|
+
await this.persist();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get queue size
|
|
135
|
+
*/
|
|
136
|
+
size() {
|
|
137
|
+
return this.queue.length;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Check if queue is empty
|
|
141
|
+
*/
|
|
142
|
+
isEmpty() {
|
|
143
|
+
return this.queue.length === 0;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get oldest event timestamp
|
|
147
|
+
*/
|
|
148
|
+
getOldestTimestamp() {
|
|
149
|
+
if (this.queue.length === 0) return null;
|
|
150
|
+
return this.queue[0]?.timestamp ?? null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Persist queue to storage
|
|
154
|
+
*/
|
|
155
|
+
async persist() {
|
|
156
|
+
try {
|
|
157
|
+
await this.storage.setItem(STORAGE_KEY, JSON.stringify(this.queue));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn("Failed to persist events:", error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/storage/LocalStorageAdapter.ts
|
|
165
|
+
var LocalStorageAdapter = class {
|
|
166
|
+
async getItem(key) {
|
|
167
|
+
try {
|
|
168
|
+
return localStorage.getItem(key);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.warn("LocalStorage not available:", error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async setItem(key, value) {
|
|
175
|
+
try {
|
|
176
|
+
localStorage.setItem(key, value);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.warn("LocalStorage setItem failed:", error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async removeItem(key) {
|
|
182
|
+
try {
|
|
183
|
+
localStorage.removeItem(key);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn("LocalStorage removeItem failed:", error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if localStorage is available
|
|
190
|
+
*/
|
|
191
|
+
static isAvailable() {
|
|
192
|
+
try {
|
|
193
|
+
const test = "__localStorage_test__";
|
|
194
|
+
localStorage.setItem(test, test);
|
|
195
|
+
localStorage.removeItem(test);
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/storage/AsyncStorageAdapter.ts
|
|
204
|
+
var AsyncStorageAdapter = class {
|
|
205
|
+
constructor() {
|
|
206
|
+
try {
|
|
207
|
+
this.asyncStorage = require("@react-native-async-storage/async-storage").default;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"AsyncStorage not found. Please install @react-native-async-storage/async-storage"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async getItem(key) {
|
|
215
|
+
try {
|
|
216
|
+
return await this.asyncStorage.getItem(key);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.warn("AsyncStorage getItem failed:", error);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async setItem(key, value) {
|
|
223
|
+
try {
|
|
224
|
+
await this.asyncStorage.setItem(key, value);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.warn("AsyncStorage setItem failed:", error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async removeItem(key) {
|
|
230
|
+
try {
|
|
231
|
+
await this.asyncStorage.removeItem(key);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.warn("AsyncStorage removeItem failed:", error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if AsyncStorage is available
|
|
238
|
+
*/
|
|
239
|
+
static isAvailable() {
|
|
240
|
+
try {
|
|
241
|
+
require("@react-native-async-storage/async-storage");
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/storage/MemoryStorageAdapter.ts
|
|
250
|
+
var MemoryStorageAdapter = class {
|
|
251
|
+
constructor() {
|
|
252
|
+
this.storage = /* @__PURE__ */ new Map();
|
|
253
|
+
}
|
|
254
|
+
async getItem(key) {
|
|
255
|
+
return this.storage.get(key) ?? null;
|
|
256
|
+
}
|
|
257
|
+
async setItem(key, value) {
|
|
258
|
+
this.storage.set(key, value);
|
|
259
|
+
}
|
|
260
|
+
async removeItem(key) {
|
|
261
|
+
this.storage.delete(key);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/storage/index.ts
|
|
266
|
+
function getDefaultStorageAdapter() {
|
|
267
|
+
if (LocalStorageAdapter.isAvailable()) {
|
|
268
|
+
return new LocalStorageAdapter();
|
|
269
|
+
}
|
|
270
|
+
if (AsyncStorageAdapter.isAvailable()) {
|
|
271
|
+
return new AsyncStorageAdapter();
|
|
272
|
+
}
|
|
273
|
+
return new MemoryStorageAdapter();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/utils/metadata.ts
|
|
277
|
+
function detectClientType() {
|
|
278
|
+
if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
|
|
279
|
+
return "react-native";
|
|
280
|
+
}
|
|
281
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
282
|
+
return "web";
|
|
283
|
+
}
|
|
284
|
+
return "node";
|
|
285
|
+
}
|
|
286
|
+
function getUserAgent() {
|
|
287
|
+
if (typeof navigator !== "undefined") {
|
|
288
|
+
return navigator.userAgent;
|
|
289
|
+
}
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
function getScreenDimensions() {
|
|
293
|
+
if (typeof window !== "undefined" && window.screen) {
|
|
294
|
+
return {
|
|
295
|
+
width: window.screen.width,
|
|
296
|
+
height: window.screen.height
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const { Dimensions } = require("react-native");
|
|
301
|
+
const { width, height } = Dimensions.get("window");
|
|
302
|
+
return { width, height };
|
|
303
|
+
} catch {
|
|
304
|
+
return void 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function getLocale() {
|
|
308
|
+
if (typeof navigator !== "undefined") {
|
|
309
|
+
return navigator.language;
|
|
310
|
+
}
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
function getTimezone() {
|
|
314
|
+
try {
|
|
315
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
316
|
+
} catch {
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function collectMetadata(version) {
|
|
321
|
+
const clientType = detectClientType();
|
|
322
|
+
const metadata = {
|
|
323
|
+
type: clientType,
|
|
324
|
+
version
|
|
325
|
+
};
|
|
326
|
+
if (clientType === "web" || clientType === "react-native") {
|
|
327
|
+
metadata.user_agent = getUserAgent();
|
|
328
|
+
metadata.screen = getScreenDimensions();
|
|
329
|
+
metadata.locale = getLocale();
|
|
330
|
+
metadata.timezone = getTimezone();
|
|
331
|
+
}
|
|
332
|
+
return metadata;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/Signals.ts
|
|
336
|
+
var VERSION = "0.1.0";
|
|
337
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
338
|
+
var DEFAULT_BATCH_INTERVAL = 5e3;
|
|
339
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
340
|
+
var DEFAULT_RETRY_DELAY = 1e3;
|
|
341
|
+
var Signals = class {
|
|
342
|
+
constructor(config) {
|
|
343
|
+
this.flushTimer = null;
|
|
344
|
+
this.isDestroyed = false;
|
|
345
|
+
this.isFlushing = false;
|
|
346
|
+
if (!config.apiKey) {
|
|
347
|
+
throw new Error("Signals: apiKey is required");
|
|
348
|
+
}
|
|
349
|
+
if (!config.endpoint) {
|
|
350
|
+
throw new Error("Signals: endpoint is required");
|
|
351
|
+
}
|
|
352
|
+
this.config = {
|
|
353
|
+
apiKey: config.apiKey,
|
|
354
|
+
endpoint: config.endpoint,
|
|
355
|
+
batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
356
|
+
batchInterval: config.batchInterval ?? DEFAULT_BATCH_INTERVAL,
|
|
357
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
358
|
+
retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
|
|
359
|
+
debug: config.debug ?? false,
|
|
360
|
+
storage: config.storage ?? getDefaultStorageAdapter(),
|
|
361
|
+
sessionId: config.sessionId ?? uuid()
|
|
362
|
+
};
|
|
363
|
+
this.queue = new EventQueue(this.config.storage, this.config.sessionId);
|
|
364
|
+
this.init();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Initialize client
|
|
368
|
+
*/
|
|
369
|
+
async init() {
|
|
370
|
+
await this.queue.init();
|
|
371
|
+
this.log("Signals initialized", {
|
|
372
|
+
sessionId: this.config.sessionId,
|
|
373
|
+
queueSize: this.queue.size()
|
|
374
|
+
});
|
|
375
|
+
this.startFlushTimer();
|
|
376
|
+
if (!this.queue.isEmpty()) {
|
|
377
|
+
await this.flush();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Track an event
|
|
382
|
+
*/
|
|
383
|
+
async event(eventData) {
|
|
384
|
+
if (this.isDestroyed) {
|
|
385
|
+
throw new Error("Signals instance has been destroyed");
|
|
386
|
+
}
|
|
387
|
+
const event = await this.queue.add(eventData);
|
|
388
|
+
this.log("Event tracked", event);
|
|
389
|
+
if (this.queue.size() >= this.config.batchSize) {
|
|
390
|
+
await this.flush();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Manually flush events
|
|
395
|
+
*/
|
|
396
|
+
async flush() {
|
|
397
|
+
if (this.isDestroyed || this.queue.isEmpty() || this.isFlushing) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.isFlushing = true;
|
|
401
|
+
const events = this.queue.getBatch(this.config.batchSize);
|
|
402
|
+
if (events.length === 0) {
|
|
403
|
+
this.isFlushing = false;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.log(`Flushing ${events.length} events`);
|
|
407
|
+
const batch = {
|
|
408
|
+
api_key: this.config.apiKey,
|
|
409
|
+
batch_id: uuid(),
|
|
410
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
411
|
+
metadata: collectMetadata(VERSION),
|
|
412
|
+
events
|
|
413
|
+
};
|
|
414
|
+
try {
|
|
415
|
+
await this.sendBatch(batch);
|
|
416
|
+
await this.queue.remove(events.length);
|
|
417
|
+
this.log("Batch sent successfully");
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.log("Failed to send batch", error);
|
|
420
|
+
} finally {
|
|
421
|
+
this.isFlushing = false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Send batch to backend with retry logic
|
|
426
|
+
*/
|
|
427
|
+
async sendBatch(batch) {
|
|
428
|
+
await retry(
|
|
429
|
+
async () => {
|
|
430
|
+
const response = await fetch(this.config.endpoint, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: {
|
|
433
|
+
"Content-Type": "application/json",
|
|
434
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify(batch)
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`HTTP ${response.status}: ${response.statusText}`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
maxRetries: this.config.maxRetries,
|
|
446
|
+
initialDelay: this.config.retryDelay,
|
|
447
|
+
onRetry: (attempt, error) => {
|
|
448
|
+
this.log(`Retry attempt ${attempt}`, error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Start periodic flush timer
|
|
455
|
+
*/
|
|
456
|
+
startFlushTimer() {
|
|
457
|
+
this.stopFlushTimer();
|
|
458
|
+
this.flushTimer = setInterval(() => {
|
|
459
|
+
if (!this.queue.isEmpty()) {
|
|
460
|
+
this.flush().catch((error) => {
|
|
461
|
+
this.log("Auto-flush failed", error);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}, this.config.batchInterval);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Stop flush timer
|
|
468
|
+
*/
|
|
469
|
+
stopFlushTimer() {
|
|
470
|
+
if (this.flushTimer) {
|
|
471
|
+
clearInterval(this.flushTimer);
|
|
472
|
+
this.flushTimer = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get current session ID
|
|
477
|
+
*/
|
|
478
|
+
getSessionId() {
|
|
479
|
+
return this.config.sessionId;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Get queue size
|
|
483
|
+
*/
|
|
484
|
+
getQueueSize() {
|
|
485
|
+
return this.queue.size();
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Destroy client and cleanup
|
|
489
|
+
*/
|
|
490
|
+
async destroy() {
|
|
491
|
+
if (this.isDestroyed) return;
|
|
492
|
+
this.log("Destroying Signals instance");
|
|
493
|
+
this.isDestroyed = true;
|
|
494
|
+
this.stopFlushTimer();
|
|
495
|
+
await this.flush();
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Debug logging
|
|
499
|
+
*/
|
|
500
|
+
log(message, data) {
|
|
501
|
+
if (this.config.debug) {
|
|
502
|
+
console.log(`[LoggClient] ${message}`, data ?? "");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
507
|
+
0 && (module.exports = {
|
|
508
|
+
AsyncStorageAdapter,
|
|
509
|
+
EventQueue,
|
|
510
|
+
LocalStorageAdapter,
|
|
511
|
+
MemoryStorageAdapter,
|
|
512
|
+
Signals,
|
|
513
|
+
getDefaultStorageAdapter
|
|
514
|
+
});
|