@wokuapp/react-native 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 +139 -0
- package/dist/index.cjs +334 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +279 -0
- package/dist/index.d.ts +279 -0
- package/dist/index.js +322 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Woku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @wokuapp/react-native
|
|
4
|
+
|
|
5
|
+
Capture **Woku ratings** and **NPS** (text + audio) from your React Native
|
|
6
|
+
app, with offline buffering and quarantine-aware delivery.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@wokuapp/react-native)
|
|
9
|
+
[](../../LICENSE)
|
|
10
|
+
[](https://www.npmjs.com/package/@wokuapp/react-native)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Drop Woku feedback capture into an existing iOS/Android app without building
|
|
17
|
+
your own ingestion, retry, or offline logic. The SDK:
|
|
18
|
+
|
|
19
|
+
- Captures **Woku ratings** (1–5) and **NPS** (0–10) with optional text or
|
|
20
|
+
audio comments.
|
|
21
|
+
- **Buffers offline** and retries automatically when connectivity returns —
|
|
22
|
+
a tap on the subway never loses a response.
|
|
23
|
+
- Is **quarantine-aware**: when the backend rate-limits a respondent (HTTP
|
|
24
|
+
429), the SDK backs off instead of hammering it.
|
|
25
|
+
- Has **zero runtime dependencies** and a **fully typed**, framework-agnostic
|
|
26
|
+
core: you inject the platform adapters (storage, http, audio), so it stays
|
|
27
|
+
small and testable.
|
|
28
|
+
|
|
29
|
+
> **Architecture note (v0.1).** This release ships the headless TypeScript
|
|
30
|
+
> core plus the adapter interfaces. Pre-built React Native adapters
|
|
31
|
+
> (MMKV storage, audio recorder) ship in a follow-up minor; until then you
|
|
32
|
+
> wire your app's libraries to the small interfaces below (a few lines).
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @wokuapp/react-native
|
|
38
|
+
# or
|
|
39
|
+
pnpm add @wokuapp/react-native
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`react` and `react-native` are optional peers (only needed once the native
|
|
43
|
+
adapters land).
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { WokuSdk } from '@wokuapp/react-native';
|
|
49
|
+
|
|
50
|
+
const woku = new WokuSdk({
|
|
51
|
+
apiUrl: 'https://api.woku.app',
|
|
52
|
+
publicKey: 'pk_live_xxx', // per-company SDK key
|
|
53
|
+
companyId: 'company_123',
|
|
54
|
+
storage: mmkvStorageAdapter, // see "Adapters" below
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// NPS capture (0–10)
|
|
58
|
+
await woku.captureNps({
|
|
59
|
+
npsId: 'nps_q2_2026',
|
|
60
|
+
score: 9,
|
|
61
|
+
comment: 'Fast checkout, loved it.',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Woku rating (1–5) with an audio comment
|
|
65
|
+
await woku.captureWoku({
|
|
66
|
+
wokuId: 'woku_store_centro',
|
|
67
|
+
rating: 5,
|
|
68
|
+
audio: { uri: fileUri, mimeType: 'audio/m4a', durationMs: 4200 },
|
|
69
|
+
respondent: { email: 'ana@example.com' },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Flush queued captures (e.g. on app foreground / reconnect)
|
|
73
|
+
const { sent, remaining } = await woku.flush();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Every capture resolves to a `SubmissionResult` with a `status` of
|
|
77
|
+
`sent` | `queued` | `quarantined` | `failed` — it never throws on network
|
|
78
|
+
loss; the submission is queued and retried.
|
|
79
|
+
|
|
80
|
+
## Adapters
|
|
81
|
+
|
|
82
|
+
The core depends on three small interfaces. Wire them to your app's libraries.
|
|
83
|
+
|
|
84
|
+
### Storage (required for offline buffering)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { MMKV } from 'react-native-mmkv';
|
|
88
|
+
import type { Storage } from '@wokuapp/react-native';
|
|
89
|
+
|
|
90
|
+
const mmkv = new MMKV();
|
|
91
|
+
export const mmkvStorageAdapter: Storage = {
|
|
92
|
+
getItem: (k) => mmkv.getString(k) ?? null,
|
|
93
|
+
setItem: (k, v) => mmkv.set(k, v),
|
|
94
|
+
removeItem: (k) => mmkv.delete(k),
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Don't pass `storage` and the SDK falls back to in-memory (lost on restart).
|
|
99
|
+
|
|
100
|
+
### HTTP (optional)
|
|
101
|
+
|
|
102
|
+
Defaults to the global `fetch`. Override to add tracing, custom TLS, etc.:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import type { HttpClient } from '@wokuapp/react-native';
|
|
106
|
+
const http: HttpClient = { request: async (req) => /* ... */ };
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Offline & retry semantics
|
|
110
|
+
|
|
111
|
+
`flush()` walks the queue oldest-first:
|
|
112
|
+
|
|
113
|
+
| Outcome | Behavior |
|
|
114
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
115
|
+
| `sent` | removed from the queue |
|
|
116
|
+
| quarantine (429) | **stops** the flush, keeps everything for the next attempt |
|
|
117
|
+
| network error | attempt count bumped, item kept, flush continues |
|
|
118
|
+
| `failed` (4xx/5xx) | attempt count bumped; dropped after `maxQueueAttempts` (default 8) so a bad item can't wedge the queue |
|
|
119
|
+
|
|
120
|
+
The queue persists through your `Storage` adapter, so it survives restarts.
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
| Member | Description |
|
|
125
|
+
| --------------------- | --------------------------------------------------- |
|
|
126
|
+
| `new WokuSdk(config)` | Create an SDK instance. |
|
|
127
|
+
| `captureWoku(input)` | Submit a 1–5 rating (+ comment/audio). |
|
|
128
|
+
| `captureNps(input)` | Submit a 0–10 NPS score (+ review). |
|
|
129
|
+
| `flush()` | Retry all queued captures. Returns a `FlushResult`. |
|
|
130
|
+
| `pendingCount()` | Number of captures waiting to send. |
|
|
131
|
+
| `clearQueue()` | Drop the queue (e.g. on logout). |
|
|
132
|
+
|
|
133
|
+
Lower-level building blocks `WokuClient` and `OfflineQueue` are exported too,
|
|
134
|
+
along with all types and error classes (`WokuValidationError`,
|
|
135
|
+
`WokuQuarantineError`, `WokuNetworkError`, …).
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
[MIT](../../LICENSE) © Woku
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/adapters.ts
|
|
4
|
+
var fetchHttpClient = {
|
|
5
|
+
async request(req) {
|
|
6
|
+
const res = await fetch(req.url, {
|
|
7
|
+
method: req.method,
|
|
8
|
+
headers: req.headers,
|
|
9
|
+
body: req.body
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
status: res.status,
|
|
13
|
+
ok: res.ok,
|
|
14
|
+
json: () => res.json(),
|
|
15
|
+
headers: { get: (name) => res.headers.get(name) }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var InMemoryStorage = class {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.map = /* @__PURE__ */ new Map();
|
|
22
|
+
}
|
|
23
|
+
getItem(key) {
|
|
24
|
+
return this.map.has(key) ? this.map.get(key) : null;
|
|
25
|
+
}
|
|
26
|
+
setItem(key, value) {
|
|
27
|
+
this.map.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
removeItem(key) {
|
|
30
|
+
this.map.delete(key);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var noopLogger = {
|
|
34
|
+
debug: () => void 0,
|
|
35
|
+
warn: () => void 0,
|
|
36
|
+
error: () => void 0
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/errors.ts
|
|
40
|
+
var WokuError = class extends Error {
|
|
41
|
+
constructor(message, code) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.name = "WokuError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var WokuConfigError = class extends WokuError {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message, "config_error");
|
|
50
|
+
this.name = "WokuConfigError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var WokuValidationError = class extends WokuError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message, "validation_error");
|
|
56
|
+
this.name = "WokuValidationError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var WokuQuarantineError = class extends WokuError {
|
|
60
|
+
constructor(message, retryAfterSeconds) {
|
|
61
|
+
super(message, "quarantine_blocked");
|
|
62
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
63
|
+
this.name = "WokuQuarantineError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var WokuNetworkError = class extends WokuError {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message, "network_error");
|
|
69
|
+
this.name = "WokuNetworkError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/client.ts
|
|
74
|
+
var DEFAULT_CAPTURE_PATH = "/sdk/v1/captures";
|
|
75
|
+
var WokuClient = class {
|
|
76
|
+
constructor(config) {
|
|
77
|
+
this.config = config;
|
|
78
|
+
if (!config.apiUrl) throw new WokuConfigError("apiUrl is required");
|
|
79
|
+
if (!config.publicKey) throw new WokuConfigError("publicKey is required");
|
|
80
|
+
if (!config.companyId) throw new WokuConfigError("companyId is required");
|
|
81
|
+
this.http = config.http ?? fetchHttpClient;
|
|
82
|
+
this.logger = config.logger ?? noopLogger;
|
|
83
|
+
const base = config.apiUrl.replace(/\/$/, "");
|
|
84
|
+
const path = config.capturePath ?? DEFAULT_CAPTURE_PATH;
|
|
85
|
+
this.endpoint = `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Sends one submission. Resolves with a `sent` result on 2xx, throws
|
|
89
|
+
* {@link WokuQuarantineError} on a quarantine 429, {@link WokuNetworkError}
|
|
90
|
+
* on transport failure, and resolves with a `failed` result on other
|
|
91
|
+
* non-2xx responses (so the caller can decide to re-queue).
|
|
92
|
+
*/
|
|
93
|
+
async send(submission) {
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await this.http.request({
|
|
97
|
+
method: "POST",
|
|
98
|
+
url: this.endpoint,
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
Authorization: `Bearer ${this.config.publicKey}`,
|
|
102
|
+
"X-Woku-Company": this.config.companyId,
|
|
103
|
+
"X-Woku-Idempotency-Key": submission.id
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(submission)
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new WokuNetworkError(err.message);
|
|
109
|
+
}
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
const body2 = await this.safeJson(response.json);
|
|
112
|
+
return { id: submission.id, status: "sent", remoteId: body2?.id };
|
|
113
|
+
}
|
|
114
|
+
if (response.status === 429) {
|
|
115
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
116
|
+
throw new WokuQuarantineError(
|
|
117
|
+
"Submission blocked by quarantine",
|
|
118
|
+
Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const body = await this.safeJson(response.json);
|
|
122
|
+
const error = body?.message ?? `HTTP ${response.status}`;
|
|
123
|
+
this.logger.warn(`woku capture rejected: ${error}`);
|
|
124
|
+
return { id: submission.id, status: "failed", error };
|
|
125
|
+
}
|
|
126
|
+
async safeJson(json) {
|
|
127
|
+
try {
|
|
128
|
+
return await json();
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/queue.ts
|
|
136
|
+
var DEFAULT_KEY = "woku.sdk.queue.v1";
|
|
137
|
+
var DEFAULT_MAX_ATTEMPTS = 8;
|
|
138
|
+
var OfflineQueue = class {
|
|
139
|
+
constructor(opts = {}) {
|
|
140
|
+
this.items = null;
|
|
141
|
+
this.storage = opts.storage ?? new InMemoryStorage();
|
|
142
|
+
this.key = opts.storageKey ?? DEFAULT_KEY;
|
|
143
|
+
this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
144
|
+
}
|
|
145
|
+
async load() {
|
|
146
|
+
if (this.items) return this.items;
|
|
147
|
+
const raw = await this.storage.getItem(this.key);
|
|
148
|
+
if (!raw) {
|
|
149
|
+
this.items = [];
|
|
150
|
+
return this.items;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(raw);
|
|
154
|
+
this.items = Array.isArray(parsed) ? parsed : [];
|
|
155
|
+
} catch {
|
|
156
|
+
this.items = [];
|
|
157
|
+
}
|
|
158
|
+
return this.items;
|
|
159
|
+
}
|
|
160
|
+
async persist() {
|
|
161
|
+
await this.storage.setItem(this.key, JSON.stringify(this.items ?? []));
|
|
162
|
+
}
|
|
163
|
+
/** Adds a submission to the tail of the queue (dedup by id). */
|
|
164
|
+
async enqueue(submission) {
|
|
165
|
+
const items = await this.load();
|
|
166
|
+
if (items.some((i) => i.submission.id === submission.id)) return;
|
|
167
|
+
items.push({ submission, attempts: 0 });
|
|
168
|
+
await this.persist();
|
|
169
|
+
}
|
|
170
|
+
async size() {
|
|
171
|
+
return (await this.load()).length;
|
|
172
|
+
}
|
|
173
|
+
/** Returns a copy of the pending submissions (oldest first). */
|
|
174
|
+
async pending() {
|
|
175
|
+
return (await this.load()).map((i) => i.submission);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Attempts to deliver every queued submission through `send`, applying
|
|
179
|
+
* the delivery semantics documented on the class. Persists once at the
|
|
180
|
+
* end and returns a summary.
|
|
181
|
+
*/
|
|
182
|
+
async flush(send) {
|
|
183
|
+
const items = await this.load();
|
|
184
|
+
const survivors = [];
|
|
185
|
+
let sent = 0;
|
|
186
|
+
let failed = 0;
|
|
187
|
+
let quarantined = false;
|
|
188
|
+
for (let index = 0; index < items.length; index++) {
|
|
189
|
+
const item = items[index];
|
|
190
|
+
if (quarantined) {
|
|
191
|
+
survivors.push(item);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const result = await send(item.submission);
|
|
196
|
+
if (result.status === "sent") {
|
|
197
|
+
sent++;
|
|
198
|
+
} else {
|
|
199
|
+
item.attempts++;
|
|
200
|
+
if (item.attempts >= this.maxAttempts) failed++;
|
|
201
|
+
else survivors.push(item);
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err instanceof WokuQuarantineError) {
|
|
205
|
+
quarantined = true;
|
|
206
|
+
survivors.push(item);
|
|
207
|
+
} else if (err instanceof WokuNetworkError) {
|
|
208
|
+
item.attempts++;
|
|
209
|
+
survivors.push(item);
|
|
210
|
+
} else {
|
|
211
|
+
item.attempts++;
|
|
212
|
+
if (item.attempts >= this.maxAttempts) failed++;
|
|
213
|
+
else survivors.push(item);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.items = survivors;
|
|
218
|
+
await this.persist();
|
|
219
|
+
return { sent, failed, remaining: survivors.length, quarantined };
|
|
220
|
+
}
|
|
221
|
+
/** Clears the queue (e.g. on logout). */
|
|
222
|
+
async clear() {
|
|
223
|
+
this.items = [];
|
|
224
|
+
await this.storage.removeItem(this.key);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/sdk.ts
|
|
229
|
+
function generateId() {
|
|
230
|
+
const g = globalThis;
|
|
231
|
+
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
232
|
+
return `wk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
233
|
+
}
|
|
234
|
+
function assertInt(value, min, max, field) {
|
|
235
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
236
|
+
throw new WokuValidationError(
|
|
237
|
+
`${field} must be an integer between ${min} and ${max}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
var WokuSdk = class {
|
|
242
|
+
constructor(config) {
|
|
243
|
+
this.client = new WokuClient(config);
|
|
244
|
+
this.queue = new OfflineQueue({
|
|
245
|
+
storage: config.storage,
|
|
246
|
+
maxAttempts: config.maxQueueAttempts
|
|
247
|
+
});
|
|
248
|
+
this.logger = config.logger ?? noopLogger;
|
|
249
|
+
this.companyId = config.companyId;
|
|
250
|
+
}
|
|
251
|
+
/** Captures a Woku rating (1..5) with optional text/audio comment. */
|
|
252
|
+
async captureWoku(input) {
|
|
253
|
+
assertInt(input.rating, 1, 5, "rating");
|
|
254
|
+
const submission = {
|
|
255
|
+
id: generateId(),
|
|
256
|
+
kind: "woku",
|
|
257
|
+
companyId: this.companyId,
|
|
258
|
+
targetId: input.wokuId,
|
|
259
|
+
rating: input.rating,
|
|
260
|
+
comment: input.comment,
|
|
261
|
+
audio: input.audio,
|
|
262
|
+
respondent: input.respondent,
|
|
263
|
+
metadata: input.metadata,
|
|
264
|
+
createdAt: Date.now()
|
|
265
|
+
};
|
|
266
|
+
return this.deliver(submission);
|
|
267
|
+
}
|
|
268
|
+
/** Captures an NPS score (0..10) with optional text/audio review. */
|
|
269
|
+
async captureNps(input) {
|
|
270
|
+
assertInt(input.score, 0, 10, "score");
|
|
271
|
+
const submission = {
|
|
272
|
+
id: generateId(),
|
|
273
|
+
kind: "nps",
|
|
274
|
+
companyId: this.companyId,
|
|
275
|
+
targetId: input.npsId,
|
|
276
|
+
score: input.score,
|
|
277
|
+
comment: input.comment,
|
|
278
|
+
audio: input.audio,
|
|
279
|
+
respondent: input.respondent,
|
|
280
|
+
metadata: input.metadata,
|
|
281
|
+
createdAt: Date.now()
|
|
282
|
+
};
|
|
283
|
+
return this.deliver(submission);
|
|
284
|
+
}
|
|
285
|
+
/** Retries every queued submission. Safe to call on app foreground or
|
|
286
|
+
* when connectivity is regained. */
|
|
287
|
+
flush() {
|
|
288
|
+
return this.queue.flush((s) => this.client.send(s));
|
|
289
|
+
}
|
|
290
|
+
/** Number of submissions waiting to be delivered. */
|
|
291
|
+
pendingCount() {
|
|
292
|
+
return this.queue.size();
|
|
293
|
+
}
|
|
294
|
+
/** Clears the offline queue (e.g. on user logout). */
|
|
295
|
+
clearQueue() {
|
|
296
|
+
return this.queue.clear();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Tries to send immediately; on quarantine or network failure the
|
|
300
|
+
* submission is queued for a later flush rather than thrown away.
|
|
301
|
+
*/
|
|
302
|
+
async deliver(submission) {
|
|
303
|
+
try {
|
|
304
|
+
const result = await this.client.send(submission);
|
|
305
|
+
if (result.status === "sent") return result;
|
|
306
|
+
await this.queue.enqueue(submission);
|
|
307
|
+
return { ...result, status: "queued" };
|
|
308
|
+
} catch (err) {
|
|
309
|
+
await this.queue.enqueue(submission);
|
|
310
|
+
if (err instanceof WokuQuarantineError) {
|
|
311
|
+
return { id: submission.id, status: "quarantined", error: err.message };
|
|
312
|
+
}
|
|
313
|
+
if (err instanceof WokuNetworkError) {
|
|
314
|
+
this.logger.debug("capture queued (offline)", { id: submission.id });
|
|
315
|
+
return { id: submission.id, status: "queued" };
|
|
316
|
+
}
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
exports.InMemoryStorage = InMemoryStorage;
|
|
323
|
+
exports.OfflineQueue = OfflineQueue;
|
|
324
|
+
exports.WokuClient = WokuClient;
|
|
325
|
+
exports.WokuConfigError = WokuConfigError;
|
|
326
|
+
exports.WokuError = WokuError;
|
|
327
|
+
exports.WokuNetworkError = WokuNetworkError;
|
|
328
|
+
exports.WokuQuarantineError = WokuQuarantineError;
|
|
329
|
+
exports.WokuSdk = WokuSdk;
|
|
330
|
+
exports.WokuValidationError = WokuValidationError;
|
|
331
|
+
exports.fetchHttpClient = fetchHttpClient;
|
|
332
|
+
exports.noopLogger = noopLogger;
|
|
333
|
+
//# sourceMappingURL=index.cjs.map
|
|
334
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters.ts","../src/errors.ts","../src/client.ts","../src/queue.ts","../src/sdk.ts"],"names":["body"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Injectable adapters. The SDK core never imports React Native or a\n * concrete HTTP/storage library directly — host apps provide these so\n * the core stays unit-testable and platform-agnostic.\n */\n\n/** Minimal HTTP response the SDK needs. */\nexport interface HttpResponse {\n status: number;\n ok: boolean;\n json: () => Promise<unknown>;\n headers: { get: (name: string) => string | null };\n}\n\nexport interface HttpRequest {\n method: string;\n url: string;\n headers: Record<string, string>;\n body?: string;\n}\n\n/** HTTP transport. Defaults to global `fetch`. */\nexport interface HttpClient {\n request: (req: HttpRequest) => Promise<HttpResponse>;\n}\n\n/**\n * Key/value persistence for the offline queue. React Native apps pass an\n * MMKV or AsyncStorage-backed adapter; tests use the in-memory one.\n */\nexport interface Storage {\n getItem: (key: string) => Promise<string | null> | string | null;\n setItem: (key: string, value: string) => Promise<void> | void;\n removeItem: (key: string) => Promise<void> | void;\n}\n\nexport interface Logger {\n debug: (msg: string, meta?: unknown) => void;\n warn: (msg: string, meta?: unknown) => void;\n error: (msg: string, meta?: unknown) => void;\n}\n\n/** Default HTTP client backed by the global `fetch`. */\nexport const fetchHttpClient: HttpClient = {\n async request(req) {\n const res = await fetch(req.url, {\n method: req.method,\n headers: req.headers,\n body: req.body,\n });\n return {\n status: res.status,\n ok: res.ok,\n json: () => res.json(),\n headers: { get: (name) => res.headers.get(name) },\n };\n },\n};\n\n/** In-memory storage. Default for tests and ephemeral usage. */\nexport class InMemoryStorage implements Storage {\n private readonly map = new Map<string, string>();\n getItem(key: string): string | null {\n return this.map.has(key) ? (this.map.get(key) as string) : null;\n }\n setItem(key: string, value: string): void {\n this.map.set(key, value);\n }\n removeItem(key: string): void {\n this.map.delete(key);\n }\n}\n\n/** No-op logger (default). */\nexport const noopLogger: Logger = {\n debug: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n};\n","/** Base error for all SDK failures. */\nexport class WokuError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'WokuError';\n }\n}\n\n/** Thrown when the SDK is misconfigured (missing companyId, etc.). */\nexport class WokuConfigError extends WokuError {\n constructor(message: string) {\n super(message, 'config_error');\n this.name = 'WokuConfigError';\n }\n}\n\n/** Validation failure on a capture input (bad rating/score, etc.). */\nexport class WokuValidationError extends WokuError {\n constructor(message: string) {\n super(message, 'validation_error');\n this.name = 'WokuValidationError';\n }\n}\n\n/**\n * The backend rejected the submission because the respondent is in\n * quarantine (HTTP 429 with a QUARANTINE_BLOCKED code). Carries the\n * retry-after hint in seconds when the server provides one.\n */\nexport class WokuQuarantineError extends WokuError {\n constructor(\n message: string,\n public readonly retryAfterSeconds?: number,\n ) {\n super(message, 'quarantine_blocked');\n this.name = 'WokuQuarantineError';\n }\n}\n\n/** Network/transport failure; the submission should be retried later. */\nexport class WokuNetworkError extends WokuError {\n constructor(message: string) {\n super(message, 'network_error');\n this.name = 'WokuNetworkError';\n }\n}\n","import {\n fetchHttpClient,\n noopLogger,\n type HttpClient,\n type Logger,\n} from './adapters';\nimport {\n WokuConfigError,\n WokuNetworkError,\n WokuQuarantineError,\n} from './errors';\nimport type { CaptureSubmission, SubmissionResult } from './types';\n\nexport interface WokuClientConfig {\n /** Base URL of the Woku API, e.g. `https://api.woku.app`. */\n apiUrl: string;\n /** Public SDK key issued per company for capture ingestion. */\n publicKey: string;\n /** Tenant the captures belong to. */\n companyId: string;\n /** HTTP transport. Defaults to the global `fetch`. */\n http?: HttpClient;\n logger?: Logger;\n /** Ingest path appended to `apiUrl`. Defaults to `/sdk/v1/captures`. */\n capturePath?: string;\n}\n\nconst DEFAULT_CAPTURE_PATH = '/sdk/v1/captures';\n\n/**\n * Thin, typed transport to the Woku capture ingest endpoint. Stateless:\n * it turns a normalized {@link CaptureSubmission} into an authenticated\n * POST and maps the response (incl. quarantine 429) to a result/error.\n */\nexport class WokuClient {\n private readonly http: HttpClient;\n private readonly logger: Logger;\n private readonly endpoint: string;\n\n constructor(private readonly config: WokuClientConfig) {\n if (!config.apiUrl) throw new WokuConfigError('apiUrl is required');\n if (!config.publicKey) throw new WokuConfigError('publicKey is required');\n if (!config.companyId) throw new WokuConfigError('companyId is required');\n this.http = config.http ?? fetchHttpClient;\n this.logger = config.logger ?? noopLogger;\n const base = config.apiUrl.replace(/\\/$/, '');\n const path = config.capturePath ?? DEFAULT_CAPTURE_PATH;\n this.endpoint = `${base}${path.startsWith('/') ? path : `/${path}`}`;\n }\n\n /**\n * Sends one submission. Resolves with a `sent` result on 2xx, throws\n * {@link WokuQuarantineError} on a quarantine 429, {@link WokuNetworkError}\n * on transport failure, and resolves with a `failed` result on other\n * non-2xx responses (so the caller can decide to re-queue).\n */\n async send(submission: CaptureSubmission): Promise<SubmissionResult> {\n let response;\n try {\n response = await this.http.request({\n method: 'POST',\n url: this.endpoint,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.publicKey}`,\n 'X-Woku-Company': this.config.companyId,\n 'X-Woku-Idempotency-Key': submission.id,\n },\n body: JSON.stringify(submission),\n });\n } catch (err) {\n throw new WokuNetworkError((err as Error).message);\n }\n\n if (response.ok) {\n const body = (await this.safeJson(response.json)) as {\n id?: string;\n } | null;\n return { id: submission.id, status: 'sent', remoteId: body?.id };\n }\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('Retry-After'));\n throw new WokuQuarantineError(\n 'Submission blocked by quarantine',\n Number.isFinite(retryAfter) ? retryAfter : undefined,\n );\n }\n\n const body = (await this.safeJson(response.json)) as {\n message?: string;\n } | null;\n const error = body?.message ?? `HTTP ${response.status}`;\n this.logger.warn(`woku capture rejected: ${error}`);\n return { id: submission.id, status: 'failed', error };\n }\n\n private async safeJson(\n json: () => Promise<unknown>,\n ): Promise<unknown | null> {\n try {\n return await json();\n } catch {\n return null;\n }\n }\n}\n","import { InMemoryStorage, type Storage } from './adapters';\nimport { WokuQuarantineError, WokuNetworkError } from './errors';\nimport type { CaptureSubmission, SubmissionResult } from './types';\n\ninterface QueuedItem {\n submission: CaptureSubmission;\n attempts: number;\n}\n\nexport interface OfflineQueueOptions {\n storage?: Storage;\n /** Storage key for the persisted queue. */\n storageKey?: string;\n /** Drop a submission after this many failed delivery attempts. */\n maxAttempts?: number;\n}\n\nexport interface FlushResult {\n sent: number;\n failed: number;\n remaining: number;\n /** Set when flushing stopped early due to quarantine. */\n quarantined?: boolean;\n}\n\nconst DEFAULT_KEY = 'woku.sdk.queue.v1';\nconst DEFAULT_MAX_ATTEMPTS = 8;\n\n/**\n * Durable, FIFO offline buffer for captures. Persists through an\n * injectable {@link Storage} so submissions survive app restarts, and\n * flushes them through a caller-provided `send` function. Pure and\n * deterministic: no timers, no platform APIs.\n *\n * Delivery semantics on flush:\n * - `sent` -> removed from the queue.\n * - quarantine -> flush stops (server is rate-limiting the\n * respondent); items are kept for the next attempt.\n * - network error -> attempt count bumped, item kept, flush continues.\n * - `failed` -> attempt count bumped; dropped once maxAttempts is\n * reached so a permanently-bad item can't wedge it.\n */\nexport class OfflineQueue {\n private readonly storage: Storage;\n private readonly key: string;\n private readonly maxAttempts: number;\n private items: QueuedItem[] | null = null;\n\n constructor(opts: OfflineQueueOptions = {}) {\n this.storage = opts.storage ?? new InMemoryStorage();\n this.key = opts.storageKey ?? DEFAULT_KEY;\n this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n }\n\n private async load(): Promise<QueuedItem[]> {\n if (this.items) return this.items;\n const raw = await this.storage.getItem(this.key);\n if (!raw) {\n this.items = [];\n return this.items;\n }\n try {\n const parsed = JSON.parse(raw) as QueuedItem[];\n this.items = Array.isArray(parsed) ? parsed : [];\n } catch {\n this.items = [];\n }\n return this.items;\n }\n\n private async persist(): Promise<void> {\n await this.storage.setItem(this.key, JSON.stringify(this.items ?? []));\n }\n\n /** Adds a submission to the tail of the queue (dedup by id). */\n async enqueue(submission: CaptureSubmission): Promise<void> {\n const items = await this.load();\n if (items.some((i) => i.submission.id === submission.id)) return;\n items.push({ submission, attempts: 0 });\n await this.persist();\n }\n\n async size(): Promise<number> {\n return (await this.load()).length;\n }\n\n /** Returns a copy of the pending submissions (oldest first). */\n async pending(): Promise<CaptureSubmission[]> {\n return (await this.load()).map((i) => i.submission);\n }\n\n /**\n * Attempts to deliver every queued submission through `send`, applying\n * the delivery semantics documented on the class. Persists once at the\n * end and returns a summary.\n */\n async flush(\n send: (submission: CaptureSubmission) => Promise<SubmissionResult>,\n ): Promise<FlushResult> {\n const items = await this.load();\n const survivors: QueuedItem[] = [];\n let sent = 0;\n let failed = 0;\n let quarantined = false;\n\n for (let index = 0; index < items.length; index++) {\n const item = items[index] as QueuedItem;\n if (quarantined) {\n survivors.push(item);\n continue;\n }\n try {\n const result = await send(item.submission);\n if (result.status === 'sent') {\n sent++;\n } else {\n item.attempts++;\n if (item.attempts >= this.maxAttempts) failed++;\n else survivors.push(item);\n }\n } catch (err) {\n if (err instanceof WokuQuarantineError) {\n quarantined = true;\n survivors.push(item);\n } else if (err instanceof WokuNetworkError) {\n item.attempts++;\n survivors.push(item);\n } else {\n item.attempts++;\n if (item.attempts >= this.maxAttempts) failed++;\n else survivors.push(item);\n }\n }\n }\n\n this.items = survivors;\n await this.persist();\n return { sent, failed, remaining: survivors.length, quarantined };\n }\n\n /** Clears the queue (e.g. on logout). */\n async clear(): Promise<void> {\n this.items = [];\n await this.storage.removeItem(this.key);\n }\n}\n","import { WokuClient, type WokuClientConfig } from './client';\nimport { OfflineQueue, type FlushResult } from './queue';\nimport { type Storage, type Logger, noopLogger } from './adapters';\nimport {\n WokuNetworkError,\n WokuQuarantineError,\n WokuValidationError,\n} from './errors';\nimport type {\n CaptureSubmission,\n NpsCaptureInput,\n SubmissionResult,\n WokuCaptureInput,\n} from './types';\n\nexport interface WokuSdkConfig extends WokuClientConfig {\n /** Storage adapter for the offline queue (MMKV/AsyncStorage in RN). */\n storage?: Storage;\n /** Drop a queued submission after this many failed attempts. */\n maxQueueAttempts?: number;\n}\n\nfunction generateId(): string {\n const g = globalThis as { crypto?: { randomUUID?: () => string } };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n // Fallback: timestamp + random, good enough as an idempotency key.\n return `wk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction assertInt(\n value: number,\n min: number,\n max: number,\n field: string,\n): void {\n if (!Number.isInteger(value) || value < min || value > max) {\n throw new WokuValidationError(\n `${field} must be an integer between ${min} and ${max}`,\n );\n }\n}\n\n/**\n * Top-level Woku SDK. Wires the typed {@link WokuClient} to a durable\n * {@link OfflineQueue}: a capture is attempted immediately and, if the\n * device is offline or the server is unreachable, transparently queued\n * and retried on the next {@link WokuSdk.flush}.\n *\n * @example\n * const woku = new WokuSdk({\n * apiUrl: 'https://api.woku.app',\n * publicKey: 'pk_live_...',\n * companyId: 'company_123',\n * storage: mmkvStorageAdapter,\n * });\n * await woku.captureNps({ npsId: 'nps_1', score: 9, comment: 'Great!' });\n */\nexport class WokuSdk {\n private readonly client: WokuClient;\n private readonly queue: OfflineQueue;\n private readonly logger: Logger;\n private readonly companyId: string;\n\n constructor(config: WokuSdkConfig) {\n this.client = new WokuClient(config);\n this.queue = new OfflineQueue({\n storage: config.storage,\n maxAttempts: config.maxQueueAttempts,\n });\n this.logger = config.logger ?? noopLogger;\n this.companyId = config.companyId;\n }\n\n /** Captures a Woku rating (1..5) with optional text/audio comment. */\n async captureWoku(input: WokuCaptureInput): Promise<SubmissionResult> {\n assertInt(input.rating, 1, 5, 'rating');\n const submission: CaptureSubmission = {\n id: generateId(),\n kind: 'woku',\n companyId: this.companyId,\n targetId: input.wokuId,\n rating: input.rating,\n comment: input.comment,\n audio: input.audio,\n respondent: input.respondent,\n metadata: input.metadata,\n createdAt: Date.now(),\n };\n return this.deliver(submission);\n }\n\n /** Captures an NPS score (0..10) with optional text/audio review. */\n async captureNps(input: NpsCaptureInput): Promise<SubmissionResult> {\n assertInt(input.score, 0, 10, 'score');\n const submission: CaptureSubmission = {\n id: generateId(),\n kind: 'nps',\n companyId: this.companyId,\n targetId: input.npsId,\n score: input.score,\n comment: input.comment,\n audio: input.audio,\n respondent: input.respondent,\n metadata: input.metadata,\n createdAt: Date.now(),\n };\n return this.deliver(submission);\n }\n\n /** Retries every queued submission. Safe to call on app foreground or\n * when connectivity is regained. */\n flush(): Promise<FlushResult> {\n return this.queue.flush((s) => this.client.send(s));\n }\n\n /** Number of submissions waiting to be delivered. */\n pendingCount(): Promise<number> {\n return this.queue.size();\n }\n\n /** Clears the offline queue (e.g. on user logout). */\n clearQueue(): Promise<void> {\n return this.queue.clear();\n }\n\n /**\n * Tries to send immediately; on quarantine or network failure the\n * submission is queued for a later flush rather than thrown away.\n */\n private async deliver(\n submission: CaptureSubmission,\n ): Promise<SubmissionResult> {\n try {\n const result = await this.client.send(submission);\n if (result.status === 'sent') return result;\n // Server-rejected but transient enough to retry: queue it.\n await this.queue.enqueue(submission);\n return { ...result, status: 'queued' };\n } catch (err) {\n await this.queue.enqueue(submission);\n if (err instanceof WokuQuarantineError) {\n return { id: submission.id, status: 'quarantined', error: err.message };\n }\n if (err instanceof WokuNetworkError) {\n this.logger.debug('capture queued (offline)', { id: submission.id });\n return { id: submission.id, status: 'queued' };\n }\n throw err;\n }\n }\n}\n"]}
|