@logbrew/react-native 0.1.1 → 0.1.3
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/LogBrewReactNative.podspec +22 -0
- package/README.md +24 -0
- package/android/build.gradle +42 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +636 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +225 -0
- package/android/src/main/java/co/logbrew/reactnative/LogBrewReactNativePackage.java +20 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +39 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +41 -0
- package/fatal-replay.cjs +531 -0
- package/global-errors.cjs +414 -0
- package/global-errors.d.cts +131 -0
- package/global-errors.d.ts +131 -0
- package/global-errors.js +7 -0
- package/global-errors.native.js +39 -0
- package/index.native.d.ts +2 -0
- package/index.native.js +3 -0
- package/ios/LBRNFatalRecordStore.h +25 -0
- package/ios/LBRNFatalRecordStore.m +542 -0
- package/ios/LBRNFatalStoreModule.h +4 -0
- package/ios/LBRNFatalStoreModule.mm +147 -0
- package/package.json +47 -3
- package/react-native.config.js +11 -0
- package/src/NativeLogBrewFatalStore.ts +11 -0
package/fatal-replay.cjs
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const FATAL_RECORD_SCHEMA_VERSION = 1;
|
|
4
|
+
const MAX_ADMITTED_FATAL_IDS = 256;
|
|
5
|
+
const MAX_FATAL_FILENAME_BYTES = 512;
|
|
6
|
+
const MAX_FATAL_STACK_BYTES = 16 * 1024;
|
|
7
|
+
const MAX_FATAL_STACK_FRAMES = 24;
|
|
8
|
+
|
|
9
|
+
const admittedFatalIdsByClient = new WeakMap();
|
|
10
|
+
let nextFatalSequence = 0;
|
|
11
|
+
|
|
12
|
+
function createFatalController({
|
|
13
|
+
client,
|
|
14
|
+
eventMessage,
|
|
15
|
+
fatalStore,
|
|
16
|
+
issue,
|
|
17
|
+
onDiagnostic,
|
|
18
|
+
sanitizers
|
|
19
|
+
}) {
|
|
20
|
+
const methods = fatalStoreMethods(fatalStore);
|
|
21
|
+
const state = {
|
|
22
|
+
acknowledgedRecords: 0,
|
|
23
|
+
available: methods !== undefined,
|
|
24
|
+
corruptRecords: 0,
|
|
25
|
+
droppedRecords: 0,
|
|
26
|
+
lastOutcome: methods ? "idle" : "unavailable",
|
|
27
|
+
replayedRecords: 0,
|
|
28
|
+
storedRecords: 0
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
available: state.available,
|
|
33
|
+
discard() {
|
|
34
|
+
return discardPendingFatal(fatalStore, methods, state, onDiagnostic);
|
|
35
|
+
},
|
|
36
|
+
health() {
|
|
37
|
+
return fatalHealthSnapshot(state);
|
|
38
|
+
},
|
|
39
|
+
replay() {
|
|
40
|
+
replayPendingFatal({
|
|
41
|
+
client,
|
|
42
|
+
eventMessage,
|
|
43
|
+
fatalStore,
|
|
44
|
+
issue,
|
|
45
|
+
methods,
|
|
46
|
+
onDiagnostic,
|
|
47
|
+
sanitizers,
|
|
48
|
+
state
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
store(error) {
|
|
52
|
+
return storeFatalError({
|
|
53
|
+
error,
|
|
54
|
+
fatalStore,
|
|
55
|
+
methods,
|
|
56
|
+
onDiagnostic,
|
|
57
|
+
sanitizers,
|
|
58
|
+
state
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fatalStoreMethods(fatalStore) {
|
|
65
|
+
if (!isObjectLike(fatalStore)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const methods = {
|
|
69
|
+
acknowledge: safeFunction(fatalStore, "acknowledgeFatalRecord"),
|
|
70
|
+
discard: safeFunction(fatalStore, "discardFatalRecord"),
|
|
71
|
+
read: safeFunction(fatalStore, "readFatalRecord"),
|
|
72
|
+
write: safeFunction(fatalStore, "writeFatalRecord")
|
|
73
|
+
};
|
|
74
|
+
return Object.values(methods).every((method) => typeof method === "function")
|
|
75
|
+
? methods
|
|
76
|
+
: undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function discardPendingFatal(fatalStore, methods, state, onDiagnostic) {
|
|
80
|
+
if (!methods) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = methods.discard.call(fatalStore);
|
|
86
|
+
} catch {
|
|
87
|
+
state.lastOutcome = "discard_failed";
|
|
88
|
+
emitDiagnostic(onDiagnostic, "fatal_discard_failed");
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (resultStatus(result) !== "discarded") {
|
|
92
|
+
state.lastOutcome = resultStatus(result) === "empty" ? "empty" : "discard_failed";
|
|
93
|
+
if (state.lastOutcome === "discard_failed") {
|
|
94
|
+
emitDiagnostic(onDiagnostic, "fatal_discard_failed");
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
state.lastOutcome = "discarded";
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function storeFatalError({
|
|
103
|
+
error,
|
|
104
|
+
fatalStore,
|
|
105
|
+
methods,
|
|
106
|
+
onDiagnostic,
|
|
107
|
+
sanitizers,
|
|
108
|
+
state
|
|
109
|
+
}) {
|
|
110
|
+
if (!methods) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = methods.write.call(fatalStore, createFatalRecord(error, sanitizers));
|
|
116
|
+
} catch {
|
|
117
|
+
result = undefined;
|
|
118
|
+
}
|
|
119
|
+
const status = resultStatus(result);
|
|
120
|
+
if (status === "stored" || status === "stored_after_corruption") {
|
|
121
|
+
state.storedRecords = incrementBounded(state.storedRecords);
|
|
122
|
+
state.corruptRecords = boundedResultCounter(result, "corruptRecords", state.corruptRecords);
|
|
123
|
+
state.lastOutcome = status;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (status === "dropped_pending") {
|
|
127
|
+
state.droppedRecords = boundedResultCounter(
|
|
128
|
+
result,
|
|
129
|
+
"droppedRecords",
|
|
130
|
+
incrementBounded(state.droppedRecords)
|
|
131
|
+
);
|
|
132
|
+
state.lastOutcome = status;
|
|
133
|
+
emitDiagnostic(onDiagnostic, "fatal_record_dropped");
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
state.lastOutcome = "storage_error";
|
|
137
|
+
emitDiagnostic(onDiagnostic, "fatal_store_failed");
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function replayPendingFatal({
|
|
142
|
+
client,
|
|
143
|
+
eventMessage,
|
|
144
|
+
fatalStore,
|
|
145
|
+
issue,
|
|
146
|
+
methods,
|
|
147
|
+
onDiagnostic,
|
|
148
|
+
sanitizers,
|
|
149
|
+
state
|
|
150
|
+
}) {
|
|
151
|
+
if (!methods) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let result;
|
|
155
|
+
try {
|
|
156
|
+
result = methods.read.call(fatalStore);
|
|
157
|
+
} catch {
|
|
158
|
+
result = undefined;
|
|
159
|
+
}
|
|
160
|
+
const status = resultStatus(result);
|
|
161
|
+
if (status === "empty") {
|
|
162
|
+
state.lastOutcome = "empty";
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (status === "corrupt_discarded") {
|
|
166
|
+
state.corruptRecords = boundedResultCounter(
|
|
167
|
+
result,
|
|
168
|
+
"corruptRecords",
|
|
169
|
+
incrementBounded(state.corruptRecords)
|
|
170
|
+
);
|
|
171
|
+
state.lastOutcome = status;
|
|
172
|
+
emitDiagnostic(onDiagnostic, "fatal_corrupt_record_discarded");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const record = status === "pending"
|
|
176
|
+
? validatedFatalRecord(safeReadProperty(result, "record"), sanitizers)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (!record) {
|
|
179
|
+
state.lastOutcome = "replay_failed";
|
|
180
|
+
emitDiagnostic(onDiagnostic, "fatal_replay_failed");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
state.corruptRecords = Math.max(state.corruptRecords, record.corruptRecords);
|
|
185
|
+
state.droppedRecords = Math.max(state.droppedRecords, record.droppedRecords);
|
|
186
|
+
if (wasAdmittedInRuntime(client, record.id)) {
|
|
187
|
+
acknowledgeFatalRecord(fatalStore, methods, record.id, state, onDiagnostic);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const before = admissionSnapshot(client);
|
|
192
|
+
try {
|
|
193
|
+
issue.call(client, record.id, record.timestamp, fatalEventAttributes(record, eventMessage));
|
|
194
|
+
} catch {
|
|
195
|
+
state.lastOutcome = "replay_failed";
|
|
196
|
+
emitDiagnostic(onDiagnostic, "fatal_replay_failed");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const after = admissionSnapshot(client);
|
|
200
|
+
if (!wasAdmitted(before, after)) {
|
|
201
|
+
state.lastOutcome = "replay_not_admitted";
|
|
202
|
+
emitDiagnostic(onDiagnostic, "fatal_replay_not_admitted");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
rememberAdmittedFatalId(client, record.id);
|
|
207
|
+
state.replayedRecords = incrementBounded(state.replayedRecords);
|
|
208
|
+
acknowledgeFatalRecord(fatalStore, methods, record.id, state, onDiagnostic);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function acknowledgeFatalRecord(fatalStore, methods, recordId, state, onDiagnostic) {
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = methods.acknowledge.call(fatalStore, recordId);
|
|
215
|
+
} catch {
|
|
216
|
+
result = undefined;
|
|
217
|
+
}
|
|
218
|
+
if (resultStatus(result) === "acknowledged"
|
|
219
|
+
&& safeReadProperty(result, "recordId") === recordId) {
|
|
220
|
+
state.acknowledgedRecords = incrementBounded(state.acknowledgedRecords);
|
|
221
|
+
state.lastOutcome = "acknowledged";
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
state.lastOutcome = "acknowledge_failed";
|
|
225
|
+
emitDiagnostic(onDiagnostic, "fatal_acknowledge_failed");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function admissionSnapshot(client) {
|
|
229
|
+
const pendingEvents = safeFunction(client, "pendingEvents");
|
|
230
|
+
const droppedEvents = safeFunction(client, "droppedEvents");
|
|
231
|
+
if (!pendingEvents || !droppedEvents) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const pending = pendingEvents.call(client);
|
|
236
|
+
const dropped = droppedEvents.call(client);
|
|
237
|
+
return isBoundedCounter(pending) && isBoundedCounter(dropped)
|
|
238
|
+
? { dropped, pending }
|
|
239
|
+
: undefined;
|
|
240
|
+
} catch {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function wasAdmitted(before, after) {
|
|
246
|
+
return before !== undefined
|
|
247
|
+
&& after !== undefined
|
|
248
|
+
&& after.pending === before.pending + 1
|
|
249
|
+
&& after.dropped === before.dropped;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function wasAdmittedInRuntime(client, recordId) {
|
|
253
|
+
return isObjectLike(client) && admittedFatalIdsByClient.get(client)?.has(recordId) === true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function rememberAdmittedFatalId(client, recordId) {
|
|
257
|
+
if (!isObjectLike(client)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let admittedIds = admittedFatalIdsByClient.get(client);
|
|
261
|
+
if (!admittedIds) {
|
|
262
|
+
admittedIds = new Set();
|
|
263
|
+
admittedFatalIdsByClient.set(client, admittedIds);
|
|
264
|
+
}
|
|
265
|
+
if (admittedIds.has(recordId)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
admittedIds.add(recordId);
|
|
269
|
+
if (admittedIds.size > MAX_ADMITTED_FATAL_IDS) {
|
|
270
|
+
admittedIds.delete(admittedIds.values().next().value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createFatalRecord(error, sanitizers) {
|
|
275
|
+
return Object.freeze({
|
|
276
|
+
corruptRecords: 0,
|
|
277
|
+
droppedRecords: 0,
|
|
278
|
+
errorName: sanitizers.errorName(error),
|
|
279
|
+
id: nextFatalEventId(),
|
|
280
|
+
schemaVersion: FATAL_RECORD_SCHEMA_VERSION,
|
|
281
|
+
stackFrames: fatalStackFrames(error, sanitizers.stackFrame),
|
|
282
|
+
timestamp: new Date().toISOString()
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fatalStackFrames(error, stackFrame) {
|
|
287
|
+
const stack = safeReadProperty(error, "stack");
|
|
288
|
+
const candidate = typeof stack === "string"
|
|
289
|
+
? stack.slice(0, MAX_FATAL_STACK_BYTES)
|
|
290
|
+
: "";
|
|
291
|
+
const frames = [];
|
|
292
|
+
for (const line of candidate.split(/\r?\n/u)) {
|
|
293
|
+
const frame = stackFrame(line);
|
|
294
|
+
const filename = frame ? normalizedStoredFatalFilename(frame.filename) : undefined;
|
|
295
|
+
if (filename && validFatalFilename(filename)) {
|
|
296
|
+
frames.push(Object.freeze({ ...frame, filename }));
|
|
297
|
+
if (frames.length === MAX_FATAL_STACK_FRAMES) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze(frames);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function fatalEventAttributes(record, eventMessage) {
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
level: "fatal",
|
|
308
|
+
message: eventMessage,
|
|
309
|
+
metadata: Object.freeze({
|
|
310
|
+
automatic: true,
|
|
311
|
+
fatal: true,
|
|
312
|
+
handled: false,
|
|
313
|
+
mechanism: "react_native_error_utils",
|
|
314
|
+
nativeCorruptRecords: record.corruptRecords,
|
|
315
|
+
nativeDroppedRecords: record.droppedRecords,
|
|
316
|
+
replayed: true,
|
|
317
|
+
source: "react-native.global_error"
|
|
318
|
+
}),
|
|
319
|
+
stackFrames: record.stackFrames.map((frame) => Object.freeze({ ...frame })),
|
|
320
|
+
title: eventMessage
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validatedFatalRecord(value, sanitizers) {
|
|
325
|
+
try {
|
|
326
|
+
if (!isPlainObject(value)
|
|
327
|
+
|| !hasExactKeys(value, [
|
|
328
|
+
"corruptRecords",
|
|
329
|
+
"droppedRecords",
|
|
330
|
+
"errorName",
|
|
331
|
+
"id",
|
|
332
|
+
"schemaVersion",
|
|
333
|
+
"stackFrames",
|
|
334
|
+
"timestamp"
|
|
335
|
+
])
|
|
336
|
+
|| value.schemaVersion !== FATAL_RECORD_SCHEMA_VERSION
|
|
337
|
+
|| typeof value.id !== "string"
|
|
338
|
+
|| !/^evt_rn_fatal_[a-z0-9]+(?:_[a-z0-9]+)*$/u.test(value.id)
|
|
339
|
+
|| !sanitizers.isErrorName(value.errorName)
|
|
340
|
+
|| !isBoundedCounter(value.corruptRecords)
|
|
341
|
+
|| !isBoundedCounter(value.droppedRecords)
|
|
342
|
+
|| !validFatalTimestamp(value.timestamp)
|
|
343
|
+
|| !Array.isArray(value.stackFrames)
|
|
344
|
+
|| value.stackFrames.length > MAX_FATAL_STACK_FRAMES) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const frames = value.stackFrames.map(validatedFatalFrame);
|
|
348
|
+
if (frames.some((frame) => frame === undefined)) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
corruptRecords: value.corruptRecords,
|
|
353
|
+
droppedRecords: value.droppedRecords,
|
|
354
|
+
errorName: value.errorName,
|
|
355
|
+
id: value.id,
|
|
356
|
+
schemaVersion: FATAL_RECORD_SCHEMA_VERSION,
|
|
357
|
+
stackFrames: frames,
|
|
358
|
+
timestamp: value.timestamp
|
|
359
|
+
};
|
|
360
|
+
} catch {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function validatedFatalFrame(value) {
|
|
366
|
+
try {
|
|
367
|
+
if (!isPlainObject(value)
|
|
368
|
+
|| !hasExactKeys(value, ["column", "filename", "line"])
|
|
369
|
+
|| !validFatalFilename(value.filename)
|
|
370
|
+
|| positiveInteger(value.line) === undefined
|
|
371
|
+
|| positiveInteger(value.column) === undefined) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
column: value.column,
|
|
376
|
+
filename: value.filename,
|
|
377
|
+
line: value.line
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validFatalFilename(value) {
|
|
385
|
+
return typeof value === "string"
|
|
386
|
+
&& value.length > 0
|
|
387
|
+
&& utf8ByteLength(value) <= MAX_FATAL_FILENAME_BYTES
|
|
388
|
+
&& !hasControlCharacter(value)
|
|
389
|
+
&& !value.startsWith("/")
|
|
390
|
+
&& !/^[A-Za-z]:/u.test(value)
|
|
391
|
+
&& !value.includes("\\")
|
|
392
|
+
&& !value.includes("://")
|
|
393
|
+
&& !value.includes("?")
|
|
394
|
+
&& !value.includes("#")
|
|
395
|
+
&& !value.split("/").includes("..");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizedStoredFatalFilename(value) {
|
|
399
|
+
return typeof value === "string"
|
|
400
|
+
&& value.startsWith("/")
|
|
401
|
+
&& value.length > 1
|
|
402
|
+
&& value.indexOf("/", 1) === -1
|
|
403
|
+
? value.slice(1)
|
|
404
|
+
: value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function validFatalTimestamp(value) {
|
|
408
|
+
return typeof value === "string"
|
|
409
|
+
&& value.length >= 20
|
|
410
|
+
&& value.length <= 35
|
|
411
|
+
&& /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z$/u.test(value);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resultStatus(result) {
|
|
415
|
+
try {
|
|
416
|
+
return isPlainObject(result) && typeof result.status === "string"
|
|
417
|
+
? result.status
|
|
418
|
+
: undefined;
|
|
419
|
+
} catch {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function boundedResultCounter(result, property, fallback) {
|
|
425
|
+
const value = isPlainObject(result) ? safeReadProperty(result, property) : undefined;
|
|
426
|
+
return isBoundedCounter(value) ? value : fallback;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function hasExactKeys(value, expected) {
|
|
430
|
+
try {
|
|
431
|
+
const keys = Object.keys(value).sort();
|
|
432
|
+
return keys.length === expected.length
|
|
433
|
+
&& keys.every((key, index) => key === expected[index]);
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isBoundedCounter(value) {
|
|
440
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isPlainObject(value) {
|
|
444
|
+
if (value === null || typeof value !== "object") {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const prototype = Object.getPrototypeOf(value);
|
|
449
|
+
return prototype === Object.prototype || prototype === null;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function safeReadProperty(value, property) {
|
|
456
|
+
if (!isObjectLike(value)) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
return value[property];
|
|
461
|
+
} catch {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function safeFunction(value, property) {
|
|
467
|
+
const candidate = safeReadProperty(value, property);
|
|
468
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function hasControlCharacter(value) {
|
|
472
|
+
return Array.from(value).some((character) => {
|
|
473
|
+
const code = character.codePointAt(0);
|
|
474
|
+
return code !== undefined && (code <= 31 || code === 127);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function positiveInteger(value) {
|
|
479
|
+
if (!/^[1-9][0-9]*$/u.test(String(value))) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
const number = Number(value);
|
|
483
|
+
return Number.isSafeInteger(number) && number <= 2147483647 ? number : undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function nextFatalEventId() {
|
|
487
|
+
nextFatalSequence = incrementBounded(nextFatalSequence);
|
|
488
|
+
return `evt_rn_fatal_${Date.now().toString(36)}_${nextFatalSequence.toString(36)}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function incrementBounded(value) {
|
|
492
|
+
return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function emitDiagnostic(onDiagnostic, code) {
|
|
496
|
+
if (typeof onDiagnostic !== "function") {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
onDiagnostic(Object.freeze({ code }));
|
|
501
|
+
} catch {
|
|
502
|
+
// Diagnostics must never interfere with application error handling.
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function utf8ByteLength(value) {
|
|
507
|
+
let bytes = 0;
|
|
508
|
+
for (const character of value) {
|
|
509
|
+
const code = character.codePointAt(0);
|
|
510
|
+
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
511
|
+
}
|
|
512
|
+
return bytes;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function fatalHealthSnapshot(state) {
|
|
516
|
+
return Object.freeze({
|
|
517
|
+
acknowledgedRecords: state.acknowledgedRecords,
|
|
518
|
+
available: state.available,
|
|
519
|
+
corruptRecords: state.corruptRecords,
|
|
520
|
+
droppedRecords: state.droppedRecords,
|
|
521
|
+
lastOutcome: state.lastOutcome,
|
|
522
|
+
replayedRecords: state.replayedRecords,
|
|
523
|
+
storedRecords: state.storedRecords
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isObjectLike(value) {
|
|
528
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = { createFatalController };
|