@logbrew/react-native 0.1.23 → 0.1.25

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.
Files changed (35) hide show
  1. package/README.md +62 -5
  2. package/android/CMakeLists.txt +7 -0
  3. package/android/build.gradle +12 -0
  4. package/android/src/main/cpp/android_diagnostics.cpp +363 -0
  5. package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
  6. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +368 -0
  7. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
  8. package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
  9. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
  10. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +111 -156
  11. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
  12. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
  13. package/android-native-diagnostics.d.ts +29 -0
  14. package/android-native-diagnostics.js +176 -0
  15. package/fatal-replay.cjs +35 -3
  16. package/global-errors.d.cts +3 -3
  17. package/global-errors.d.ts +3 -3
  18. package/global-errors.native.js +1 -12
  19. package/index.cjs +1 -1
  20. package/index.native.d.ts +4 -0
  21. package/index.native.js +21 -3
  22. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
  28. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
  29. package/ios/LBRNFatalStoreModule.mm +15 -86
  30. package/package.json +18 -6
  31. package/persistent-delivery.native.js +4 -3
  32. package/src/NativeLogBrewFatalStore.ts +5 -4
  33. package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
  34. package/ios/LBRNFatalRecordStore.h +0 -25
  35. package/ios/LBRNFatalRecordStore.m +0 -542
@@ -1,623 +0,0 @@
1
- package co.logbrew.reactnative;
2
-
3
- import java.io.ByteArrayInputStream;
4
- import java.io.ByteArrayOutputStream;
5
- import java.io.DataInputStream;
6
- import java.io.DataOutputStream;
7
- import java.io.EOFException;
8
- import java.io.File;
9
- import java.io.FileInputStream;
10
- import java.io.FileOutputStream;
11
- import java.io.IOException;
12
- import java.nio.charset.StandardCharsets;
13
- import java.util.ArrayList;
14
- import java.util.Arrays;
15
- import java.util.Collections;
16
- import java.util.HashSet;
17
- import java.util.List;
18
- import java.util.Set;
19
- import java.util.regex.Pattern;
20
- import java.util.zip.CRC32;
21
-
22
- final class FatalRecordStore {
23
- static final String RECORD_FILE_NAME = "fatal-js-v1.record";
24
- static final String TEMP_FILE_NAME = "fatal-js-v1.tmp";
25
-
26
- private static final int MAGIC = 0x4c425246;
27
- private static final int FILE_FORMAT_VERSION = 1;
28
- private static final int MAX_RECORD_BYTES = 16 * 1024;
29
- private static final int MAX_FRAMES = 24;
30
- private static final int MAX_FILENAME_BYTES = 512;
31
- private static final int MAX_ID_BYTES = 96;
32
- private static final int MAX_TIMESTAMP_BYTES = 35;
33
- private static final Pattern ID_PATTERN =
34
- Pattern.compile("^evt_rn_fatal_[a-z0-9]+(?:_[a-z0-9]+)*$");
35
- private static final Pattern TIMESTAMP_PATTERN =
36
- Pattern.compile(
37
- "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{1,9})?Z$");
38
- private static final Set<String> ERROR_NAMES =
39
- Collections.unmodifiableSet(
40
- new HashSet<>(
41
- Arrays.asList(
42
- "Error",
43
- "EvalError",
44
- "RangeError",
45
- "ReferenceError",
46
- "SyntaxError",
47
- "TypeError",
48
- "URIError")));
49
- private static final ParentDirectorySync UNSUPPORTED_PARENT_DIRECTORY_SYNC =
50
- directory -> ParentDirectorySyncResult.UNSUPPORTED;
51
-
52
- private final File directory;
53
- private final File recordFile;
54
- private final File temporaryFile;
55
- private final ParentDirectorySync parentDirectorySync;
56
-
57
- FatalRecordStore(File directory) {
58
- this(directory, UNSUPPORTED_PARENT_DIRECTORY_SYNC);
59
- }
60
-
61
- FatalRecordStore(File directory, ParentDirectorySync parentDirectorySync) {
62
- this.directory = resolveCanonicalParent(directory);
63
- this.recordFile = new File(this.directory, RECORD_FILE_NAME);
64
- this.temporaryFile = new File(this.directory, TEMP_FILE_NAME);
65
- this.parentDirectorySync = parentDirectorySync;
66
- }
67
-
68
- synchronized Result write(Record incoming) {
69
- Record validated = validateRecord(incoming, true);
70
- if (validated == null) {
71
- return Result.status("invalid_record");
72
- }
73
- if (!prepareDirectory()) {
74
- return Result.status("storage_error");
75
- }
76
-
77
- Result existing = readPrepared();
78
- if ("storage_error".equals(existing.status)) {
79
- return existing;
80
- }
81
- if ("pending".equals(existing.status)) {
82
- int dropped =
83
- existing.record.droppedRecords == Integer.MAX_VALUE
84
- ? Integer.MAX_VALUE
85
- : existing.record.droppedRecords + 1;
86
- Record preserved =
87
- existing.record.withCounters(dropped, existing.record.corruptRecords);
88
- if (!atomicWrite(preserved)) {
89
- return Result.status("storage_error");
90
- }
91
- return new Result(
92
- "dropped_pending", null, preserved.id, dropped, preserved.corruptRecords);
93
- }
94
-
95
- boolean recoveredCorruption = "corrupt_discarded".equals(existing.status);
96
- Record stored = validated.withCounters(0, recoveredCorruption ? 1 : 0);
97
- if (!atomicWrite(stored)) {
98
- return Result.status("storage_error");
99
- }
100
- return new Result(
101
- recoveredCorruption ? "stored_after_corruption" : "stored",
102
- null,
103
- stored.id,
104
- stored.droppedRecords,
105
- stored.corruptRecords);
106
- }
107
-
108
- synchronized Result read() {
109
- if (!prepareDirectory()) {
110
- return Result.status("storage_error");
111
- }
112
- return readPrepared();
113
- }
114
-
115
- synchronized Result acknowledge(String recordId) {
116
- if (!validIdentifier(recordId) || !prepareDirectory()) {
117
- return Result.status(validIdentifier(recordId) ? "storage_error" : "id_mismatch");
118
- }
119
- Result existing = readPrepared();
120
- if (!"pending".equals(existing.status)) {
121
- return existing;
122
- }
123
- if (!existing.record.id.equals(recordId)) {
124
- return new Result(
125
- "id_mismatch",
126
- null,
127
- existing.record.id,
128
- existing.record.droppedRecords,
129
- existing.record.corruptRecords);
130
- }
131
- if (!deleteRegularFile(recordFile) || !syncParentDirectory()) {
132
- return Result.status("storage_error");
133
- }
134
- return new Result(
135
- "acknowledged",
136
- null,
137
- recordId,
138
- existing.record.droppedRecords,
139
- existing.record.corruptRecords);
140
- }
141
-
142
- synchronized Result discard() {
143
- if (!prepareDirectory()) {
144
- return Result.status("storage_error");
145
- }
146
- Result existing = readPrepared();
147
- if (!"pending".equals(existing.status)) {
148
- return existing;
149
- }
150
- if (!deleteRegularFile(recordFile) || !syncParentDirectory()) {
151
- return Result.status("storage_error");
152
- }
153
- return new Result(
154
- "discarded",
155
- null,
156
- existing.record.id,
157
- existing.record.droppedRecords,
158
- existing.record.corruptRecords);
159
- }
160
-
161
- private Result readPrepared() {
162
- if (!removeStaleTemporaryFile()) {
163
- return Result.status("storage_error");
164
- }
165
- if (!recordFile.exists()) {
166
- return Result.status("empty");
167
- }
168
- if (!isSafeRegularFile(recordFile)) {
169
- return Result.status("storage_error");
170
- }
171
- if (recordFile.length() <= 0 || recordFile.length() > MAX_RECORD_BYTES) {
172
- return discardCorruptRecord();
173
- }
174
-
175
- byte[] bytes;
176
- try {
177
- bytes = readBounded(recordFile);
178
- } catch (IOException error) {
179
- return Result.status("storage_error");
180
- }
181
- if (bytes == null) {
182
- return discardCorruptRecord();
183
- }
184
- Record record = decode(bytes);
185
- if (record == null) {
186
- return discardCorruptRecord();
187
- }
188
- return new Result(
189
- "pending", record, record.id, record.droppedRecords, record.corruptRecords);
190
- }
191
-
192
- private Result discardCorruptRecord() {
193
- if (!deleteRegularFile(recordFile) || !syncParentDirectory()) {
194
- return Result.status("storage_error");
195
- }
196
- return new Result("corrupt_discarded", null, null, 0, 1);
197
- }
198
-
199
- private boolean prepareDirectory() {
200
- try {
201
- if (directory.exists()) {
202
- if (!directory.isDirectory()) {
203
- return false;
204
- }
205
- if (!isCanonicalPath(directory)) {
206
- return false;
207
- }
208
- } else if (!directory.mkdir()) {
209
- return false;
210
- }
211
- if (!makeDirectoryPrivate(directory)) {
212
- return false;
213
- }
214
- if (!isCanonicalPath(directory)) {
215
- return false;
216
- }
217
- return true;
218
- } catch (IOException | SecurityException error) {
219
- return false;
220
- }
221
- }
222
-
223
- private boolean removeStaleTemporaryFile() {
224
- if (!temporaryFile.exists()) {
225
- return true;
226
- }
227
- return isSafeRegularFile(temporaryFile) && temporaryFile.delete();
228
- }
229
-
230
- private boolean atomicWrite(Record record) {
231
- byte[] bytes = encode(record);
232
- if (bytes == null || bytes.length == 0 || bytes.length > MAX_RECORD_BYTES) {
233
- return false;
234
- }
235
- if (!removeStaleTemporaryFile()) {
236
- return false;
237
- }
238
- if (recordFile.exists() && !isSafeRegularFile(recordFile)) {
239
- return false;
240
- }
241
-
242
- boolean wrote = false;
243
- try {
244
- if (!temporaryFile.createNewFile() || !isSafeRegularFile(temporaryFile)) {
245
- return false;
246
- }
247
- if (!makeFilePrivate(temporaryFile)) {
248
- return false;
249
- }
250
- try (FileOutputStream output = new FileOutputStream(temporaryFile, false)) {
251
- output.write(bytes);
252
- output.flush();
253
- output.getFD().sync();
254
- }
255
- if (!makeFilePrivate(temporaryFile)) {
256
- return false;
257
- }
258
- if (!temporaryFile.renameTo(recordFile)) {
259
- return false;
260
- }
261
- if (!makeFilePrivate(recordFile) || !syncParentDirectory()) {
262
- return false;
263
- }
264
- wrote = true;
265
- return true;
266
- } catch (IOException | SecurityException error) {
267
- return false;
268
- } finally {
269
- if (!wrote && temporaryFile.exists() && isSafeRegularFile(temporaryFile)) {
270
- temporaryFile.delete();
271
- }
272
- }
273
- }
274
-
275
- private byte[] readBounded(File file) throws IOException {
276
- try (FileInputStream input = new FileInputStream(file);
277
- ByteArrayOutputStream output = new ByteArrayOutputStream()) {
278
- byte[] buffer = new byte[1024];
279
- int total = 0;
280
- int count;
281
- while ((count = input.read(buffer)) != -1) {
282
- total += count;
283
- if (total > MAX_RECORD_BYTES) {
284
- return null;
285
- }
286
- output.write(buffer, 0, count);
287
- }
288
- return output.toByteArray();
289
- }
290
- }
291
-
292
- private byte[] encode(Record record) {
293
- try {
294
- ByteArrayOutputStream payloadBytes = new ByteArrayOutputStream();
295
- try (DataOutputStream payload = new DataOutputStream(payloadBytes)) {
296
- payload.writeInt(record.schemaVersion);
297
- writeString(payload, record.id);
298
- writeString(payload, record.timestamp);
299
- writeString(payload, record.errorName);
300
- payload.writeInt(record.stackFrames.size());
301
- for (Frame frame : record.stackFrames) {
302
- writeString(payload, frame.filename);
303
- payload.writeInt(frame.line);
304
- payload.writeInt(frame.column);
305
- }
306
- payload.writeInt(record.droppedRecords);
307
- payload.writeInt(record.corruptRecords);
308
- }
309
- byte[] rawPayload = payloadBytes.toByteArray();
310
- CRC32 checksum = new CRC32();
311
- checksum.update(rawPayload);
312
-
313
- ByteArrayOutputStream fileBytes = new ByteArrayOutputStream();
314
- try (DataOutputStream file = new DataOutputStream(fileBytes)) {
315
- file.writeInt(MAGIC);
316
- file.writeInt(FILE_FORMAT_VERSION);
317
- file.writeInt(rawPayload.length);
318
- file.write(rawPayload);
319
- file.writeLong(checksum.getValue());
320
- }
321
- return fileBytes.toByteArray();
322
- } catch (IOException impossible) {
323
- return null;
324
- }
325
- }
326
-
327
- private Record decode(byte[] bytes) {
328
- try (DataInputStream file = new DataInputStream(new ByteArrayInputStream(bytes))) {
329
- if (file.readInt() != MAGIC || file.readInt() != FILE_FORMAT_VERSION) {
330
- return null;
331
- }
332
- int payloadLength = file.readInt();
333
- if (payloadLength <= 0 || payloadLength > MAX_RECORD_BYTES - 20) {
334
- return null;
335
- }
336
- byte[] payload = new byte[payloadLength];
337
- file.readFully(payload);
338
- long expectedChecksum = file.readLong();
339
- if (file.read() != -1) {
340
- return null;
341
- }
342
- CRC32 checksum = new CRC32();
343
- checksum.update(payload);
344
- if (checksum.getValue() != expectedChecksum) {
345
- return null;
346
- }
347
-
348
- try (DataInputStream recordInput =
349
- new DataInputStream(new ByteArrayInputStream(payload))) {
350
- int schemaVersion = recordInput.readInt();
351
- String id = readString(recordInput, MAX_ID_BYTES);
352
- String timestamp = readString(recordInput, MAX_TIMESTAMP_BYTES);
353
- String errorName = readString(recordInput, 32);
354
- int frameCount = recordInput.readInt();
355
- if (frameCount < 0 || frameCount > MAX_FRAMES) {
356
- return null;
357
- }
358
- List<Frame> frames = new ArrayList<>(frameCount);
359
- for (int index = 0; index < frameCount; index += 1) {
360
- frames.add(
361
- new Frame(
362
- readString(recordInput, MAX_FILENAME_BYTES),
363
- recordInput.readInt(),
364
- recordInput.readInt()));
365
- }
366
- int droppedRecords = recordInput.readInt();
367
- int corruptRecords = recordInput.readInt();
368
- if (recordInput.read() != -1) {
369
- return null;
370
- }
371
- return validateRecord(
372
- new Record(
373
- schemaVersion,
374
- id,
375
- timestamp,
376
- errorName,
377
- frames,
378
- droppedRecords,
379
- corruptRecords),
380
- false);
381
- }
382
- } catch (EOFException error) {
383
- return null;
384
- } catch (IOException | RuntimeException error) {
385
- return null;
386
- }
387
- }
388
-
389
- private static void writeString(DataOutputStream output, String value)
390
- throws IOException {
391
- byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
392
- output.writeInt(bytes.length);
393
- output.write(bytes);
394
- }
395
-
396
- private static String readString(DataInputStream input, int maximumBytes)
397
- throws IOException {
398
- int length = input.readInt();
399
- if (length < 1 || length > maximumBytes) {
400
- throw new IOException("invalid bounded string");
401
- }
402
- byte[] bytes = new byte[length];
403
- input.readFully(bytes);
404
- String value = new String(bytes, StandardCharsets.UTF_8);
405
- if (!Arrays.equals(bytes, value.getBytes(StandardCharsets.UTF_8))) {
406
- throw new IOException("invalid UTF-8");
407
- }
408
- return value;
409
- }
410
-
411
- private static Record validateRecord(Record record, boolean requireZeroCounters) {
412
- if (record == null
413
- || record.schemaVersion != 1
414
- || !validIdentifier(record.id)
415
- || !validTimestamp(record.timestamp)
416
- || !ERROR_NAMES.contains(record.errorName)
417
- || record.stackFrames == null
418
- || record.stackFrames.size() > MAX_FRAMES
419
- || record.droppedRecords < 0
420
- || record.corruptRecords < 0
421
- || (requireZeroCounters
422
- && (record.droppedRecords != 0 || record.corruptRecords != 0))) {
423
- return null;
424
- }
425
- List<Frame> frames = new ArrayList<>(record.stackFrames.size());
426
- for (Frame frame : record.stackFrames) {
427
- if (frame == null
428
- || !validFilename(frame.filename)
429
- || frame.line < 1
430
- || frame.column < 1) {
431
- return null;
432
- }
433
- frames.add(new Frame(frame.filename, frame.line, frame.column));
434
- }
435
- return new Record(
436
- 1,
437
- record.id,
438
- record.timestamp,
439
- record.errorName,
440
- frames,
441
- record.droppedRecords,
442
- record.corruptRecords);
443
- }
444
-
445
- private static boolean validIdentifier(String value) {
446
- return value != null
447
- && value.getBytes(StandardCharsets.UTF_8).length <= MAX_ID_BYTES
448
- && ID_PATTERN.matcher(value).matches();
449
- }
450
-
451
- private static boolean validTimestamp(String value) {
452
- if (value == null) {
453
- return false;
454
- }
455
- int bytes = value.getBytes(StandardCharsets.UTF_8).length;
456
- return bytes >= 20
457
- && bytes <= MAX_TIMESTAMP_BYTES
458
- && TIMESTAMP_PATTERN.matcher(value).matches();
459
- }
460
-
461
- private static boolean validFilename(String value) {
462
- if (value == null
463
- || value.isEmpty()
464
- || value.getBytes(StandardCharsets.UTF_8).length > MAX_FILENAME_BYTES
465
- || value.startsWith("/")
466
- || value.contains("\\")
467
- || value.contains("://")
468
- || value.contains("?")
469
- || value.contains("#")) {
470
- return false;
471
- }
472
- for (String component : value.split("/", -1)) {
473
- if ("..".equals(component)) {
474
- return false;
475
- }
476
- }
477
- for (int index = 0; index < value.length(); index += 1) {
478
- char character = value.charAt(index);
479
- if (character <= 31 || character == 127) {
480
- return false;
481
- }
482
- }
483
- return true;
484
- }
485
-
486
- private boolean deleteRegularFile(File file) {
487
- return file.exists() && isSafeRegularFile(file) && file.delete();
488
- }
489
-
490
- private boolean isSafeRegularFile(File file) {
491
- try {
492
- return file.isFile() && isCanonicalPath(file);
493
- } catch (IOException | SecurityException error) {
494
- return false;
495
- }
496
- }
497
-
498
- private boolean isCanonicalPath(File file) throws IOException {
499
- return file.getCanonicalFile().equals(file.getAbsoluteFile());
500
- }
501
-
502
- private static File resolveCanonicalParent(File value) {
503
- File absolute = value.getAbsoluteFile();
504
- File parent = absolute.getParentFile();
505
- if (parent == null) {
506
- return absolute;
507
- }
508
- try {
509
- return new File(parent.getCanonicalFile(), absolute.getName());
510
- } catch (IOException | SecurityException error) {
511
- return absolute;
512
- }
513
- }
514
-
515
- private boolean makeDirectoryPrivate(File value) {
516
- return value.setReadable(false, false)
517
- && value.setWritable(false, false)
518
- && value.setExecutable(false, false)
519
- && value.setReadable(true, true)
520
- && value.setWritable(true, true)
521
- && value.setExecutable(true, true);
522
- }
523
-
524
- private boolean makeFilePrivate(File value) {
525
- return value.setReadable(false, false)
526
- && value.setWritable(false, false)
527
- && value.setExecutable(false, false)
528
- && value.setReadable(true, true)
529
- && value.setWritable(true, true);
530
- }
531
-
532
- private boolean syncParentDirectory() {
533
- return parentDirectorySync.sync(directory) != ParentDirectorySyncResult.FAILED;
534
- }
535
-
536
- interface ParentDirectorySync {
537
- ParentDirectorySyncResult sync(File directory);
538
- }
539
-
540
- enum ParentDirectorySyncResult {
541
- SYNCHRONIZED,
542
- UNSUPPORTED,
543
- FAILED
544
- }
545
-
546
- static final class Frame {
547
- final String filename;
548
- final int line;
549
- final int column;
550
-
551
- Frame(String filename, int line, int column) {
552
- this.filename = filename;
553
- this.line = line;
554
- this.column = column;
555
- }
556
- }
557
-
558
- static final class Record {
559
- final int schemaVersion;
560
- final String id;
561
- final String timestamp;
562
- final String errorName;
563
- final List<Frame> stackFrames;
564
- final int droppedRecords;
565
- final int corruptRecords;
566
-
567
- Record(
568
- int schemaVersion,
569
- String id,
570
- String timestamp,
571
- String errorName,
572
- List<Frame> stackFrames,
573
- int droppedRecords,
574
- int corruptRecords) {
575
- this.schemaVersion = schemaVersion;
576
- this.id = id;
577
- this.timestamp = timestamp;
578
- this.errorName = errorName;
579
- this.stackFrames =
580
- stackFrames == null
581
- ? null
582
- : Collections.unmodifiableList(new ArrayList<>(stackFrames));
583
- this.droppedRecords = droppedRecords;
584
- this.corruptRecords = corruptRecords;
585
- }
586
-
587
- Record withCounters(int droppedRecords, int corruptRecords) {
588
- return new Record(
589
- schemaVersion,
590
- id,
591
- timestamp,
592
- errorName,
593
- stackFrames,
594
- droppedRecords,
595
- corruptRecords);
596
- }
597
- }
598
-
599
- static final class Result {
600
- final String status;
601
- final Record record;
602
- final String recordId;
603
- final int droppedRecords;
604
- final int corruptRecords;
605
-
606
- Result(
607
- String status,
608
- Record record,
609
- String recordId,
610
- int droppedRecords,
611
- int corruptRecords) {
612
- this.status = status;
613
- this.record = record;
614
- this.recordId = recordId;
615
- this.droppedRecords = droppedRecords;
616
- this.corruptRecords = corruptRecords;
617
- }
618
-
619
- static Result status(String status) {
620
- return new Result(status, null, null, 0, 0);
621
- }
622
- }
623
- }
@@ -1,25 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
-
3
- NS_ASSUME_NONNULL_BEGIN
4
-
5
- FOUNDATION_EXPORT NSString *const LBRNFatalRecordFileName;
6
- FOUNDATION_EXPORT NSString *const LBRNFatalRecordTemporaryFileName;
7
-
8
- typedef BOOL (^LBRNFatalDirectoryPreparation)(NSURL *directoryURL);
9
-
10
- @interface LBRNFatalRecordStore : NSObject
11
-
12
- - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL;
13
- - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL
14
- directoryPreparation:(LBRNFatalDirectoryPreparation)directoryPreparation
15
- NS_DESIGNATED_INITIALIZER;
16
- - (instancetype)init NS_UNAVAILABLE;
17
-
18
- - (NSDictionary *)writeRecord:(NSDictionary *)record;
19
- - (NSDictionary *)readRecord;
20
- - (NSDictionary *)acknowledgeRecordId:(NSString *)recordId;
21
- - (NSDictionary *)discardRecord;
22
-
23
- @end
24
-
25
- NS_ASSUME_NONNULL_END