react-native-qpush 0.0.0-stage → 1.0.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/README.md +439 -2
- package/android/build.gradle +77 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +29 -0
- package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
- package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
- package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
- package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
- package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
- package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
- package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
- package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
- package/android/src/main/res/values/qpush_strings.xml +6 -0
- package/app.plugin.js +1 -0
- package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
- package/ios/Extension/QPushPushExtension.podspec +34 -0
- package/ios/QPushPush.m +62 -0
- package/ios/QPushPush.swift +186 -0
- package/ios/QPushPushBridge.h +5 -0
- package/ios/QPushPushCore.swift +457 -0
- package/ios/QPushPushHooks.m +237 -0
- package/ios/QPushPushSupport.swift +249 -0
- package/ios_nse_template/Info.plist +38 -0
- package/ios_nse_template/NotificationService.entitlements +10 -0
- package/ios_nse_template/NotificationService.swift +44 -0
- package/lib/NativeQPushPush.d.ts +44 -0
- package/lib/NativeQPushPush.js +4 -0
- package/lib/QPush.d.ts +117 -0
- package/lib/QPush.js +206 -0
- package/lib/core/QPushCore.d.ts +148 -0
- package/lib/core/QPushCore.js +1387 -0
- package/lib/core/constants.d.ts +37 -0
- package/lib/core/constants.js +40 -0
- package/lib/core/events.d.ts +11 -0
- package/lib/core/events.js +40 -0
- package/lib/core/http.d.ts +41 -0
- package/lib/core/http.js +123 -0
- package/lib/core/logger.d.ts +18 -0
- package/lib/core/logger.js +43 -0
- package/lib/core/operationQueue.d.ts +117 -0
- package/lib/core/operationQueue.js +264 -0
- package/lib/core/operations.d.ts +80 -0
- package/lib/core/operations.js +88 -0
- package/lib/core/payload.d.ts +20 -0
- package/lib/core/payload.js +187 -0
- package/lib/core/platform.d.ts +72 -0
- package/lib/core/platform.js +7 -0
- package/lib/core/state.d.ts +51 -0
- package/lib/core/state.js +74 -0
- package/lib/core/storage.d.ts +34 -0
- package/lib/core/storage.js +68 -0
- package/lib/core/subscription.d.ts +67 -0
- package/lib/core/subscription.js +98 -0
- package/lib/core/tags.d.ts +39 -0
- package/lib/core/tags.js +123 -0
- package/lib/core/url.d.ts +17 -0
- package/lib/core/url.js +64 -0
- package/lib/core/uuid.d.ts +2 -0
- package/lib/core/uuid.js +18 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +15 -0
- package/lib/notification.d.ts +67 -0
- package/lib/notification.js +73 -0
- package/lib/reactNativePlatform.d.ts +3 -0
- package/lib/reactNativePlatform.js +164 -0
- package/lib/types.d.ts +157 -0
- package/lib/types.js +31 -0
- package/package.json +70 -4
- package/plugin/withQPush.js +330 -0
- package/react-native-qpush.podspec +30 -0
- package/react-native.config.js +18 -0
- package/src/NativeQPushPush.ts +57 -0
- package/src/QPush.ts +292 -0
- package/src/core/QPushCore.ts +1515 -0
- package/src/core/constants.ts +42 -0
- package/src/core/events.ts +34 -0
- package/src/core/http.ts +146 -0
- package/src/core/logger.ts +42 -0
- package/src/core/operationQueue.ts +324 -0
- package/src/core/operations.ts +166 -0
- package/src/core/payload.ts +175 -0
- package/src/core/platform.ts +84 -0
- package/src/core/state.ts +127 -0
- package/src/core/storage.ts +76 -0
- package/src/core/subscription.ts +133 -0
- package/src/core/tags.ts +116 -0
- package/src/core/url.ts +58 -0
- package/src/core/uuid.ts +13 -0
- package/src/index.ts +23 -0
- package/src/notification.ts +113 -0
- package/src/reactNativePlatform.ts +172 -0
- package/src/types.ts +174 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const SDK_NAME = "qpush-react-native";
|
|
2
|
+
export declare const SDK_VERSION_NUMBER = "1.0.0";
|
|
3
|
+
/** Value of the `SDK-Version` header and of `subscription.sdk`. */
|
|
4
|
+
export declare const SDK_VERSION = "qpush-react-native/1.0.0";
|
|
5
|
+
/** Keys shared with native code (SharedPreferences / UserDefaults + App Group). */
|
|
6
|
+
export declare const NativeKeys: {
|
|
7
|
+
readonly appId: "app_id";
|
|
8
|
+
readonly serverUrl: "server_url";
|
|
9
|
+
readonly subscriptionId: "subscription_id";
|
|
10
|
+
};
|
|
11
|
+
/** Keys used by the TypeScript layer for its own persisted state. */
|
|
12
|
+
export declare const StorageKeys: {
|
|
13
|
+
readonly state: "qpush_ts_state_v1";
|
|
14
|
+
readonly queue: "qpush_ts_queue_v1";
|
|
15
|
+
};
|
|
16
|
+
/** Events emitted by the native module. */
|
|
17
|
+
export declare const NativeEventNames: {
|
|
18
|
+
readonly click: "QPushPushClick";
|
|
19
|
+
readonly foregroundWillDisplay: "QPushPushForegroundWillDisplay";
|
|
20
|
+
readonly pushToken: "QPushPushToken";
|
|
21
|
+
};
|
|
22
|
+
/** A new session starts after this much time in background. */
|
|
23
|
+
export declare const SESSION_TIMEOUT_MS = 30000;
|
|
24
|
+
/** Max time to wait for the native token before syncing the subscription anyway. */
|
|
25
|
+
export declare const PUSH_REGISTRATION_TIMEOUT_MS = 20000;
|
|
26
|
+
/** Max time to keep processing the queue after the app goes to background (iOS). */
|
|
27
|
+
export declare const BACKGROUND_FLUSH_TIMEOUT_MS = 20000;
|
|
28
|
+
/** Max number of click events buffered until a listener is attached. */
|
|
29
|
+
export declare const MAX_PENDING_CLICKS = 50;
|
|
30
|
+
/** `notification_types` values from the protocol. */
|
|
31
|
+
export declare const NotificationTypes: {
|
|
32
|
+
readonly subscribed: 1;
|
|
33
|
+
readonly notAsked: 0;
|
|
34
|
+
readonly unsubscribed: -2;
|
|
35
|
+
readonly missingPlayServices: -3;
|
|
36
|
+
readonly apnsFailed: -10;
|
|
37
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationTypes = exports.MAX_PENDING_CLICKS = exports.BACKGROUND_FLUSH_TIMEOUT_MS = exports.PUSH_REGISTRATION_TIMEOUT_MS = exports.SESSION_TIMEOUT_MS = exports.NativeEventNames = exports.StorageKeys = exports.NativeKeys = exports.SDK_VERSION = exports.SDK_VERSION_NUMBER = exports.SDK_NAME = void 0;
|
|
4
|
+
exports.SDK_NAME = 'qpush-react-native';
|
|
5
|
+
exports.SDK_VERSION_NUMBER = '1.0.0';
|
|
6
|
+
/** Value of the `SDK-Version` header and of `subscription.sdk`. */
|
|
7
|
+
exports.SDK_VERSION = `${exports.SDK_NAME}/${exports.SDK_VERSION_NUMBER}`;
|
|
8
|
+
/** Keys shared with native code (SharedPreferences / UserDefaults + App Group). */
|
|
9
|
+
exports.NativeKeys = {
|
|
10
|
+
appId: 'app_id',
|
|
11
|
+
serverUrl: 'server_url',
|
|
12
|
+
subscriptionId: 'subscription_id',
|
|
13
|
+
};
|
|
14
|
+
/** Keys used by the TypeScript layer for its own persisted state. */
|
|
15
|
+
exports.StorageKeys = {
|
|
16
|
+
state: 'qpush_ts_state_v1',
|
|
17
|
+
queue: 'qpush_ts_queue_v1',
|
|
18
|
+
};
|
|
19
|
+
/** Events emitted by the native module. */
|
|
20
|
+
exports.NativeEventNames = {
|
|
21
|
+
click: 'QPushPushClick',
|
|
22
|
+
foregroundWillDisplay: 'QPushPushForegroundWillDisplay',
|
|
23
|
+
pushToken: 'QPushPushToken',
|
|
24
|
+
};
|
|
25
|
+
/** A new session starts after this much time in background. */
|
|
26
|
+
exports.SESSION_TIMEOUT_MS = 30000;
|
|
27
|
+
/** Max time to wait for the native token before syncing the subscription anyway. */
|
|
28
|
+
exports.PUSH_REGISTRATION_TIMEOUT_MS = 20000;
|
|
29
|
+
/** Max time to keep processing the queue after the app goes to background (iOS). */
|
|
30
|
+
exports.BACKGROUND_FLUSH_TIMEOUT_MS = 20000;
|
|
31
|
+
/** Max number of click events buffered until a listener is attached. */
|
|
32
|
+
exports.MAX_PENDING_CLICKS = 50;
|
|
33
|
+
/** `notification_types` values from the protocol. */
|
|
34
|
+
exports.NotificationTypes = {
|
|
35
|
+
subscribed: 1,
|
|
36
|
+
notAsked: 0,
|
|
37
|
+
unsubscribed: -2,
|
|
38
|
+
missingPlayServices: -3,
|
|
39
|
+
apnsFailed: -10,
|
|
40
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Minimal typed event hub (no dependency on Node's EventEmitter). */
|
|
2
|
+
export declare class EventHub<Events extends {
|
|
3
|
+
[K in keyof Events]: unknown;
|
|
4
|
+
}> {
|
|
5
|
+
private readonly listeners;
|
|
6
|
+
add<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void;
|
|
7
|
+
remove<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void;
|
|
8
|
+
count<K extends keyof Events>(event: K): number;
|
|
9
|
+
/** Calls every listener synchronously; a throwing listener does not stop the others. */
|
|
10
|
+
emit<K extends keyof Events>(event: K, payload: Events[K], onError?: (error: unknown) => void): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventHub = void 0;
|
|
4
|
+
/** Minimal typed event hub (no dependency on Node's EventEmitter). */
|
|
5
|
+
class EventHub {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.listeners = {};
|
|
8
|
+
}
|
|
9
|
+
add(event, listener) {
|
|
10
|
+
let set = this.listeners[event];
|
|
11
|
+
if (!set) {
|
|
12
|
+
set = new Set();
|
|
13
|
+
this.listeners[event] = set;
|
|
14
|
+
}
|
|
15
|
+
set.add(listener);
|
|
16
|
+
}
|
|
17
|
+
remove(event, listener) {
|
|
18
|
+
var _a;
|
|
19
|
+
(_a = this.listeners[event]) === null || _a === void 0 ? void 0 : _a.delete(listener);
|
|
20
|
+
}
|
|
21
|
+
count(event) {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
return (_b = (_a = this.listeners[event]) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0;
|
|
24
|
+
}
|
|
25
|
+
/** Calls every listener synchronously; a throwing listener does not stop the others. */
|
|
26
|
+
emit(event, payload, onError) {
|
|
27
|
+
const set = this.listeners[event];
|
|
28
|
+
if (!set)
|
|
29
|
+
return;
|
|
30
|
+
for (const listener of Array.from(set)) {
|
|
31
|
+
try {
|
|
32
|
+
listener(payload);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
onError === null || onError === void 0 ? void 0 : onError(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.EventHub = EventHub;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Logger } from './logger';
|
|
2
|
+
import type { ExecutionResult } from './operationQueue';
|
|
3
|
+
import type { FetchLike, Scheduler } from './platform';
|
|
4
|
+
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
5
|
+
export interface HttpResponse<T = unknown> {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
/** HTTP status, `0` for network failures / timeouts. */
|
|
8
|
+
status: number;
|
|
9
|
+
data?: T;
|
|
10
|
+
/** First `errors[].code` of an error response. */
|
|
11
|
+
errorCode?: string;
|
|
12
|
+
/** First `errors[].title` of an error response. */
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
retryAfterMs?: number;
|
|
15
|
+
networkError?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface HttpClientOptions {
|
|
18
|
+
fetch: FetchLike;
|
|
19
|
+
getBaseUrl: () => string | undefined;
|
|
20
|
+
getSubscriptionId: () => string | undefined;
|
|
21
|
+
scheduler: Scheduler;
|
|
22
|
+
logger: Logger;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
sdkVersion?: string;
|
|
26
|
+
}
|
|
27
|
+
/** 429 and 5xx are retryable (protocol), as are network failures (status 0). */
|
|
28
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
29
|
+
/** Parses a `Retry-After` header (seconds or HTTP date). */
|
|
30
|
+
export declare function parseRetryAfter(value: string | null | undefined, now?: number): number | undefined;
|
|
31
|
+
/** Maps a failed (or successful) response to a queue execution result. */
|
|
32
|
+
export declare function classifyResponse(response: HttpResponse): ExecutionResult;
|
|
33
|
+
export declare class HttpClient {
|
|
34
|
+
private readonly options;
|
|
35
|
+
private readonly timeoutMs;
|
|
36
|
+
private readonly sdkVersion;
|
|
37
|
+
private readonly now;
|
|
38
|
+
constructor(options: HttpClientOptions);
|
|
39
|
+
headers(): Record<string, string>;
|
|
40
|
+
request<T = unknown>(method: HttpMethod, path: string, body?: unknown): Promise<HttpResponse<T>>;
|
|
41
|
+
}
|
package/lib/core/http.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpClient = void 0;
|
|
4
|
+
exports.isRetryableStatus = isRetryableStatus;
|
|
5
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
6
|
+
exports.classifyResponse = classifyResponse;
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const url_1 = require("./url");
|
|
9
|
+
/** 429 and 5xx are retryable (protocol), as are network failures (status 0). */
|
|
10
|
+
function isRetryableStatus(status) {
|
|
11
|
+
return status === 0 || status === 429 || status >= 500;
|
|
12
|
+
}
|
|
13
|
+
/** Parses a `Retry-After` header (seconds or HTTP date). */
|
|
14
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return undefined;
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
19
|
+
return Math.round(parseFloat(trimmed) * 1000);
|
|
20
|
+
const date = Date.parse(trimmed);
|
|
21
|
+
if (Number.isNaN(date))
|
|
22
|
+
return undefined;
|
|
23
|
+
return Math.max(0, date - now);
|
|
24
|
+
}
|
|
25
|
+
/** Maps a failed (or successful) response to a queue execution result. */
|
|
26
|
+
function classifyResponse(response) {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
if (response.ok)
|
|
29
|
+
return { type: 'success' };
|
|
30
|
+
if (response.status === 0)
|
|
31
|
+
return { type: 'retry', countAttempt: false };
|
|
32
|
+
if (isRetryableStatus(response.status))
|
|
33
|
+
return { type: 'retry', retryAfterMs: response.retryAfterMs };
|
|
34
|
+
const detail = (_b = (_a = response.errorCode) !== null && _a !== void 0 ? _a : response.errorMessage) !== null && _b !== void 0 ? _b : '';
|
|
35
|
+
return { type: 'drop', reason: `HTTP ${response.status}${detail ? ` (${detail})` : ''}` };
|
|
36
|
+
}
|
|
37
|
+
class HttpClient {
|
|
38
|
+
constructor(options) {
|
|
39
|
+
var _a, _b, _c;
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.timeoutMs = (_a = options.timeoutMs) !== null && _a !== void 0 ? _a : 30000;
|
|
42
|
+
this.sdkVersion = (_b = options.sdkVersion) !== null && _b !== void 0 ? _b : constants_1.SDK_VERSION;
|
|
43
|
+
this.now = (_c = options.now) !== null && _c !== void 0 ? _c : Date.now;
|
|
44
|
+
}
|
|
45
|
+
headers() {
|
|
46
|
+
const headers = {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
Accept: 'application/json',
|
|
49
|
+
'SDK-Version': this.sdkVersion,
|
|
50
|
+
};
|
|
51
|
+
const subscriptionId = this.options.getSubscriptionId();
|
|
52
|
+
if (subscriptionId)
|
|
53
|
+
headers['QPush-Subscription-Id'] = subscriptionId;
|
|
54
|
+
return headers;
|
|
55
|
+
}
|
|
56
|
+
async request(method, path, body) {
|
|
57
|
+
var _a, _b, _c, _d, _e, _f;
|
|
58
|
+
const base = this.options.getBaseUrl();
|
|
59
|
+
if (!base) {
|
|
60
|
+
return { ok: false, status: 0, networkError: true, errorMessage: 'SDK not initialized' };
|
|
61
|
+
}
|
|
62
|
+
const url = (0, url_1.joinUrl)(base, path);
|
|
63
|
+
const init = {
|
|
64
|
+
method,
|
|
65
|
+
headers: this.headers(),
|
|
66
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
67
|
+
signal: undefined,
|
|
68
|
+
};
|
|
69
|
+
let controller;
|
|
70
|
+
if (typeof AbortController !== 'undefined') {
|
|
71
|
+
controller = new AbortController();
|
|
72
|
+
init.signal = controller.signal;
|
|
73
|
+
}
|
|
74
|
+
let timedOut = false;
|
|
75
|
+
const timer = this.options.scheduler.setTimeout(() => {
|
|
76
|
+
timedOut = true;
|
|
77
|
+
controller === null || controller === void 0 ? void 0 : controller.abort();
|
|
78
|
+
}, this.timeoutMs);
|
|
79
|
+
this.options.logger.verbose(`HTTP ${method} ${url}`, (_a = init.body) !== null && _a !== void 0 ? _a : '');
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.options.fetch(url, init);
|
|
82
|
+
const text = await response.text();
|
|
83
|
+
let parsed;
|
|
84
|
+
if (text) {
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
parsed = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const ok = response.status >= 200 && response.status < 300;
|
|
93
|
+
const result = { ok, status: response.status };
|
|
94
|
+
if (ok) {
|
|
95
|
+
result.data = parsed;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const errors = parsed === null || parsed === void 0 ? void 0 : parsed.errors;
|
|
99
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
100
|
+
result.errorCode = (_b = errors[0]) === null || _b === void 0 ? void 0 : _b.code;
|
|
101
|
+
result.errorMessage = (_c = errors[0]) === null || _c === void 0 ? void 0 : _c.title;
|
|
102
|
+
}
|
|
103
|
+
result.retryAfterMs = parseRetryAfter((_f = (_e = (_d = response.headers) === null || _d === void 0 ? void 0 : _d.get) === null || _e === void 0 ? void 0 : _e.call(_d, 'Retry-After')) !== null && _f !== void 0 ? _f : null, this.now());
|
|
104
|
+
this.options.logger.debug(`HTTP ${method} ${path} -> ${response.status}`, text);
|
|
105
|
+
}
|
|
106
|
+
this.options.logger.verbose(`HTTP ${method} ${path} -> ${response.status}`);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.options.logger.debug(`HTTP ${method} ${path} failed${timedOut ? ' (timeout)' : ''}`, error);
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
status: 0,
|
|
114
|
+
networkError: true,
|
|
115
|
+
errorMessage: timedOut ? 'timeout' : String(error),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
this.options.scheduler.clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.HttpClient = HttpClient;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LogLevel } from '../types';
|
|
2
|
+
type ConsoleLike = Pick<Console, 'error' | 'warn' | 'info' | 'log'>;
|
|
3
|
+
/** Tiny leveled logger. Default level is `Warn`, like OneSignal. */
|
|
4
|
+
export declare class Logger {
|
|
5
|
+
private readonly sink;
|
|
6
|
+
private readonly prefix;
|
|
7
|
+
private level;
|
|
8
|
+
constructor(sink?: ConsoleLike, prefix?: string);
|
|
9
|
+
setLevel(level: LogLevel): void;
|
|
10
|
+
getLevel(): LogLevel;
|
|
11
|
+
fatal(message: string, ...args: unknown[]): void;
|
|
12
|
+
error(message: string, ...args: unknown[]): void;
|
|
13
|
+
warn(message: string, ...args: unknown[]): void;
|
|
14
|
+
info(message: string, ...args: unknown[]): void;
|
|
15
|
+
debug(message: string, ...args: unknown[]): void;
|
|
16
|
+
verbose(message: string, ...args: unknown[]): void;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Logger = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
/** Tiny leveled logger. Default level is `Warn`, like OneSignal. */
|
|
6
|
+
class Logger {
|
|
7
|
+
constructor(sink = console, prefix = '[QPush]') {
|
|
8
|
+
this.sink = sink;
|
|
9
|
+
this.prefix = prefix;
|
|
10
|
+
this.level = types_1.LogLevel.Warn;
|
|
11
|
+
}
|
|
12
|
+
setLevel(level) {
|
|
13
|
+
this.level = level;
|
|
14
|
+
}
|
|
15
|
+
getLevel() {
|
|
16
|
+
return this.level;
|
|
17
|
+
}
|
|
18
|
+
fatal(message, ...args) {
|
|
19
|
+
if (this.level >= types_1.LogLevel.Fatal)
|
|
20
|
+
this.sink.error(`${this.prefix} ${message}`, ...args);
|
|
21
|
+
}
|
|
22
|
+
error(message, ...args) {
|
|
23
|
+
if (this.level >= types_1.LogLevel.Error)
|
|
24
|
+
this.sink.error(`${this.prefix} ${message}`, ...args);
|
|
25
|
+
}
|
|
26
|
+
warn(message, ...args) {
|
|
27
|
+
if (this.level >= types_1.LogLevel.Warn)
|
|
28
|
+
this.sink.warn(`${this.prefix} ${message}`, ...args);
|
|
29
|
+
}
|
|
30
|
+
info(message, ...args) {
|
|
31
|
+
if (this.level >= types_1.LogLevel.Info)
|
|
32
|
+
this.sink.info(`${this.prefix} ${message}`, ...args);
|
|
33
|
+
}
|
|
34
|
+
debug(message, ...args) {
|
|
35
|
+
if (this.level >= types_1.LogLevel.Debug)
|
|
36
|
+
this.sink.log(`${this.prefix} ${message}`, ...args);
|
|
37
|
+
}
|
|
38
|
+
verbose(message, ...args) {
|
|
39
|
+
if (this.level >= types_1.LogLevel.Verbose)
|
|
40
|
+
this.sink.log(`${this.prefix} ${message}`, ...args);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.Logger = Logger;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Logger } from './logger';
|
|
2
|
+
import { type Operation } from './operations';
|
|
3
|
+
import { type Scheduler } from './platform';
|
|
4
|
+
import { type KeyValueStore } from './storage';
|
|
5
|
+
export interface QueueItem {
|
|
6
|
+
id: string;
|
|
7
|
+
operation: Operation;
|
|
8
|
+
/** Failed attempts that count towards `maxAttempts` (429 / 5xx). */
|
|
9
|
+
attempts: number;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
export type ExecutionResult = {
|
|
13
|
+
type: 'success';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Retry later with exponential backoff. `countAttempt: false` is used for
|
|
17
|
+
* network failures (offline): they are retried forever with backoff so
|
|
18
|
+
* offline changes are delivered later, instead of being dropped.
|
|
19
|
+
*/
|
|
20
|
+
| {
|
|
21
|
+
type: 'retry';
|
|
22
|
+
retryAfterMs?: number;
|
|
23
|
+
countAttempt?: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'drop';
|
|
26
|
+
reason?: string;
|
|
27
|
+
};
|
|
28
|
+
export interface OperationQueueOptions {
|
|
29
|
+
store: KeyValueStore;
|
|
30
|
+
storageKey: string;
|
|
31
|
+
execute: (operation: Operation, item: QueueItem) => Promise<ExecutionResult>;
|
|
32
|
+
/** Max counted attempts before an operation is dropped (default 5). */
|
|
33
|
+
maxAttempts?: number;
|
|
34
|
+
baseDelayMs?: number;
|
|
35
|
+
maxDelayMs?: number;
|
|
36
|
+
/** Processing only happens while this returns true. */
|
|
37
|
+
canProcess?: () => boolean;
|
|
38
|
+
onDrop?: (item: QueueItem, reason: string) => void;
|
|
39
|
+
scheduler?: Scheduler;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
random?: () => number;
|
|
42
|
+
logger?: Logger;
|
|
43
|
+
}
|
|
44
|
+
export interface BackoffOptions {
|
|
45
|
+
baseDelayMs: number;
|
|
46
|
+
maxDelayMs: number;
|
|
47
|
+
/** 0..1, adds up to ±25% jitter. Pass 0.5 for no jitter. */
|
|
48
|
+
random: number;
|
|
49
|
+
retryAfterMs?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Exponential backoff: base * 2^(level-1), capped at max, ±25% jitter.
|
|
53
|
+
* A server provided `Retry-After` wins when it is larger.
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeBackoffDelay(level: number, options: BackoffOptions): number;
|
|
56
|
+
/**
|
|
57
|
+
* Persisted, ordered operation queue.
|
|
58
|
+
*
|
|
59
|
+
* - Operations run strictly one at a time, in order (head-of-line blocking is
|
|
60
|
+
* intended: later operations may depend on earlier ones).
|
|
61
|
+
* - Every change is persisted, so pending operations survive restarts.
|
|
62
|
+
* - Adjacent compatible operations are merged (see `mergeOperations`).
|
|
63
|
+
* - 429 / 5xx are retried with exponential backoff up to `maxAttempts`;
|
|
64
|
+
* network failures are retried without limit; other errors drop the item.
|
|
65
|
+
*/
|
|
66
|
+
export declare class OperationQueue {
|
|
67
|
+
private readonly options;
|
|
68
|
+
private items;
|
|
69
|
+
private loaded;
|
|
70
|
+
private inFlightId;
|
|
71
|
+
private running;
|
|
72
|
+
private retryTimer;
|
|
73
|
+
private retryAt;
|
|
74
|
+
/** Consecutive failures of the head item (drives the backoff delay). */
|
|
75
|
+
private backoffLevel;
|
|
76
|
+
private sequence;
|
|
77
|
+
private readonly writer;
|
|
78
|
+
private readonly scheduler;
|
|
79
|
+
private readonly now;
|
|
80
|
+
private readonly random;
|
|
81
|
+
private readonly logger;
|
|
82
|
+
private readonly maxAttempts;
|
|
83
|
+
private readonly baseDelayMs;
|
|
84
|
+
private readonly maxDelayMs;
|
|
85
|
+
constructor(options: OperationQueueOptions);
|
|
86
|
+
/** Loads persisted items. Items enqueued before `load()` are kept after them. */
|
|
87
|
+
load(): Promise<void>;
|
|
88
|
+
get isLoaded(): boolean;
|
|
89
|
+
/** Snapshot of pending items (head first). */
|
|
90
|
+
get pending(): readonly QueueItem[];
|
|
91
|
+
get size(): number;
|
|
92
|
+
/** Id of the item currently executing, if any. */
|
|
93
|
+
get inFlight(): string | null;
|
|
94
|
+
/** Time of the next scheduled retry, if the queue is backing off. */
|
|
95
|
+
get nextRetryAt(): number | null;
|
|
96
|
+
has(predicate: (operation: Operation, item: QueueItem) => boolean): boolean;
|
|
97
|
+
/** Adds an operation (merged into the tail when possible) and processes. */
|
|
98
|
+
enqueue(operation: Operation): void;
|
|
99
|
+
/** Removes pending (not in-flight) items matching the predicate. */
|
|
100
|
+
remove(predicate: (operation: Operation, item: QueueItem) => boolean): number;
|
|
101
|
+
/** Drops everything that is not in flight. */
|
|
102
|
+
clear(): void;
|
|
103
|
+
/** Cancels any backoff wait and processes immediately. */
|
|
104
|
+
kick(): void;
|
|
105
|
+
/** Resolves when the current processing run is over (empty, blocked or backing off). */
|
|
106
|
+
idle(): Promise<void>;
|
|
107
|
+
/** Resolves when all pending writes reached the store. */
|
|
108
|
+
persisted(): Promise<void>;
|
|
109
|
+
/** Starts a processing run if possible. Returns the run promise. */
|
|
110
|
+
process(): Promise<void>;
|
|
111
|
+
private afterRun;
|
|
112
|
+
private append;
|
|
113
|
+
private nextId;
|
|
114
|
+
private persist;
|
|
115
|
+
private runLoop;
|
|
116
|
+
private removeById;
|
|
117
|
+
}
|