marsrs-react-native-xlog 0.1.0-alpha.2

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 ADDED
@@ -0,0 +1,88 @@
1
+ # marsrs-react-native-xlog
2
+
3
+ The React Native module of the xlog half of
4
+ [mars-rs](https://github.com/orangeboyChen/mars-rs), a Rust implementation of
5
+ [Tencent/mars](https://github.com/Tencent/mars): it writes the `.xlog` the C++
6
+ implementation produced, encrypted or plain, zlib- or zstd-compressed, sync or
7
+ async.
8
+
9
+ ```ts
10
+ import { Xlog, LogLevel } from 'marsrs-react-native-xlog';
11
+
12
+ const xlog = Xlog.open({
13
+ logDir: RNFS.DocumentDirectoryPath,
14
+ namePrefix: 'marsrs',
15
+ pubKey: '...',
16
+ });
17
+ xlog.i('Net', 'hello');
18
+ xlog.log(LogLevel.Debug, 'Net', 'a debug line');
19
+ xlog.flush(true);
20
+ xlog.close();
21
+ ```
22
+
23
+ `Xlog` is one appender: `Xlog.open(config)` opens it and `close()` closes it —
24
+ the same class, under the same name, the Swift package,
25
+ `platforms/kmp/marsrs-xlog` and the Android AAR publish. Nothing here answers
26
+ a `Promise` and nothing needs an `await`: the module is a TurboModule, so a
27
+ call is made on the JS thread and returned from, and the settings are the
28
+ properties they are in Kotlin and in Swift — `xlog.level = LogLevel.Debug` —
29
+ and not `setLevel` / `getLevel` pairs.
30
+
31
+ That is what the New Architecture buys, and it is the only thing asked for in
32
+ return: a TurboModule is not a bridge module, so an app still on the old
33
+ architecture has no module to `TurboModuleRegistry.getEnforcing`.
34
+
35
+ ## Installing it
36
+
37
+ `npm install marsrs-react-native-xlog`, and `pod install` for the iOS half: the
38
+ module is published to npm, and the release of a tag is what publishes it.
39
+
40
+ ```bash
41
+ npm install marsrs-react-native-xlog
42
+ cd ios && pod install
43
+ ```
44
+
45
+ | | |
46
+ |---|---|
47
+ | version | the version of the release; `npm install` takes the newest published one |
48
+ | React Native | >= 0.74, on the New Architecture |
49
+ | iOS | 12.0 |
50
+ | Android | `minSdk` 24 |
51
+
52
+ The package ships its TypeScript as the `main` entry, which is what Metro
53
+ compiles — there is no build step, and `npm run typecheck` is the only script.
54
+ `src/NativeXlog.ts` is the spec React Native's codegen reads, and
55
+ `codegenConfig` in `package.json` is what points it at `src`: `NativeXlogSpec`
56
+ is generated into the app's `React-Codegen` pod and into the Android build, and
57
+ neither half of the module has to be told the eleven signatures twice.
58
+
59
+ ## What each platform resolves
60
+
61
+ The two halves do not get their native the same way, and the difference is the
62
+ ecosystem's, not the port's:
63
+
64
+ * **Android** depends on the `xlog` AAR of the same version
65
+ (`io.github.orangeboychen.marsrs:xlog`) from JitPack — the same coordinate,
66
+ resolved the same way, as for an app that takes the AAR directly. The `.so`
67
+ files are the AAR's, so nothing is built here and nothing Rust is needed.
68
+ * **iOS** carries `marsrs-xlog.xcframework` with it, in `ios/Frameworks`, and
69
+ `mars_xlog.h` next to it in `ios/include`. CocoaPods cannot resolve the SwiftPM
70
+ binary target of `Package.swift`, and there is no CocoaPods pod for it, so the
71
+ release drops the framework of its own tag into the package before packing it.
72
+
73
+ Both files arrive with the release and not with this directory: what is checked
74
+ in is the module, and `scripts/package_react_native.sh` is what makes a complete
75
+ package out of it.
76
+
77
+ ## Why it is called xlog's
78
+
79
+ The C ABI is 28 `mars_xlog_*` symbols and nothing else, and
80
+ `scripts/build_xcframework.sh` fails the day it is not — the same reason the
81
+ framework and the AAR are called xlog's. `marsrs-react-native` is the umbrella
82
+ the whole port joins under: STN and SDT land there and not here, so an app that
83
+ only logs keeps this one and downloads nothing it does not call. Today the two
84
+ are the same module under two names, and taking the umbrella costs exactly what
85
+ this one costs.
86
+
87
+ Take one of the two and not both: both carry the same `libmarsrsxlog.so`, and an
88
+ app with two of it does not build.
File without changes
@@ -0,0 +1,2 @@
1
+ #Mon Sep 28 14:47:25 CST 2026
2
+ gradle.version=9.2.0
File without changes
@@ -0,0 +1,49 @@
1
+ // The Android half of `marsrs-react-native-xlog`: a `ReactPackage` over `Xlog`,
2
+ // the Kotlin face of `libmarsrsxlog.so` in the `xlog` AAR.
3
+ // `marsrs-react-native` is the same module over the `marsrs` AAR — the whole
4
+ // port, which today is the same xlog — and it is the one an app takes when it
5
+ // wants more than logging.
6
+ //
7
+ // Nothing is compiled from Rust here, and no `.so` is packaged either: the AAR
8
+ // of the release this module was packaged for carries them, which is what the
9
+ // dependency below is. `scripts/package_react_native.sh` stamps its version.
10
+
11
+ plugins {
12
+ id("com.android.library")
13
+ // No version, and no `apply false`: the app that takes this module is the
14
+ // one that pins the Kotlin plugin, and Gradle resolves the request below
15
+ // from there. The AAR modules of `platforms/android/` need no plugin at
16
+ // all — their AGP is pinned by this repository and is 9, which compiles
17
+ // Kotlin on its own — but an app's is not, and is 8 today.
18
+ id("org.jetbrains.kotlin.android")
19
+ }
20
+
21
+ android {
22
+ namespace = "io.github.orangeboychen.marsrs.xlog.reactnative"
23
+ compileSdk = 36
24
+
25
+ defaultConfig {
26
+ // React Native's own floor since 0.76, and above the 24 `mars-jni` is
27
+ // built against.
28
+ minSdk = 24
29
+ }
30
+
31
+ compileOptions {
32
+ sourceCompatibility = JavaVersion.VERSION_17
33
+ targetCompatibility = JavaVersion.VERSION_17
34
+ }
35
+ }
36
+
37
+ repositories {
38
+ google()
39
+ mavenCentral()
40
+ // `xlog`, the AAR of the release; JitPack is where it is published.
41
+ maven("https://jitpack.io")
42
+ }
43
+
44
+ dependencies {
45
+ // `react-android` and not `react-native`: the artifact React Native has
46
+ // published since 0.71, and the one the app's own repository resolves.
47
+ implementation("com.facebook.react:react-android")
48
+ implementation("io.github.orangeboychen.marsrs:xlog:0.1.0-alpha.2")
49
+ }
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest />
@@ -0,0 +1,198 @@
1
+ // The Android half of `marsrs-react-native-xlog`: the eleven methods of the
2
+ // `Xlog` native module, each of them a straight call of a member of `Xlog` — the
3
+ // Kotlin face of `libmarsrsxlog.so` in the `marsrs-xlog` AAR, and the same
4
+ // class `platforms/kmp/marsrs-xlog` publishes to a Kotlin Multiplatform app
5
+ // and `platforms/apple/MarsRSXlog/Xlog.swift` to a Swift one. So what this
6
+ // file is is a native module over an API that already exists, and nothing of
7
+ // the API is invented here: `Xlog.open(XlogConfig(...))`, `log`,
8
+ // `isLoggable`, `flush`, `close`, and the five settings, under the names
9
+ // every other platform of the port gives them.
10
+ //
11
+ // A TurboModule, and not a bridge module: `src/NativeXlog.ts` is the spec
12
+ // codegen reads, and `NativeXlogSpec` — the abstract class it generates into
13
+ // this package — is what this class extends. What that buys is a call that is
14
+ // made where it is asked for and returned from: a method that answers no promise
15
+ // is a call and not an `await`, and one that answers a value answers it before
16
+ // the call returns. That, and only that, is why `src/index.ts` has no `Promise`
17
+ // in it and no `await` in front of a line.
18
+ //
19
+ // `marsrs-react-native` is the same module over the `marsrs` AAR — the whole
20
+ // port, which today is the same xlog — and this file is its Kotlin with the two
21
+ // names changed: xlog is the whole C ABI today, so the two are one package under
22
+ // two names, and they diverge the day STN and SDT land — there, and not here.
23
+ //
24
+ // Every key below is a field `src/index.ts` put there, and every number is the
25
+ // one `mars_xlog.h` gives a level, a mode and a compression. The appenders are
26
+ // kept by the prefix they were opened with because that is what an appender is
27
+ // known by in the C ABI and in `marsrs-jni`: two `Xlog`s of two prefixes are
28
+ // two appenders, and one of a prefix the module already has is the appender it
29
+ // already has.
30
+
31
+ package io.github.orangeboychen.marsrs.xlog.reactnative
32
+
33
+ import com.facebook.react.bridge.ReactApplicationContext
34
+ import com.facebook.react.bridge.ReadableMap
35
+ import io.github.orangeboychen.marsrs.xlog.AppenderMode
36
+ import io.github.orangeboychen.marsrs.xlog.CompressMode
37
+ import io.github.orangeboychen.marsrs.xlog.LogLevel
38
+ import io.github.orangeboychen.marsrs.xlog.Xlog
39
+ import io.github.orangeboychen.marsrs.xlog.XlogConfig
40
+ import java.util.concurrent.ConcurrentHashMap
41
+
42
+ /** The Android half of `Xlog`. */
43
+ class XlogModule(reactContext: ReactApplicationContext) : NativeXlogSpec(reactContext) {
44
+
45
+ /**
46
+ * The appender of every prefix `open` has opened, by the prefix: what keeps
47
+ * two `Xlog`s apart, which a single field cannot — the C ABI and
48
+ * `marsrs-jni` both answer an appender by the name it was opened with.
49
+ */
50
+ private val appenders = ConcurrentHashMap<String, Xlog>()
51
+
52
+ /**
53
+ * `Xlog.open(XlogConfig(...))`: opens the appender of the configuration
54
+ * `src/index.ts` sent, and answers whether it took it.
55
+ *
56
+ * One appender per prefix: a prefix [appenders] already holds is answered
57
+ * as `true` without a second `Xlog` over the first, which would be a handle
58
+ * nothing releases. `src/index.ts` does the same, so the two names an app
59
+ * holds for one prefix are one appender and `close` on either closes it for
60
+ * both.
61
+ *
62
+ * [Xlog.open] is what refuses a configuration the appender cannot honour —
63
+ * a blank `logDir` or `namePrefix`, a compression level out of range — and
64
+ * its `IllegalArgumentException` is what would cross back into JS as a
65
+ * crash, so it is caught here and answered as `false`: the caller's mistake
66
+ * to throw on, and not one to take the app with.
67
+ */
68
+ override fun open(config: ReadableMap): Boolean {
69
+ val namePrefix = config.string("namePrefix").ifBlank { DEFAULT_NAME_PREFIX }
70
+ if (appenders.containsKey(namePrefix)) {
71
+ return true
72
+ }
73
+ val xlogConfig = XlogConfig(
74
+ logDir = config.string("logDir"),
75
+ namePrefix = namePrefix,
76
+ level = LogLevel.of(config.int("level", LogLevel.INFO.ordinal)),
77
+ mode = appenderModeOf(config.int("mode", AppenderMode.ASYNC.ordinal)),
78
+ pubKey = config.string("pubKey"),
79
+ compressMode = compressModeOf(config.int("compressMode", CompressMode.ZLIB.ordinal)),
80
+ compressLevel = config.int("compressLevel", DEFAULT_COMPRESS_LEVEL),
81
+ cacheDir = config.optionalString("cacheDir"),
82
+ cacheDays = config.int("cacheDays", NO_CACHE_DAYS)
83
+ )
84
+ val xlog = try {
85
+ Xlog.open(xlogConfig)
86
+ } catch (e: IllegalArgumentException) {
87
+ return false
88
+ }
89
+ appenders[xlogConfig.namePrefix] = xlog
90
+ return true
91
+ }
92
+
93
+ /**
94
+ * `Xlog.log`. The file, the function and the line are the C++'s own
95
+ * defaults: there is no JS frame to name, and the C++ writes an empty one
96
+ * too.
97
+ */
98
+ override fun log(namePrefix: String, level: Double, tag: String, message: String) {
99
+ appender(namePrefix)?.log(LogLevel.of(level.toInt()), tag, message)
100
+ }
101
+
102
+ /** `Xlog.isLoggable`: whether a record of the level would be written. */
103
+ override fun isLoggable(namePrefix: String, level: Double): Boolean =
104
+ appender(namePrefix)?.isLoggable(LogLevel.of(level.toInt())) ?: false
105
+
106
+ /** `Xlog.level`, read: what `marsrs-jni` answers, and not what JS holds. */
107
+ override fun getLevel(namePrefix: String): Double =
108
+ appender(namePrefix)?.level?.ordinal?.toDouble() ?: LogLevel.NONE.ordinal.toDouble()
109
+
110
+ /** `Xlog.flush`. */
111
+ override fun flush(namePrefix: String, sync: Boolean) {
112
+ appender(namePrefix)?.flush(sync)
113
+ }
114
+
115
+ /** `Xlog.level`. */
116
+ override fun setLevel(namePrefix: String, level: Double) {
117
+ appender(namePrefix)?.level = LogLevel.of(level.toInt())
118
+ }
119
+
120
+ /** `Xlog.mode`. */
121
+ override fun setMode(namePrefix: String, mode: Double) {
122
+ appender(namePrefix)?.mode = appenderModeOf(mode.toInt())
123
+ }
124
+
125
+ /** `Xlog.consoleLogEnabled`. */
126
+ override fun setConsoleLogEnabled(namePrefix: String, enabled: Boolean) {
127
+ appender(namePrefix)?.consoleLogEnabled = enabled
128
+ }
129
+
130
+ /** `Xlog.maxFileSizeBytes`. A `Double` and not a `Long`: the module carries
131
+ * every JS number as one, and a file size is below 2^53. */
132
+ override fun setMaxFileSize(namePrefix: String, bytes: Double) {
133
+ appender(namePrefix)?.maxFileSizeBytes = bytes.toLong()
134
+ }
135
+
136
+ /** `Xlog.maxAliveTimeSeconds`. */
137
+ override fun setMaxAliveTime(namePrefix: String, seconds: Double) {
138
+ appender(namePrefix)?.maxAliveTimeSeconds = seconds.toLong()
139
+ }
140
+
141
+ /** `Xlog.close`: releases the appender `open` made. */
142
+ override fun close(namePrefix: String) {
143
+ appenders.remove(namePrefix)?.close()
144
+ }
145
+
146
+ /**
147
+ * The appender of [namePrefix], or `null` when there is none — which is a
148
+ * no-op and not a crash, the same answer the Swift and the Kotlin give an
149
+ * `Xlog` that is closed: no appender is the process-wide one to
150
+ * `marsrs-jni`, so a call that went on without one would write through
151
+ * whatever appender the rest of the process writes through.
152
+ */
153
+ private fun appender(namePrefix: String): Xlog? = appenders[namePrefix]?.takeIf { it.isOpen }
154
+
155
+ internal companion object {
156
+ /** The name `src/index.ts` reads the module out of
157
+ * `TurboModuleRegistry` by — and the one codegen gave the generated
158
+ * `NativeXlogSpec`. */
159
+ const val NAME = "Xlog"
160
+
161
+ /** `XlogConfig.namePrefix` of the Kotlin, of the Swift and of the TS. */
162
+ private const val DEFAULT_NAME_PREFIX = "xlog"
163
+
164
+ /** `0` — what the C++ passes on, which is the appender's own 6. */
165
+ private const val DEFAULT_COMPRESS_LEVEL = 0
166
+
167
+ /** `0` keeps every log file, which is what the C++'s default is. */
168
+ private const val NO_CACHE_DAYS = 0
169
+
170
+ /**
171
+ * `AppenderMode` of the number `mars_xlog.h` gives a mode: an ordinal
172
+ * the enum does not carry is one the caller sent by mistake, and `ASYNC`
173
+ * is what the C++ opens with.
174
+ */
175
+ fun appenderModeOf(ordinal: Int): AppenderMode = when (ordinal) {
176
+ AppenderMode.SYNC.ordinal -> AppenderMode.SYNC
177
+ else -> AppenderMode.ASYNC
178
+ }
179
+
180
+ /** `CompressMode` of the number `mars_xlog.h` gives a compression. */
181
+ fun compressModeOf(ordinal: Int): CompressMode = when (ordinal) {
182
+ CompressMode.ZSTD.ordinal -> CompressMode.ZSTD
183
+ else -> CompressMode.ZLIB
184
+ }
185
+ }
186
+ }
187
+
188
+ /** The number sent for [key], or [fallback] when none was sent: a field
189
+ * `src/index.ts` left out is not one it sent as `null`. */
190
+ private fun ReadableMap.int(key: String, fallback: Int): Int = if (hasKey(key)) getInt(key) else fallback
191
+
192
+ /** The string sent for [key], or the empty one. */
193
+ private fun ReadableMap.string(key: String): String = if (hasKey(key)) getString(key).orEmpty() else ""
194
+
195
+ /** The string sent for [key], or `null` — `cacheDir` is the field a caller may
196
+ * leave out, and `null` is what puts the cache in the log directory. */
197
+ private fun ReadableMap.optionalString(key: String): String? =
198
+ if (hasKey(key)) getString(key)?.takeIf { it.isNotBlank() } else null
@@ -0,0 +1,65 @@
1
+ // The `ReactPackage` of the Android half of `marsrs-react-native-xlog`.
2
+ //
3
+ // Autolinking finds this class by the `ReactPackage` it is and the
4
+ // `react-native` directory it is in: it is what puts `XlogModule` in React
5
+ // Native's hands, and nothing in the app has to name either of them.
6
+ //
7
+ // A `BaseReactPackage` and not a `ReactPackage`: `BaseReactPackage` is the one
8
+ // that answers `getModule`, which is what React Native asks a TurboModule for,
9
+ // and the `ReactModuleInfo` below is what tells it this module is one. A package
10
+ // that only answered `createNativeModules` would be handed to the bridge, and
11
+ // every call would be a promise again — which is the whole of what
12
+ // `src/index.ts` is not.
13
+
14
+ package io.github.orangeboychen.marsrs.xlog.reactnative
15
+
16
+ import com.facebook.react.BaseReactPackage
17
+ import com.facebook.react.bridge.NativeModule
18
+ import com.facebook.react.bridge.ReactApplicationContext
19
+ import com.facebook.react.module.model.ReactModuleInfo
20
+ import com.facebook.react.module.model.ReactModuleInfoProvider
21
+
22
+ /** The package of `Xlog`. */
23
+ class XlogPackage : BaseReactPackage() {
24
+
25
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
26
+ if (name == XlogModule.NAME) XlogModule(reactContext) else null
27
+
28
+ /**
29
+ * The one module this package has, and what React Native asks it about
30
+ * before it has an instance: its class, so that one can be made without
31
+ * asking, and `isTurboModule`, which is what sends the JSI after it rather
32
+ * than the bridge.
33
+ */
34
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
35
+ mapOf(
36
+ XlogModule.NAME to ReactModuleInfo(
37
+ XlogModule.NAME,
38
+ XlogModule::class.java.name,
39
+ CAN_OVERRIDE_EXISTING_MODULE,
40
+ NEEDS_EAGER_INIT,
41
+ HAS_CONSTANTS,
42
+ IS_CXX_MODULE,
43
+ IS_TURBO_MODULE
44
+ )
45
+ )
46
+ }
47
+
48
+ private companion object {
49
+ /** The module is the only one of its name an app has. */
50
+ const val CAN_OVERRIDE_EXISTING_MODULE = false
51
+
52
+ /** Nothing of the module is read before it is asked for. */
53
+ const val NEEDS_EAGER_INIT = false
54
+
55
+ /** The module carries no `constantsToExport`: a logger has none. */
56
+ const val HAS_CONSTANTS = false
57
+
58
+ /** Kotlin over a JNI bridge, and not a C++ module of React Native's
59
+ * own. */
60
+ const val IS_CXX_MODULE = false
61
+
62
+ /** What makes `src/index.ts` synchronous. */
63
+ const val IS_TURBO_MODULE = true
64
+ }
65
+ }
@@ -0,0 +1,14 @@
1
+ // What the Swift of the pod sees of the C ABI: `mars_xlog.h`, copied out of
2
+ // `crates/marsrs-ffi/include` by `scripts/package_react_native.sh` rather than
3
+ // kept here, so it cannot drift from the header the Rust is built against.
4
+ //
5
+ // A bridging header and not an `import`, because the framework the pod carries
6
+ // is a static library with a C header: `MarsRSFFI`, the module SwiftPM makes of
7
+ // the two, is nothing CocoaPods can hand a pod.
8
+ //
9
+ // React Native is not here, and that is the point: `Xlog.swift` conforms to no
10
+ // React Native protocol, because the one it would conform to — `NativeXlogSpec`,
11
+ // which codegen wrote — is declared in a header that refuses to be imported by
12
+ // anything but Objective-C++. What the module owes React Native instead is in
13
+ // `Xlog.mm`, which says why there.
14
+ #include "mars_xlog.h"
package/ios/Xlog.mm ADDED
@@ -0,0 +1,63 @@
1
+ // The one file of the iOS half of `marsrs-react-native-xlog` that Swift cannot
2
+ // be, and why: a TurboModule is made of a factory that answers a
3
+ // `std::shared_ptr<facebook::react::TurboModule>`, and `NativeXlogSpecJSI` — the
4
+ // C++ class codegen wrote out of `src/NativeXlog.ts` — is a class of templates
5
+ // and jsi types. Neither has a Swift spelling: Swift's C++ interop stops at
6
+ // templates and at the standard library. So what is here is the three things
7
+ // React Native asks a module for and cannot read out of `Xlog.swift`, and the
8
+ // eleven methods are there and not here.
9
+ //
10
+ // * `RCT_EXTERN_MODULE` registers the Swift class, which Swift cannot do for
11
+ // itself — registration is a constructor the runtime runs, and a Swift class
12
+ // has no `+load`. It is also what gives the class its `moduleName`, which is
13
+ // the name `src/index.ts` reads it out of `TurboModuleRegistry` by.
14
+ // * `RCTTurboModule` is adopted here, in a category, and not in the Swift:
15
+ // its one method is the C++ factory above, so a Swift class can neither name
16
+ // it nor be asked about it. `conformsToProtocol:` still answers `YES`, which
17
+ // is what `RCTTurboModuleManager` asks before it hands a module to JSI.
18
+ // * `methodQueue` answers `RCTJSThread`, the queue that is no queue: it is
19
+ // what makes a TurboModule's methods run on the JS thread, so a call is made
20
+ // where it is asked for and a method that answers a value answers it before
21
+ // it returns. Without it the eleven would be dispatched to a queue of the
22
+ // module's own and every answer would arrive as `undefined`.
23
+ //
24
+ // Objective-C++, and not Objective-C, because of the one `#include` a Swift
25
+ // class cannot make: `RCTNativeXlogSpec.h` opens with
26
+ // `#error This file must be compiled as Obj-C++`, and with good reason — it
27
+ // declares a C++ class.
28
+
29
+ #import <React/RCTBridgeModule.h>
30
+ #import <React/RCTJSThread.h>
31
+ #import <React/RCTTurboModule.h>
32
+
33
+ // What codegen wrote out of `src/NativeXlog.ts`: the protocol `NativeXlogSpec`
34
+ // and the C++ class `NativeXlogSpecJSI`, in one header. React Native names the
35
+ // file, and renamed it once — `RCTNativeXlogSpec.h` since 0.74,
36
+ // `NativeXlogSpec.h` before — so both spellings are asked for.
37
+ #if __has_include(<React-Codegen/RCTNativeXlogSpec.h>)
38
+ #import <React-Codegen/RCTNativeXlogSpec.h>
39
+ #else
40
+ #import <React-Codegen/NativeXlogSpec.h>
41
+ #endif
42
+
43
+ // `Xlog`, the Swift class, registered under the name `src/index.ts` reads it by.
44
+ @interface RCT_EXTERN_MODULE(Xlog, NSObject)
45
+ @end
46
+
47
+ @interface Xlog (XlogTurboModule) <RCTTurboModule>
48
+ @end
49
+
50
+ @implementation Xlog (XlogTurboModule)
51
+
52
+ - (dispatch_queue_t)methodQueue
53
+ {
54
+ return RCTJSThread;
55
+ }
56
+
57
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
58
+ (const facebook::react::ObjCTurboModule::InitParams &)params
59
+ {
60
+ return std::make_shared<facebook::react::NativeXlogSpecJSI>(params);
61
+ }
62
+
63
+ @end
package/ios/Xlog.swift ADDED
@@ -0,0 +1,253 @@
1
+ // The iOS half of `marsrs-react-native-xlog`: the eleven methods of the `Xlog`
2
+ // native module, each of them a straight call of a `mars_xlog_*` symbol — the
3
+ // C ABI of `crates/marsrs-ffi`, in the `marsrs-xlog.xcframework` the pod carries.
4
+ //
5
+ // Swift, and not Objective-C — except for `Xlog.mm`, which is the one file of
6
+ // the module Swift cannot be, and which says why in its own header: a
7
+ // TurboModule is made of a factory that answers a `std::shared_ptr` of a C++
8
+ // class, and there is no Swift for either.
9
+ //
10
+ // The module is a TurboModule and not a bridge module. `src/NativeXlog.ts` is
11
+ // the spec codegen reads, and what it makes of it — `NativeXlogSpec`, the
12
+ // protocol, and `NativeXlogSpecJSI`, the C++ class behind it — is what lets a
13
+ // call land where it is asked for rather than cross a bridge on a queue: the
14
+ // module answers `RCTJSThread` as its method queue, and every method of it is
15
+ // therefore made on the JS thread and returned from. A method that answers no
16
+ // promise is a call and not an `await`, which is why `src/index.ts` can be the
17
+ // Kotlin and the Swift of the port member for member.
18
+ //
19
+ // The appenders are kept by the prefix they were opened with, because that is
20
+ // what an appender is known by in the C ABI: two `Xlog`s of two prefixes are two
21
+ // appenders, and one of a prefix the module already has is the appender it
22
+ // already has.
23
+ //
24
+ // `marsrs-react-native` is the whole-port module, and this file is its Swift
25
+ // with the two names changed: xlog is the whole C ABI today, so the two are one
26
+ // package under two names, and they diverge the day STN and SDT land — there,
27
+ // and not here.
28
+
29
+ import Foundation
30
+
31
+ /// The iOS half of `Xlog`.
32
+ ///
33
+ /// `Xlog` and no suffix: the name `src/index.ts` reads the module out of
34
+ /// `TurboModuleRegistry` by is the class's.
35
+ @objc(Xlog)
36
+ internal final class Xlog: NSObject {
37
+ /// The handle of the appender of every prefix `open` has opened, by the
38
+ /// prefix: what keeps two `Xlog`s apart, which one handle cannot, because
39
+ /// the appender a `mars_xlog_release_instance` releases is the one of the
40
+ /// prefix it is given.
41
+ ///
42
+ /// Written and read on the JS thread and nowhere else, which is what the
43
+ /// module's `RCTJSThread` method queue makes true: the dictionary is not
44
+ /// locked, and a logger asked to wait for one would be a logger an app
45
+ /// routes around.
46
+ private var handles: [String: Int64] = [:]
47
+
48
+ /// `mars_xlog_new_instance`: opens the appender of `config` and answers
49
+ /// whether it took the configuration.
50
+ ///
51
+ /// `false` is a configuration it refused — an empty `logDir` or
52
+ /// `namePrefix`, or a directory it cannot write to — and it is what the JS
53
+ /// caller turns into a throw rather than a handle it would write through
54
+ /// the process-wide appender with.
55
+ @objc(open:)
56
+ internal func openAppender(_ config: [AnyHashable: Any]) -> Bool {
57
+ let logDir = string(config, "logDir")
58
+ guard !logDir.isEmpty else {
59
+ return false
60
+ }
61
+ let namePrefix = string(config, "namePrefix").isEmpty
62
+ ? Self.defaultNamePrefix
63
+ : string(config, "namePrefix")
64
+ // One appender per prefix: a prefix this module has already opened is
65
+ // answered as it is, and not opened again over the first, which would be
66
+ // a handle nothing releases. `src/index.ts` keeps the same map, so the
67
+ // two names an app holds for one prefix are one appender, and `close` on
68
+ // either is `close` on both.
69
+ guard handles[namePrefix] == nil else {
70
+ return true
71
+ }
72
+ let level = int(config, "level", Int32(MarsLevelInfo.rawValue))
73
+ let mode = int(config, "mode", Int32(MarsAppenderAsync.rawValue))
74
+ let compressMode = int(config, "compressMode", Int32(MarsCompressZlib.rawValue))
75
+ let compressLevel = int(config, "compressLevel", Self.defaultCompressLevel)
76
+ let cacheDays = int(config, "cacheDays", Self.keepEveryFile)
77
+
78
+ var handle: Int64 = 0
79
+ // `pubKey` and `cacheDir` are the two fields a caller may leave out, and
80
+ // `mars_xlog.h` reads the empty string the way it reads `NULL`.
81
+ withCStrings([logDir, namePrefix, string(config, "pubKey"), string(config, "cacheDir")]) { pointers in
82
+ var native = MarsXLogConfig(
83
+ mode: mode,
84
+ log_dir: pointers[Field.logDir.rawValue],
85
+ name_prefix: pointers[Field.namePrefix.rawValue],
86
+ pub_key: pointers[Field.pubKey.rawValue],
87
+ compress_mode: compressMode,
88
+ compress_level: compressLevel,
89
+ cache_dir: pointers[Field.cacheDir.rawValue],
90
+ cache_days: cacheDays
91
+ )
92
+ handle = mars_xlog_new_instance(&native, level)
93
+ }
94
+ guard handle != 0 else {
95
+ return false
96
+ }
97
+ handles[namePrefix] = handle
98
+ return true
99
+ }
100
+
101
+ /// `mars_xlog_write_instance`. The file, the function and the line are left
102
+ /// empty: there is no JS frame to name, and the C++ writes an empty one too.
103
+ @objc(log:level:tag:message:)
104
+ internal func log(_ namePrefix: String, level: Double, tag: String, message: String) {
105
+ guard let handle = handles[namePrefix] else {
106
+ return
107
+ }
108
+ tag.withCString { cTag in
109
+ message.withCString { cMessage in
110
+ mars_xlog_write_instance(handle, Int32(level), cTag, nil, nil, 0, cMessage)
111
+ }
112
+ }
113
+ }
114
+
115
+ /// `mars_xlog_is_enabled_for`: whether a record of the level would be
116
+ /// written, which an app asks before it builds a message that is expensive
117
+ /// to build.
118
+ @objc(isLoggable:level:)
119
+ internal func isLoggable(_ namePrefix: String, level: Double) -> Bool {
120
+ guard let handle = handles[namePrefix] else {
121
+ return false
122
+ }
123
+ return mars_xlog_is_enabled_for(handle, Int32(level)) != 0
124
+ }
125
+
126
+ /// `mars_xlog_get_level`: what the appender answers, and not what JS holds.
127
+ @objc(getLevel:)
128
+ internal func level(of namePrefix: String) -> Double {
129
+ guard let handle = handles[namePrefix] else {
130
+ return Double(MARS_LEVEL_NONE)
131
+ }
132
+ return Double(mars_xlog_get_level(handle))
133
+ }
134
+
135
+ /// `mars_xlog_flush_instance`.
136
+ @objc(flush:sync:)
137
+ internal func flush(_ namePrefix: String, sync: Bool) {
138
+ guard let handle = handles[namePrefix] else {
139
+ return
140
+ }
141
+ mars_xlog_flush_instance(handle, sync ? 1 : 0)
142
+ }
143
+
144
+ /// `mars_xlog_set_level_instance`.
145
+ @objc(setLevel:level:)
146
+ internal func setLevel(_ namePrefix: String, level: Double) {
147
+ guard let handle = handles[namePrefix] else {
148
+ return
149
+ }
150
+ mars_xlog_set_level_instance(handle, Int32(level))
151
+ }
152
+
153
+ /// `mars_xlog_set_mode_instance`.
154
+ @objc(setMode:mode:)
155
+ internal func setMode(_ namePrefix: String, mode: Double) {
156
+ guard let handle = handles[namePrefix] else {
157
+ return
158
+ }
159
+ mars_xlog_set_mode_instance(handle, Int32(mode))
160
+ }
161
+
162
+ /// `mars_xlog_set_console_log_instance`.
163
+ @objc(setConsoleLogEnabled:enabled:)
164
+ internal func setConsoleLogEnabled(_ namePrefix: String, enabled: Bool) {
165
+ guard let handle = handles[namePrefix] else {
166
+ return
167
+ }
168
+ mars_xlog_set_console_log_instance(handle, enabled ? 1 : 0)
169
+ }
170
+
171
+ /// `mars_xlog_set_max_file_size_instance`. A `Double` and not a `UInt64`:
172
+ /// the module carries every JS number as one, and a file size is below
173
+ /// 2^53.
174
+ @objc(setMaxFileSize:bytes:)
175
+ internal func setMaxFileSize(_ namePrefix: String, bytes: Double) {
176
+ guard let handle = handles[namePrefix] else {
177
+ return
178
+ }
179
+ mars_xlog_set_max_file_size_instance(handle, UInt64(bytes))
180
+ }
181
+
182
+ /// `mars_xlog_set_max_alive_duration_instance`.
183
+ @objc(setMaxAliveTime:seconds:)
184
+ internal func setMaxAliveTime(_ namePrefix: String, seconds: Double) {
185
+ guard let handle = handles[namePrefix] else {
186
+ return
187
+ }
188
+ mars_xlog_set_max_alive_duration_instance(handle, Int64(seconds))
189
+ }
190
+
191
+ /// `mars_xlog_release_instance`: closes the appender `open` made.
192
+ @objc(close:)
193
+ internal func close(_ namePrefix: String) {
194
+ guard handles.removeValue(forKey: namePrefix) != nil else {
195
+ return
196
+ }
197
+ namePrefix.withCString { mars_xlog_release_instance($0) }
198
+ }
199
+
200
+ /// Closes what `open` opened and `close` was not asked about: the module
201
+ /// goes away with the bridge, and an appender is a file its writer thread
202
+ /// holds open.
203
+ deinit {
204
+ for namePrefix in handles.keys {
205
+ namePrefix.withCString { mars_xlog_release_instance($0) }
206
+ }
207
+ handles.removeAll()
208
+ }
209
+
210
+ /// `XlogConfig.namePrefix` of the Kotlin, of the Swift and of the TS: what
211
+ /// an appender is opened with when the caller gave none.
212
+ private static let defaultNamePrefix = "xlog"
213
+
214
+ /// What the C++ passes on, which is the appender's own 6.
215
+ private static let defaultCompressLevel: Int32 = 0
216
+
217
+ /// `0` keeps every log file, which is what the C++'s default is.
218
+ private static let keepEveryFile: Int32 = 0
219
+
220
+ /// The four fields of `MarsXLogConfig` that are C strings, in the order the
221
+ /// copies are made in: what names the index a pointer is read out of, which
222
+ /// is what a `pointers[2]` is not.
223
+ private enum Field: Int {
224
+ case logDir = 0
225
+ case namePrefix = 1
226
+ case pubKey = 2
227
+ case cacheDir = 3
228
+ }
229
+
230
+ /// Runs `body` with every one of `strings` as a C string, and frees the
231
+ /// copies it made: `MarsXLogConfig` holds pointers rather than copies, and a
232
+ /// Swift `String` has no C string of its own to hand out — `withCString` is
233
+ /// the answer for one, and a configuration is four at once.
234
+ private func withCStrings(_ strings: [String], body: ([UnsafePointer<CChar>?]) -> Void) {
235
+ let copies: [UnsafeMutablePointer<CChar>?] = strings.map { strdup($0) }
236
+ defer { copies.forEach { free($0) } }
237
+ body(copies.map { $0.map { UnsafePointer($0) } })
238
+ }
239
+
240
+ /// What the caller sent for `key`, read as a string; a field it left out is
241
+ /// not one it sent as `null`, and the empty string is what every field of
242
+ /// the configuration reads as "not given".
243
+ private func string(_ config: [AnyHashable: Any], _ key: String) -> String {
244
+ (config[key] as? String) ?? ""
245
+ }
246
+
247
+ /// What the caller sent for `key`, read as an `Int32`, or `fallback` when it
248
+ /// sent nothing: a `Number`, because JS has one number type and the codec
249
+ /// hands over whichever the value fits in.
250
+ private func int(_ config: [AnyHashable: Any], _ key: String, _ fallback: Int32) -> Int32 {
251
+ (config[key] as? NSNumber)?.int32Value ?? fallback
252
+ }
253
+ }
@@ -0,0 +1,57 @@
1
+ # The iOS half of `marsrs-react-native-xlog`.
2
+ #
3
+ # `marsrs-xlog.xcframework` is carried here and not resolved: CocoaPods cannot
4
+ # take the SwiftPM binary target of Package.swift, and there is no pod for it,
5
+ # so `.github/workflows/release.yml` drops the release's framework into
6
+ # `ios/Frameworks/` before the package is packed. `ios/include/mars_xlog.h` is
7
+ # the C ABI's own header, copied by `scripts/package_react_native.sh` out of
8
+ # `crates/marsrs-ffi/include` rather than kept here, so it cannot drift from it.
9
+ #
10
+ # The glue is Swift — `ios/Xlog.swift` — over the C header the framework
11
+ # carries, because `MarsRSFFI`, the module SwiftPM makes of the two, is nothing
12
+ # CocoaPods can hand a pod: `ios/Xlog-Bridging-Header.h` is what shows the Swift
13
+ # `mars_xlog.h`, and `ios/Xlog.mm` is the one file of the module that has no
14
+ # Swift spelling, which its own header says why.
15
+ #
16
+ # `React-Codegen` is what `src/NativeXlog.ts` is generated by: `NativeXlogSpec`,
17
+ # the protocol, and `NativeXlogSpecJSI`, the C++ class `Xlog.mm` hands React
18
+ # Native — a pod of the app's own `use_react_native!`, and the pod the generated
19
+ # headers land in.
20
+ #
21
+ # The version is package.json's, and not a number of its own: one place a
22
+ # release stamps.
23
+
24
+ require "json"
25
+
26
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
27
+
28
+ Pod::Spec.new do |s|
29
+ s.name = package["name"]
30
+ s.version = package["version"]
31
+ s.summary = package["description"]
32
+ s.homepage = package["homepage"]
33
+ s.license = package["license"]
34
+ s.author = { "orangeboyChen" => "https://github.com/orangeboyChen" }
35
+ s.source = { :git => "https://github.com/orangeboyChen/mars-rs.git",
36
+ :tag => "v#{s.version}" }
37
+ # What Package.swift declares for iOS; the framework's own floor.
38
+ s.platform = :ios, "12.0"
39
+ s.swift_version = "5.0"
40
+ s.source_files = "ios/*.{h,mm,swift}"
41
+ s.vendored_frameworks = "ios/Frameworks/marsrs-xlog.xcframework"
42
+ s.preserve_paths = "ios/include/mars_xlog.h"
43
+ # One header for the pod's Swift to be bridged to the C ABI with, and one for
44
+ # the app's to be bridged to this pod's ObjC with. The first is the module's
45
+ # own; the second is the one CocoaPods reads when it compiles a pod that mixes
46
+ # the two languages.
47
+ s.xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/include\"",
48
+ "SWIFT_OBJC_BRIDGING_HEADER" => "\"$(PODS_TARGET_SRCROOT)/ios/Xlog-Bridging-Header.h\"" }
49
+ # `iana-time-zone` calls `CFTimeZone*`, and a Rust static library carries no
50
+ # link flags of its own: Package.swift names the framework for the same reason.
51
+ s.frameworks = "CoreFoundation"
52
+ # React Native's own, in place of `s.dependency "React-Core"`: it is what the
53
+ # app's `use_react_native!` provides, and it keeps the pod off a version of
54
+ # React Native the app did not choose.
55
+ install_modules_dependencies(s)
56
+ s.dependency "React-Codegen"
57
+ end
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "marsrs-react-native-xlog",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "React Native module for xlog, the logging half of mars-rs: a Rust implementation of Tencent/mars that writes the .xlog the C++ implementation produced.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/orangeboyChen/mars-rs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/orangeboyChen/mars-rs.git",
10
+ "directory": "platforms/react-native/marsrs-xlog"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/orangeboyChen/mars-rs/issues"
14
+ },
15
+ "keywords": [
16
+ "mars",
17
+ "xlog",
18
+ "logging",
19
+ "react-native"
20
+ ],
21
+ "main": "src/index.ts",
22
+ "types": "src/index.ts",
23
+ "files": [
24
+ "src",
25
+ "android",
26
+ "ios",
27
+ "*.podspec",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "codegenConfig": {
35
+ "name": "NativeXlogSpec",
36
+ "type": "modules",
37
+ "jsSrcsDir": "src",
38
+ "android": {
39
+ "javaPackageName": "io.github.orangeboychen.marsrs.xlog.reactnative"
40
+ }
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=18.0.0",
44
+ "react-native": ">=0.74.0"
45
+ }
46
+ }
@@ -0,0 +1,63 @@
1
+ // The TurboModule spec of `Xlog`: the file codegen reads, and what
2
+ // `src/index.ts` calls at runtime.
3
+ //
4
+ // Codegen makes two things of it — `NativeXlogSpec`, the abstract class the
5
+ // Android half extends, and `NativeXlogSpec`, the protocol the iOS half conforms
6
+ // to — and the two of them are what let a call land where it is made instead of
7
+ // crossing a bridge: a TurboModule method that answers no promise is called on
8
+ // the JS thread and returns, and one that answers a value answers it before it
9
+ // returns. That is the only reason `src/index.ts` can be the Kotlin and the
10
+ // Swift member for member — `Xlog.open(config)`, `xlog.level`,
11
+ // `xlog.i(tag, message)` — where the bridge's eleven `Promise`s left it with a
12
+ // `setLevel`, a `getLevel` and an `await` in front of every line.
13
+ //
14
+ // The configuration crosses as an `Object` and not as a declared type: codegen
15
+ // reads this file and not the ones it imports, so `XlogConfig` is documented in
16
+ // `src/index.ts`, where an app reads it — and a `ReadableMap` and an
17
+ // `NSDictionary` is what the two halves take either way.
18
+
19
+ import type { TurboModule } from 'react-native';
20
+ import { TurboModuleRegistry } from 'react-native';
21
+
22
+ export interface Spec extends TurboModule {
23
+ /** `Xlog.open`: whether the appender took the configuration. `false` is a
24
+ * configuration it refused, which is an empty `logDir` or `namePrefix`, or a
25
+ * directory it cannot write to. */
26
+ open(config: Object): boolean;
27
+
28
+ /** `mars_xlog_write_instance`. The file, the function and the line are left
29
+ * empty on the platform side: there is no JS frame to name, and the C++ writes
30
+ * an empty one too. */
31
+ log(namePrefix: string, level: number, tag: string, message: string): void;
32
+
33
+ /** `mars_xlog_is_enabled_for`: whether a record of the level would be written. */
34
+ isLoggable(namePrefix: string, level: number): boolean;
35
+
36
+ /** `mars_xlog_get_level`: the level the appender is at, and not the one JS
37
+ * holds. */
38
+ getLevel(namePrefix: string): number;
39
+
40
+ /** `mars_xlog_flush_instance`. */
41
+ flush(namePrefix: string, sync: boolean): void;
42
+
43
+ /** `mars_xlog_set_level_instance`. */
44
+ setLevel(namePrefix: string, level: number): void;
45
+
46
+ /** `mars_xlog_set_mode_instance`. */
47
+ setMode(namePrefix: string, mode: number): void;
48
+
49
+ /** `mars_xlog_set_console_log_instance`. */
50
+ setConsoleLogEnabled(namePrefix: string, enabled: boolean): void;
51
+
52
+ /** `mars_xlog_set_max_file_size_instance`. A `number` and not an `Int64`: the
53
+ * module carries every JS number as one, and a file size is below 2^53. */
54
+ setMaxFileSize(namePrefix: string, bytes: number): void;
55
+
56
+ /** `mars_xlog_set_max_alive_duration_instance`. */
57
+ setMaxAliveTime(namePrefix: string, seconds: number): void;
58
+
59
+ /** `mars_xlog_release_instance`: releases the appender `open` made. */
60
+ close(namePrefix: string): void;
61
+ }
62
+
63
+ export default TurboModuleRegistry.getEnforcing<Spec>('Xlog');
package/src/index.ts ADDED
@@ -0,0 +1,315 @@
1
+ // The TypeScript face of the xlog half of mars-rs.
2
+ //
3
+ // A TurboModule, and not a bridge module: `src/NativeXlog.ts` is the spec
4
+ // codegen reads, and what it answers is a module whose methods are called where
5
+ // they are asked for and return before the next line runs — one that answers no
6
+ // promise is a call and not an `await`, and one that answers a value answers it
7
+ // before it returns. So this `Xlog` is the `Xlog` of
8
+ // `platforms/apple/MarsRSXlog/Xlog.swift`, of
9
+ // `platforms/android/marsrs-xlog` and of `platforms/kmp/marsrs-xlog` member
10
+ // for member: `Xlog.open(config)` answers the appender, `xlog.level` is the
11
+ // property it is in Kotlin and in Swift, and `xlog.i(tag, message)` has
12
+ // landed by the time it returns.
13
+ //
14
+ // Five settings are properties and not `setLevel` / `getLevel` pairs, because a
15
+ // JS property is the Kotlin and the Swift spelling and a TurboModule setter is
16
+ // called where it is written: `xlog.level` reads the appender's own and writes
17
+ // it, and `mode`, `consoleLogEnabled`, `maxFileSizeBytes` and
18
+ // `maxAliveTimeSeconds` answer what this instance last wrote — the C ABI has no
19
+ // getter for those four, and neither the Kotlin nor the Swift answers one.
20
+
21
+ import NativeXlog from './NativeXlog';
22
+
23
+ /** `TLogLevel`; `none` is `MARS_LEVEL_NONE`, which the filter understands but
24
+ * the C enum does not carry. */
25
+ export const LogLevel = {
26
+ verbose: 0,
27
+ debug: 1,
28
+ info: 2,
29
+ warning: 3,
30
+ error: 4,
31
+ fatal: 5,
32
+ none: 6,
33
+ } as const;
34
+
35
+ export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
36
+
37
+ /** `TAppenderMode`: whether a record reaches the file before the call returns. */
38
+ export const AppenderMode = {
39
+ /** The record goes into the memory-mapped cache and a writer thread takes it
40
+ * to the file — what the C++ opens with, and what `flush` drains. */
41
+ async: 0,
42
+ /** The record is in the file before the call returns, at a write and a lock
43
+ * per line. */
44
+ sync: 1,
45
+ } as const;
46
+
47
+ export type AppenderMode = (typeof AppenderMode)[keyof typeof AppenderMode];
48
+
49
+ /** `TCompressMode`: how a log file is compressed once it is closed. */
50
+ export const CompressMode = {
51
+ /** zlib, what the C++ opens with and what every reader of a mars log file
52
+ * understands. */
53
+ zlib: 0,
54
+ /** Zstandard: smaller at the same speed, and unreadable by a tool that only
55
+ * knows zlib. */
56
+ zstd: 1,
57
+ } as const;
58
+
59
+ export type CompressMode = (typeof CompressMode)[keyof typeof CompressMode];
60
+
61
+ /** `XLogConfig` of the C++ project, in the spelling the Kotlin of the port
62
+ * gives it: what an `Xlog` is opened with.
63
+ *
64
+ * Every field has the default the C++'s own `XLogConfig` carries, so the one an
65
+ * app has to give is `logDir` — the appender answers
66
+ * `MARS_XLOG_ERR_EMPTY_LOG_DIR` without it. */
67
+ export interface XlogConfig {
68
+ /** Where the log files are written; created if it is missing, and the one
69
+ * field with no default. */
70
+ logDir: string;
71
+ /** What every log file starts with (`marsrs_20260927.xlog`), and the name the
72
+ * appender is known by — an app that writes through two of them gives them
73
+ * two. `xlog` when left out. */
74
+ namePrefix?: string;
75
+ /** The level the appender is opened at; `info` when left out. */
76
+ level?: LogLevel;
77
+ /** Whether a write reaches the file before it returns. */
78
+ mode?: AppenderMode;
79
+ /** Empty means the log is written unencrypted. */
80
+ pubKey?: string;
81
+ /** How a closed log file is compressed. */
82
+ compressMode?: CompressMode;
83
+ /** `0` keeps the appender's own default (6). */
84
+ compressLevel?: number;
85
+ /** Left out, the mmap cache lives in the log directory. */
86
+ cacheDir?: string;
87
+ /** `0` keeps every file. */
88
+ cacheDays?: number;
89
+ }
90
+
91
+ /** `XlogConfig.namePrefix` of the Kotlin and of the Swift: what an appender is
92
+ * opened with when a caller gives none. */
93
+ const DEFAULT_NAME_PREFIX = 'xlog';
94
+
95
+ /** The appender of every `namePrefix` `Xlog.open` has opened and `close` has
96
+ * not closed, by the prefix: what makes two `Xlog.open`s of one prefix one
97
+ * appender, which the native side already is — one module, one appender per
98
+ * prefix, and every call carrying the prefix it is about.
99
+ *
100
+ * The second `open` answers the appender the first made, so the two names an
101
+ * app holds are one `Xlog`: `close` on either is `close` on both, and nothing
102
+ * writes through an appender that is gone — which is what it would do, and in
103
+ * silence, if the second call were a second instance the first one's `close`
104
+ * did not know about. */
105
+ const openAppenders = new Map<string, Xlog>();
106
+
107
+ /** The appender of one `namePrefix`: the `Xlog` of the Swift and the Kotlin of
108
+ * the port. `Xlog.open` makes it, and `close` releases the appender it made. */
109
+ export class Xlog {
110
+ /** The name the appender is known by, and what every one of its files starts
111
+ * with: the `namePrefix` of the Kotlin and of the Swift. */
112
+ readonly namePrefix: string;
113
+
114
+ private currentLevel: LogLevel;
115
+
116
+ private currentMode: AppenderMode;
117
+
118
+ private currentConsoleLogEnabled = false;
119
+
120
+ private currentMaxFileSize = 0;
121
+
122
+ private currentMaxAliveTime = 0;
123
+
124
+ /** Whether `close` has been called on this instance. */
125
+ private open = true;
126
+
127
+ private constructor(config: XlogConfig) {
128
+ this.namePrefix = config.namePrefix ?? DEFAULT_NAME_PREFIX;
129
+ this.currentLevel = config.level ?? LogLevel.info;
130
+ this.currentMode = config.mode ?? AppenderMode.async;
131
+ }
132
+
133
+ /** `mars_xlog_new_instance`: opens the appender of `config` and answers the
134
+ * `Xlog` that writes through it.
135
+ *
136
+ * The appender of a `namePrefix` this has already opened is answered as it
137
+ * is, and not opened again: the native side is one appender per prefix, and a
138
+ * second one over the first would be a handle nothing releases. So two calls
139
+ * of one prefix are one `Xlog`, and `close` on it is `close` on both.
140
+ *
141
+ * Throws when the appender would not take the configuration — an empty
142
+ * `logDir` or `namePrefix`, or a directory it cannot write to. */
143
+ static open(config: XlogConfig): Xlog {
144
+ const namePrefix = config.namePrefix ?? DEFAULT_NAME_PREFIX;
145
+ const alreadyOpen = openAppenders.get(namePrefix);
146
+ if (alreadyOpen) {
147
+ return alreadyOpen;
148
+ }
149
+ const xlog = new Xlog(config);
150
+ if (!NativeXlog.open(config)) {
151
+ throw new Error(
152
+ `marsrs-react-native-xlog: the appender of '${xlog.namePrefix}' refused ${config.logDir}`
153
+ );
154
+ }
155
+ openAppenders.set(namePrefix, xlog);
156
+ return xlog;
157
+ }
158
+
159
+ /** Whether the appender is still open: `false` after `close`. */
160
+ get isOpen(): boolean {
161
+ return this.open;
162
+ }
163
+
164
+ /** The level of this appender: what the appender answers, and not what this
165
+ * instance holds — `mars_xlog_get_level`, which is the one of the five
166
+ * settings the C ABI has a getter for. */
167
+ get level(): LogLevel {
168
+ if (!this.open) {
169
+ return this.currentLevel;
170
+ }
171
+ const level = NativeXlog.getLevel(this.namePrefix) as LogLevel;
172
+ this.currentLevel = level;
173
+ return level;
174
+ }
175
+
176
+ set level(level: LogLevel) {
177
+ if (!this.open) {
178
+ return;
179
+ }
180
+ NativeXlog.setLevel(this.namePrefix, level);
181
+ this.currentLevel = level;
182
+ }
183
+
184
+ /** The mode of this appender: what `open` or the last write to it gave it,
185
+ * because the C ABI is asked for a mode and never answers one. */
186
+ get mode(): AppenderMode {
187
+ return this.currentMode;
188
+ }
189
+
190
+ set mode(mode: AppenderMode) {
191
+ if (!this.open) {
192
+ return;
193
+ }
194
+ NativeXlog.setMode(this.namePrefix, mode);
195
+ this.currentMode = mode;
196
+ }
197
+
198
+ /** Whether the console prints the log too: what the last write to it gave it,
199
+ * `false` when there was none. */
200
+ get consoleLogEnabled(): boolean {
201
+ return this.currentConsoleLogEnabled;
202
+ }
203
+
204
+ set consoleLogEnabled(enabled: boolean) {
205
+ if (!this.open) {
206
+ return;
207
+ }
208
+ NativeXlog.setConsoleLogEnabled(this.namePrefix, enabled);
209
+ this.currentConsoleLogEnabled = enabled;
210
+ }
211
+
212
+ /** The size a log file is split at, in bytes; `0` never splits. */
213
+ get maxFileSizeBytes(): number {
214
+ return this.currentMaxFileSize;
215
+ }
216
+
217
+ set maxFileSizeBytes(bytes: number) {
218
+ if (!this.open) {
219
+ return;
220
+ }
221
+ NativeXlog.setMaxFileSize(this.namePrefix, bytes);
222
+ this.currentMaxFileSize = bytes;
223
+ }
224
+
225
+ /** How long a log file is written to before the appender opens the next one,
226
+ * in seconds; `0` is the C++'s own ten days. */
227
+ get maxAliveTimeSeconds(): number {
228
+ return this.currentMaxAliveTime;
229
+ }
230
+
231
+ set maxAliveTimeSeconds(seconds: number) {
232
+ if (!this.open) {
233
+ return;
234
+ }
235
+ NativeXlog.setMaxAliveTime(this.namePrefix, seconds);
236
+ this.currentMaxAliveTime = seconds;
237
+ }
238
+
239
+ /** Whether a record at `level` is written: the appender's own answer, and the
240
+ * one the Swift's `isLoggable` and the Kotlin's give. An app asks it before it
241
+ * builds a message that is expensive to build. */
242
+ isLoggable(level: LogLevel): boolean {
243
+ if (!this.open) {
244
+ return false;
245
+ }
246
+ return NativeXlog.isLoggable(this.namePrefix, level);
247
+ }
248
+
249
+ /** `mars_xlog_write_instance`: writes `message` at `level`, tagged `tag`.
250
+ *
251
+ * The file, the function and the line of the record are left empty on the
252
+ * platform side — there is no JS frame to name, and the C++ writes an empty
253
+ * one too. */
254
+ log(level: LogLevel, tag: string, message: string): void {
255
+ if (!this.open) {
256
+ return;
257
+ }
258
+ NativeXlog.log(this.namePrefix, level, tag, message);
259
+ }
260
+
261
+ /** `log` at `LogLevel.verbose`. */
262
+ v(tag: string, message: string): void {
263
+ this.log(LogLevel.verbose, tag, message);
264
+ }
265
+
266
+ /** `log` at `LogLevel.debug`. */
267
+ d(tag: string, message: string): void {
268
+ this.log(LogLevel.debug, tag, message);
269
+ }
270
+
271
+ /** `log` at `LogLevel.info`. */
272
+ i(tag: string, message: string): void {
273
+ this.log(LogLevel.info, tag, message);
274
+ }
275
+
276
+ /** `log` at `LogLevel.warning`. */
277
+ w(tag: string, message: string): void {
278
+ this.log(LogLevel.warning, tag, message);
279
+ }
280
+
281
+ /** `log` at `LogLevel.error`. */
282
+ e(tag: string, message: string): void {
283
+ this.log(LogLevel.error, tag, message);
284
+ }
285
+
286
+ /** `log` at `LogLevel.fatal`. */
287
+ f(tag: string, message: string): void {
288
+ this.log(LogLevel.fatal, tag, message);
289
+ }
290
+
291
+ /** `mars_xlog_flush_instance`: takes what is in the cache to the file. `sync`
292
+ * waits for the write, which is what an app wants before it reads the file,
293
+ * uploads it, or lets the process go. */
294
+ flush(sync = false): void {
295
+ if (!this.open) {
296
+ return;
297
+ }
298
+ NativeXlog.flush(this.namePrefix, sync);
299
+ }
300
+
301
+ /** `mars_xlog_release_instance`: closes the appender `Xlog.open` made.
302
+ * Nothing is closed twice: an `Xlog` that is already closed answers `false`
303
+ * from `isOpen` and drops what it is asked to write — and an appender two
304
+ * names hold is closed for both, because `Xlog.open` gave them one `Xlog`. */
305
+ close(): void {
306
+ if (!this.open) {
307
+ return;
308
+ }
309
+ this.open = false;
310
+ openAppenders.delete(this.namePrefix);
311
+ NativeXlog.close(this.namePrefix);
312
+ }
313
+ }
314
+
315
+ export default Xlog;