@chipmobilesdk/rn-core 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # @chipmobilesdk/rn-core
package/README.md CHANGED
@@ -1,3 +1,256 @@
1
- # Temporary Holding Version
1
+ # @chipmobilesdk/rn-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The shared foundation of the `@chipmobilesdk/*` React Native packages: one error
4
+ model and one redaction mechanism, so every package fails and redacts the same way.
5
+ Pure TypeScript, no native code, no side effects on import.
6
+
7
+ > **License:** `UNLICENSED`. Published publicly for use by the owner's
8
+ > applications; no open-source license is granted.
9
+
10
+ - **Version**: see [CHANGELOG](CHANGELOG.md)
11
+
12
+ Apps rarely import it directly: every other package depends on it. Import it when
13
+ you handle errors from several packages in one place, or when you write a package
14
+ of your own in the same family.
15
+
16
+ ## Entrypoints
17
+
18
+ | Import | What it holds |
19
+ |---|---|
20
+ | `@chipmobilesdk/rn-core` | Everything below except test helpers |
21
+ | `@chipmobilesdk/rn-core/errors` | `SdkError`, `isSdkError`, `defineErrorCodes`, `createIssueCollector` |
22
+ | `@chipmobilesdk/rn-core/redact` | `createRedactor` and the redaction primitives |
23
+ | `@chipmobilesdk/rn-core/listeners` | `createListenerSet`, `createEmitter` |
24
+ | `@chipmobilesdk/rn-core/diagnostics` | `installSdkSink`, `createDiagnosticChannel` |
25
+ | `@chipmobilesdk/rn-core/scheduler` | timers, backoff, retry, single-flight, timeouts, bounds |
26
+ | `@chipmobilesdk/rn-core/lazy` | `lazyPeer` |
27
+ | `@chipmobilesdk/rn-core/kv` | `KeyValuePort`, `openDurable`, `fromMmkvLike`, `createMemoryKeyValue`, `namespaced` |
28
+ | `@chipmobilesdk/rn-core/runtime` | `currentPlatform`, `byPlatform`, `isDevBuild` |
29
+ | `@chipmobilesdk/rn-core/intl` | `getNumberFormat`, `getDateTimeFormat`, `isValidTimeZone` |
30
+ | `@chipmobilesdk/rn-core/messages` | `defineMessages`, `formatMessage` |
31
+ | `@chipmobilesdk/rn-core/store` | `createStore`, `shallowEqual` |
32
+ | `@chipmobilesdk/rn-core/react` | `useExternalStore`, `useLatestCallback`, `createSdkContext` (needs React) |
33
+ | `@chipmobilesdk/rn-core/testing` | `redactionConformance`, the conformance harness, `createManualScheduler` — for tests only |
34
+
35
+ ## Errors
36
+
37
+ Every package's public error class (`AdsError`, `PaymentError`, `StorageError`...)
38
+ extends `SdkError`, keeping its own name and code union. So one handler covers all
39
+ of them:
40
+
41
+ ```ts
42
+ import { isSdkError } from '@chipmobilesdk/rn-core';
43
+
44
+ function report(error: unknown) {
45
+ if (isSdkError(error)) {
46
+ // error.pkg: 'rn-ads', 'rn-payment'...; error.code: that package's code
47
+ log({ pkg: error.pkg, code: error.code, retryable: error.retryable, cause: error.redactedCause });
48
+ }
49
+ }
50
+ ```
51
+
52
+ - `code` is the package's stable code; branch on it, never on `message`.
53
+ - `retryable` says whether the same call can succeed if made again.
54
+ - `redactedCause` is the underlying failure, already reduced to text that is safe to
55
+ log. The original error object is never kept, since it may quote user data.
56
+ - `isSdkError` checks a brand, not `instanceof`, so it still works when an app ends up
57
+ with two copies of rn-core in `node_modules`.
58
+
59
+ `defineErrorCodes(codes, { retryable })` declares a package's code set once and
60
+ answers `is(value)` and `isRetryable(code)`. `createIssueCollector()` gathers every
61
+ problem in a config so it can be thrown at once.
62
+
63
+ ## Redaction
64
+
65
+ `createRedactor(policy)` returns `{ text, detail, context }`:
66
+
67
+ - `text(value)` reduces anything to one safe string. An `SdkError` becomes its code.
68
+ - `text: 'identifier'` (the default) keeps only a leading engine constant such as
69
+ `SQLITE_BUSY` and drops all other prose: no pattern can tell prose about a
70
+ failure from prose about a person.
71
+ - `text: 'patterns'` keeps prose but replaces URLs, JWTs, e-mail addresses, UUIDs
72
+ and long base64 runs (`COMMON_PATTERNS`), plus the policy's own `patterns`.
73
+ `dropKeys` are replaced wholesale when an object is rendered.
74
+ - `detail(record)` keeps only `allowDetailKeys`, and only primitive values.
75
+ - `context(record)` replaces the values of `shapeKeys` by their shape
76
+ (`[string:12]`), drops `dropKeys`, and pattern-redacts every other string.
77
+
78
+ The primitives are exported too: `redactPatterns`, `leadingIdentifier`,
79
+ `describeShape`, `pickAllowed`, `REDACTED`, `COMMON_PATTERNS`, `PATH_PATTERN`.
80
+
81
+ ## Listeners
82
+
83
+ `createListenerSet<Args>()` is how every package notifies subscribers:
84
+ `add(listener)` returns an unsubscribe that is safe to call twice, and
85
+ `notify(...args)` calls each listener present when it starts. A listener removed
86
+ during a round is not called; one added waits for the next round. Each listener runs
87
+ in isolation, so one that throws neither stops the others nor reaches the code that
88
+ notified. Its error is reported to `onListenerError` when given, otherwise rethrown on
89
+ a fresh task, where the app's global error handler sees it.
90
+
91
+ `createEmitter<Event>({ dedupe })` adds dispose and an optional dedupe by key. The
92
+ dedupe remembers at most `capacity` keys, oldest forgotten first, so memory stays
93
+ bounded however long the emitter lives. `emit` returns whether the event was
94
+ delivered.
95
+
96
+ ## Diagnostics
97
+
98
+ Every package reports what it observes as an `SdkDiagnostic`: `pkg`, `code`,
99
+ `severity` (`debug`, `info`, `warn`, `error`), `at`, and optionally a
100
+ `correlationId` and an already-redacted `context`. One sink receives all of them:
101
+
102
+ ```ts
103
+ import { installSdkSink } from '@chipmobilesdk/rn-core';
104
+
105
+ const uninstall = installSdkSink(d => {
106
+ if (d.severity === 'error' || d.severity === 'warn') crashReporter.log(`${d.pkg}:${d.code}`, d.context);
107
+ });
108
+ ```
109
+
110
+ Install it at startup, before creating the packages' instances: rn-local-db only
111
+ instruments its operations while someone listens. Sinks are shared through a
112
+ `Symbol.for` registry, so one installed through any copy of rn-core receives
113
+ every package's diagnostics. A sink that throws is reported on a fresh task and
114
+ never affects the package. Packages create a `createDiagnosticChannel(pkg)` and
115
+ `emit(code, severity, context?, correlationId?)`; the context is redacted before
116
+ any sink sees it.
117
+
118
+ ## Scheduling and bounds
119
+
120
+ - `Scheduler` (`setTimeout`/`clearTimeout`) is injectable everywhere time is used;
121
+ `realScheduler` is the default. `createTimerSet(scheduler)` tracks every timer an
122
+ owner started, so disposing can prove none is left (`size()`).
123
+ - `computeBackoff(attempt, { initialDelayMs, maxDelayMs, factor = 2, jitter })`:
124
+ initial × factor^(attempt−1), capped; `jitter: 'full'` spreads it over [0, delay].
125
+ - `runWithRetry({ spec: { ...backoff, maxAttempts }, attempt, onExhausted })` runs an
126
+ attempt now and then on that schedule; a throw is a failed attempt, exhaustion is
127
+ reported rather than thrown, and `cancel()` is idempotent.
128
+ - `createSingleFlight()` joins concurrent callers onto one operation;
129
+ `createExclusive()` refuses a second caller instead; `createRateLimiter(ms)` drops
130
+ calls inside the interval; `withTimeout(promise, ms, onTimeout)` always clears its
131
+ timer.
132
+ - `checkRange(label, value, range, fail, unit?)` and `checkInteger(...)` reject an
133
+ out-of-range value, never clamp it, and throw what `fail(message)` builds, so each
134
+ package raises its own error with its own code.
135
+
136
+ `createManualScheduler()` (`@chipmobilesdk/rn-core/testing`) is a scheduler a test
137
+ drives with `advance(ms)`.
138
+
139
+ ## Native peers, storage and runtime
140
+
141
+ - `lazyPeer(name, () => require('peer'))` loads a native peer on first use and caches
142
+ the outcome. `get()` never throws: `{ ok: true, module }` or `{ ok: false, peer,
143
+ cause }`. `require(fail)` returns the module or throws the package's own error.
144
+ `override(fake | null | undefined)` is the test seam. Keep the literal `require`
145
+ in the package that depends on the peer, so Metro bundles it there, and put it
146
+ inside a `try` block: Metro fails the whole bundle on a module it cannot find
147
+ unless the `require` sits in one, so an app that leaves an optional peer out
148
+ would not build.
149
+
150
+ ```ts
151
+ const messaging = lazyPeer('@react-native-firebase/messaging', () => {
152
+ try {
153
+ return require('@react-native-firebase/messaging') as MessagingModule;
154
+ } catch (cause) {
155
+ throw cause;
156
+ }
157
+ });
158
+ ```
159
+ - `KeyValuePort` is a synchronous store with a `durable` flag. `openDurable(open,
160
+ onDegraded)` opens MMKV (or anything `MmkvLike`), or falls back to memory and
161
+ reports why. `namespaced(port, prefix)` shares one store between owners.
162
+ - `currentPlatform()` is the one platform source: `'android'`, `'ios'` or
163
+ `'unsupported'` (Node, web), read from React Native lazily. `setPlatformOverride`
164
+ pins it in a test. `isDevBuild()` reads `__DEV__`, or `NODE_ENV` where that global
165
+ does not exist. React Native is an optional peer, needed only for `runtime`.
166
+ - `appLifecycle()` is the app's lifecycle, shared by every package: one
167
+ reference-counted `AppState` subscription that exists only while somebody listens.
168
+ `current()` is `'active'`, `'inactive'` or `'background'`; `subscribe((state,
169
+ previous) => …)` hears changes only, never a repeat of the same state; `listenerCount`
170
+ says who holds it. `onForeground(listener, port?)` fires on a return from the
171
+ background, not on a trip through `inactive` (Control Center, a call banner).
172
+ `createAppLifecycle(source?)` builds a port over another source, and
173
+ `createManualLifecycle(initial?)` (`./testing`) is one a test moves with `set(state)`.
174
+ Packages never subscribe to `AppState` themselves.
175
+
176
+ ## Messages
177
+
178
+ `defineMessages({ user, developer? })` declares a package's copy once: `user` texts
179
+ may reach an end user, `developer` texts describe a configuration fault and must not.
180
+ The set exposes `defaults`, `user`, `developer`, the key lists, `resolve(overrides)`,
181
+ `audienceOf(key)` and `format(key, vars)`. Variables are `{name}`; `formatMessage`
182
+ fills them and leaves an unknown one as written. A catalog is plain data, so rn-i18n's
183
+ `runtimeEntriesFromCatalog` registers it for translation.
184
+
185
+ ## Intl
186
+
187
+ `getNumberFormat(locale, options)` and `getDateTimeFormat(locale, options)` return a
188
+ shared formatter per locale and options (key order does not matter), from a small
189
+ LRU cache: constructing one is expensive on Hermes, and formatters never change once
190
+ built. They throw what the constructor throws, so the caller decides how a bad option
191
+ is reported. `isValidTimeZone(zone)` is memoized; Hermes raises a hard error for an
192
+ unknown zone, so check a stored zone before formatting with it.
193
+
194
+ ## React
195
+
196
+ `createStore(initial, isEqual?)` holds state whose snapshot keeps its identity until
197
+ it actually changes, and notifies only then; it needs no React, so a package's pure
198
+ core can own one. From `@chipmobilesdk/rn-core/react`:
199
+
200
+ - `useExternalStore(subscribe, getSnapshot, select?, isEqual?)` reads any external
201
+ store through `useSyncExternalStore`. With `select`, the component re-renders only
202
+ when the selected value changes, however often the store changes.
203
+ - `useLatestCallback(fn)` keeps one identity while calling the latest `fn`, for
204
+ listeners passed inline.
205
+ - `createSdkContext(name)` returns a `Provider` and a `use()` that throws a clear
206
+ error outside it.
207
+
208
+ React is an optional peer, needed only for this entrypoint; the main entrypoint
209
+ never imports it.
210
+
211
+ ## Testing
212
+
213
+ `redactionConformance(redact, { stringsOnly? })` runs a fixed set of secrets (a JWT,
214
+ a token in a URL, an e-mail address, a UUID, a base64 payload, an `Error` carrying a
215
+ token, and objects carrying them) through a redaction function and returns a report
216
+ listing every secret that survived. Every `@chipmobilesdk` package runs it against
217
+ each of its redaction entrypoints. The report is plain data, so it runs in Jest and
218
+ on a device alike.
219
+
220
+ ### Port conformance
221
+
222
+ A package states what any implementation of one of its ports must do as a list of
223
+ checks, and runs the same list against its fake and against its real adapter (native
224
+ module mocked in Jest, or for real from a demo screen). That is what keeps a fake
225
+ honest: if it drifts from the adapter it stands in for, a check fails.
226
+
227
+ ```ts
228
+ import { ensure, expectConformance, runChecks, type Check } from '@chipmobilesdk/rn-core/testing';
229
+
230
+ const checks: readonly Check<{ create(): KeyValuePort }>[] = [
231
+ {
232
+ name: 'reads back what it wrote',
233
+ run(harness) {
234
+ const store = harness.create();
235
+ store.set('k', 'v');
236
+ ensure(store.get('k') === 'v', 'value changed');
237
+ },
238
+ },
239
+ ];
240
+
241
+ expectConformance(await runChecks(() => harness, checks), 'memory store');
242
+ ```
243
+
244
+ - `runChecks(makeSubject, checks)` runs every check against a fresh subject and returns
245
+ a `ConformanceReport`; a throwing check is a failure with its message as `detail`.
246
+ - `expectConformance(report, subject?)` throws one error naming every failed check,
247
+ for a test runner. On a device, render the report instead.
248
+ - `ensure(condition, message)` is the assertion a check uses; it needs no test runner.
249
+ - `createFaultRegistry<Point>()` is deterministic fault injection: a fake calls
250
+ `throwIfFaulted(point)` at its named points, a test `arm`s the ones that should fail
251
+ (`times` defaults to one), and `fired()` proves the fault was reached.
252
+
253
+ The suites built on it ship from each package's `./testing` entrypoint:
254
+ `runPermissionPortConformance` and `runCameraEngineConformance` (rn-camera),
255
+ `runProviderPortConformance` (rn-payment), `runAdsEngineConformance` (rn-ads), and
256
+ `runSecureTokenStoreConformance` and `runOidcAuthClientConformance` (rn-auth).
package/package.json CHANGED
@@ -1,6 +1,108 @@
1
1
  {
2
- "name": "@chipmobilesdk/rn-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@chipmobilesdk/rn-core",
3
+ "version": "0.0.0",
4
+ "private": false,
5
+ "license": "UNLICENSED",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "react-native": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./errors": {
15
+ "types": "./src/errors/index.ts",
16
+ "react-native": "./src/errors/index.ts",
17
+ "default": "./src/errors/index.ts"
18
+ },
19
+ "./redact": {
20
+ "types": "./src/redact/index.ts",
21
+ "react-native": "./src/redact/index.ts",
22
+ "default": "./src/redact/index.ts"
23
+ },
24
+ "./listeners": {
25
+ "types": "./src/listeners/index.ts",
26
+ "react-native": "./src/listeners/index.ts",
27
+ "default": "./src/listeners/index.ts"
28
+ },
29
+ "./diagnostics": {
30
+ "types": "./src/diagnostics/index.ts",
31
+ "react-native": "./src/diagnostics/index.ts",
32
+ "default": "./src/diagnostics/index.ts"
33
+ },
34
+ "./scheduler": {
35
+ "types": "./src/scheduler/index.ts",
36
+ "react-native": "./src/scheduler/index.ts",
37
+ "default": "./src/scheduler/index.ts"
38
+ },
39
+ "./lazy": {
40
+ "types": "./src/lazy/index.ts",
41
+ "react-native": "./src/lazy/index.ts",
42
+ "default": "./src/lazy/index.ts"
43
+ },
44
+ "./kv": {
45
+ "types": "./src/kv/index.ts",
46
+ "react-native": "./src/kv/index.ts",
47
+ "default": "./src/kv/index.ts"
48
+ },
49
+ "./runtime": {
50
+ "types": "./src/runtime/index.ts",
51
+ "react-native": "./src/runtime/index.ts",
52
+ "default": "./src/runtime/index.ts"
53
+ },
54
+ "./intl": {
55
+ "types": "./src/intl/index.ts",
56
+ "react-native": "./src/intl/index.ts",
57
+ "default": "./src/intl/index.ts"
58
+ },
59
+ "./messages": {
60
+ "types": "./src/messages/index.ts",
61
+ "react-native": "./src/messages/index.ts",
62
+ "default": "./src/messages/index.ts"
63
+ },
64
+ "./store": {
65
+ "types": "./src/store/index.ts",
66
+ "react-native": "./src/store/index.ts",
67
+ "default": "./src/store/index.ts"
68
+ },
69
+ "./react": {
70
+ "types": "./src/react/index.ts",
71
+ "react-native": "./src/react/index.ts",
72
+ "default": "./src/react/index.ts"
73
+ },
74
+ "./testing": {
75
+ "types": "./src/testing/index.ts",
76
+ "react-native": "./src/testing/index.ts",
77
+ "default": "./src/testing/index.ts"
78
+ }
79
+ },
80
+ "files": [
81
+ "src",
82
+ "!src/**/__tests__",
83
+ "skills",
84
+ "README.md",
85
+ "CHANGELOG.md"
86
+ ],
87
+ "sideEffects": false,
88
+ "scripts": {
89
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
90
+ "pack:check": "npm pack --dry-run"
91
+ },
92
+ "peerDependencies": {
93
+ "react": ">=19",
94
+ "react-native": ">=0.85"
95
+ },
96
+ "peerDependenciesMeta": {
97
+ "react": {
98
+ "optional": true
99
+ },
100
+ "react-native": {
101
+ "optional": true
102
+ }
103
+ },
104
+ "publishConfig": {
105
+ "access": "public",
106
+ "registry": "https://registry.npmjs.org/"
107
+ }
108
+ }
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: sdk-rn-core
3
+ description: "Shared foundation of the @chipmobilesdk packages via @chipmobilesdk/rn-core: SdkError and isSdkError for every package's errors, createRedactor, createListenerSet and createEmitter, installSdkSink for one diagnostic stream from every package, redactionConformance. Use when handling or logging errors from several SDK packages, wiring crash reporting or analytics to the SDK packages, or writing redaction or listeners for a package in this family."
4
+ metadata:
5
+ package: "@chipmobilesdk/rn-core"
6
+ user-invocable: true
7
+ disable-model-invocation: false
8
+ ---
9
+
10
+ # @chipmobilesdk/rn-core
11
+
12
+ One error model and one redaction mechanism for every `@chipmobilesdk` package.
13
+ Pure TypeScript; every other package depends on it.
14
+
15
+ ## Use this when
16
+
17
+ - Handling or logging errors from several SDK packages in one place
18
+ - Deciding whether a failed SDK call is worth retrying
19
+ - Writing the redaction for a package in this family, or testing it
20
+ - Writing a conformance suite for a port, so a fake and the real adapter pass the same checks
21
+
22
+ ## Errors
23
+
24
+ Every package's error class extends `SdkError`. Branch on `error.code` and
25
+ `error.retryable`; log `error.pkg`, `error.code` and `error.redactedCause`, never
26
+ the message of a foreign error. Use `isSdkError`, not `instanceof`: it works across
27
+ two copies of the package. README: `## Errors`.
28
+
29
+ ## Redaction
30
+
31
+ Default to `createRedactor()` (identifier-only). Use `text: 'patterns'` only for
32
+ text that describes a failure, never for text that may describe a person. Every
33
+ redaction entrypoint must pass `redactionConformance` from
34
+ `@chipmobilesdk/rn-core/testing`. README: `## Redaction`, `## Testing`.
35
+
36
+ ## Listeners and diagnostics
37
+
38
+ Notify subscribers with `createListenerSet`, never a hand-written `Set` loop: it
39
+ isolates a throwing listener and reports it. To get every SDK package's
40
+ diagnostics, call `installSdkSink` once at startup, before creating instances.
41
+ README: `## Listeners`, `## Diagnostics`.
42
+
43
+ ## Time and retries
44
+
45
+ Never call `setTimeout` directly in a package: take a `Scheduler` so tests can drive
46
+ it (`createManualScheduler`), and track owned timers in `createTimerSet`. Validate
47
+ config with `checkRange`/`checkInteger` and the package's own error code.
48
+ README: `## Scheduling and bounds`.
49
+
50
+ ## Native peers and platform
51
+
52
+ Load a native peer with `lazyPeer`, never a bare `require` at module level or in a
53
+ factory. The loader keeps its literal `require` inside a `try` block, or Metro cannot
54
+ bundle an app without that optional peer. Ask `currentPlatform()` instead of reading `Platform.OS`. Persist through a
55
+ `KeyValuePort` from `openDurable` and report a non-durable fallback. Hear the app's
56
+ lifecycle through `appLifecycle()` (or `onForeground`), never `AppState` directly, so
57
+ every package shares one subscription.
58
+ README: `## Native peers, storage and runtime`.
59
+
60
+ ## React hooks
61
+
62
+ Read package state with `useExternalStore` and a selector, never `useState` plus a
63
+ subscribing `useEffect`. Keep snapshots stable (`createStore`, structural sharing).
64
+ README: `## React`.
65
+
66
+ ## Intl
67
+
68
+ Never construct `Intl.NumberFormat` or `Intl.DateTimeFormat` in a hot path: use
69
+ `getNumberFormat`/`getDateTimeFormat`, and catch what they throw for a bad option.
70
+ README: `## Intl`.
71
+
72
+ ## Messages
73
+
74
+ Declare user-facing copy with `defineMessages`, keeping developer copy separate; an
75
+ app registers the `user` catalog with rn-i18n. README: `## Messages`.
76
+
77
+ ## Public API
78
+
79
+ Runtime:
80
+
81
+ - `SdkError`, `isSdkError`, `defineErrorCodes`, `createIssueCollector`
82
+ - `createRedactor`, `redactPatterns`, `leadingIdentifier`, `describeShape`, `pickAllowed`
83
+ - `REDACTED`, `COMMON_PATTERNS`, `PATH_PATTERN`
84
+ - `createListenerSet`, `createEmitter`, `reportAsynchronously`
85
+ - `installSdkSink`, `hasSdkSinks`, `createDiagnosticChannel`
86
+ - `realScheduler`, `createTimerSet`, `computeBackoff`, `runWithRetry`, `withTimeout`
87
+ - `createSingleFlight`, `createExclusive`, `createRateLimiter`, `checkRange`, `checkInteger`
88
+ - `lazyPeer`, `openDurable`, `fromMmkvLike`, `createMemoryKeyValue`, `namespaced`
89
+ - `currentPlatform`, `setPlatformOverride`, `byPlatform`, `isDevBuild`
90
+ - `appLifecycle`, `onForeground`, `createAppLifecycle` (+ `AppLifecyclePort`,
91
+ `AppLifecycleState`, `AppStateSource`)
92
+ - `createStore`, `shallowEqual`
93
+ - `getNumberFormat`, `getDateTimeFormat`, `isValidTimeZone`, `clearIntlCaches`
94
+ - `defineMessages`, `formatMessage`
95
+
96
+ React (`@chipmobilesdk/rn-core/react`):
97
+
98
+ - `useExternalStore`, `useLatestCallback`, `createSdkContext`
99
+
100
+ Types:
101
+
102
+ - `SdkErrorOptions`, `ErrorCodes`, `ConfigIssue`, `RedactPolicy`, `Redactor`
103
+ - `ListenerSet`, `ListenerErrorOptions`, `Emitter`, `EmitterOptions`, `DedupeOptions`, `Unsubscribe`
104
+ - `SdkDiagnostic`, `DiagnosticSink`, `DiagnosticSeverity`, `DiagnosticChannel`, `DiagnosticChannelOptions`
105
+ - `Scheduler`, `TimerSet`, `BackoffSpec`, `RetryHandle`, `RetryOptions`, `ExclusiveOutcome`, `Range`
106
+ - `LazyPeer`, `PeerResult`, `KeyValuePort`, `MmkvLike`, `RuntimePlatform`, `BuildEnvironment`
107
+
108
+ Testing (`@chipmobilesdk/rn-core/testing`):
109
+
110
+ - `redactionConformance`, `REDACTION_PROBES`, `createManualScheduler`
111
+ - `createManualLifecycle` — a lifecycle a test moves by hand
112
+ - `runChecks`, `expectConformance`, `ensure`, `createFaultRegistry` — run one list of
113
+ port checks against a fake and the real adapter (README `### Port conformance`)
114
+ - `ConformanceReport`, `ConformanceFailure`, `RedactionProbe`, `ManualScheduler`, `Check`,
115
+ `FaultRegistry`
@@ -0,0 +1,110 @@
1
+ /**
2
+ * One diagnostic stream for every package (CORE-08).
3
+ *
4
+ * Each package keeps its own typed events (`AdEvent`, `PaymentEvent`...), and also
5
+ * reports what it observed as an `SdkDiagnostic` through a channel. An app installs
6
+ * one `DiagnosticSink` and receives every package's diagnostics in one shape, so
7
+ * wiring crash reporting or analytics is one adapter rather than ten.
8
+ *
9
+ * Sinks live in a registry anchored on `Symbol.for`, so a sink installed through
10
+ * one copy of rn-core also receives diagnostics from packages bundling another.
11
+ * Every context passes through a redactor before any sink sees it.
12
+ */
13
+ import type { Unsubscribe } from '../listeners';
14
+ import { reportAsynchronously } from '../listeners';
15
+ import { createRedactor, type Redactor } from '../redact';
16
+
17
+ export type DiagnosticSeverity = 'debug' | 'info' | 'warn' | 'error';
18
+
19
+ export interface SdkDiagnostic<Code extends string = string> {
20
+ /** The package that reported it, e.g. `rn-payment`. */
21
+ readonly pkg: string;
22
+ /** That package's stable diagnostic or event code. */
23
+ readonly code: Code;
24
+ readonly severity: DiagnosticSeverity;
25
+ /** Epoch milliseconds. */
26
+ readonly at: number;
27
+ /** Ties together everything reported within one operation, when the package has one. */
28
+ readonly correlationId?: string;
29
+ /** Already redacted. */
30
+ readonly context?: Readonly<Record<string, unknown>>;
31
+ }
32
+
33
+ export type DiagnosticSink = (diagnostic: SdkDiagnostic) => void;
34
+
35
+ const REGISTRY = Symbol.for('chipmobilesdk.diagnosticSinks');
36
+
37
+ function sinks(): Set<DiagnosticSink> {
38
+ const host = globalThis as { [REGISTRY]?: Set<DiagnosticSink> };
39
+ host[REGISTRY] ??= new Set();
40
+ return host[REGISTRY];
41
+ }
42
+
43
+ /**
44
+ * Sends every package's diagnostics to `sink` until the returned function is
45
+ * called. A sink that throws is reported on a fresh task and never affects the
46
+ * package that emitted.
47
+ */
48
+ export function installSdkSink(sink: DiagnosticSink): Unsubscribe {
49
+ const registry = sinks();
50
+ registry.add(sink);
51
+ let active = true;
52
+ return () => {
53
+ if (!active) return;
54
+ active = false;
55
+ registry.delete(sink);
56
+ };
57
+ }
58
+
59
+ /** Whether any sink is installed, for a package that would do extra work only to feed one. */
60
+ export function hasSdkSinks(): boolean {
61
+ return sinks().size > 0;
62
+ }
63
+
64
+ export interface DiagnosticChannelOptions {
65
+ /** A package-level sink, in addition to every installed one. */
66
+ readonly sink?: DiagnosticSink | undefined;
67
+ /** Defaults to rn-core's identifier-only redactor. */
68
+ readonly redactor?: Redactor | undefined;
69
+ readonly now?: (() => number) | undefined;
70
+ }
71
+
72
+ export interface DiagnosticChannel<Code extends string> {
73
+ emit(
74
+ code: Code,
75
+ severity: DiagnosticSeverity,
76
+ context?: Readonly<Record<string, unknown>>,
77
+ correlationId?: string,
78
+ ): void;
79
+ }
80
+
81
+ export function createDiagnosticChannel<Code extends string = string>(
82
+ pkg: string,
83
+ options: DiagnosticChannelOptions = {},
84
+ ): DiagnosticChannel<Code> {
85
+ const redactor = options.redactor ?? createRedactor();
86
+ const now = options.now ?? Date.now;
87
+
88
+ return {
89
+ emit(code, severity, context, correlationId) {
90
+ const targets = options.sink ? [options.sink, ...sinks()] : [...sinks()];
91
+ if (targets.length === 0) return;
92
+ const redacted = redactor.context(context);
93
+ const diagnostic: SdkDiagnostic<Code> = {
94
+ pkg,
95
+ code,
96
+ severity,
97
+ at: now(),
98
+ ...(correlationId === undefined ? {} : { correlationId }),
99
+ ...(redacted === undefined ? {} : { context: redacted }),
100
+ };
101
+ for (const sink of targets) {
102
+ try {
103
+ sink(diagnostic);
104
+ } catch (error) {
105
+ reportAsynchronously(error);
106
+ }
107
+ }
108
+ },
109
+ };
110
+ }