@chipmobilesdk/rn-core 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +255 -2
- package/package.json +107 -5
- package/skills/sdk-rn-core/SKILL.md +115 -0
- package/src/diagnostics/index.ts +110 -0
- package/src/errors/index.ts +105 -0
- package/src/index.ts +105 -0
- package/src/intl/index.ts +99 -0
- package/src/kv/index.ts +98 -0
- package/src/lazy/index.ts +67 -0
- package/src/listeners/index.ts +153 -0
- package/src/messages/index.ts +65 -0
- package/src/react/index.ts +89 -0
- package/src/redact/index.ts +176 -0
- package/src/runtime/index.ts +70 -0
- package/src/runtime/lifecycle.ts +133 -0
- package/src/scheduler/index.ts +278 -0
- package/src/store/index.ts +43 -0
- package/src/testing/conformance.ts +85 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/lifecycle.ts +41 -0
- package/src/testing/redaction.ts +113 -0
- package/src/testing/scheduler.ts +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @chipmobilesdk/rn-core
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- First release: the shared foundation of the @chipmobilesdk packages. `SdkError` and
|
|
8
|
+
`isSdkError` give every package one error model (`pkg`, `code`, `retryable`,
|
|
9
|
+
`redactedCause`), recognised across two copies of the package; `defineErrorCodes`
|
|
10
|
+
and `createIssueCollector` declare codes and collect config problems.
|
|
11
|
+
`createRedactor` and its primitives give one redaction mechanism with
|
|
12
|
+
identifier-only and pattern policies. `createListenerSet` and `createEmitter` notify
|
|
13
|
+
subscribers in isolation (a throwing listener is reported, never swallowed, and never
|
|
14
|
+
reaches the notifier), with a bounded dedupe. `installSdkSink` receives every
|
|
15
|
+
package's diagnostics as one `SdkDiagnostic` stream, across copies of the package.
|
|
16
|
+
`redactionConformance`
|
|
17
|
+
(`@chipmobilesdk/rn-core/testing`) checks any redaction entrypoint against a fixed set
|
|
18
|
+
of secrets. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
19
|
+
|
|
20
|
+
- Port conformance suites: one list of checks that a package's fake and its real adapter
|
|
21
|
+
both pass, so a test written against the fake proves something about the device. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
22
|
+
|
|
23
|
+
- rn-core: `@chipmobilesdk/rn-core/testing` adds `runChecks`, `expectConformance`,
|
|
24
|
+
`ensure` and `createFaultRegistry` (deterministic fault injection by named point).
|
|
25
|
+
- rn-camera: a `./testing` entrypoint with `runPermissionPortConformance`,
|
|
26
|
+
`runCameraEngineConformance` and the test adapter, which stays on the main
|
|
27
|
+
entrypoint too until the next major. `createTestCamera(...).prompts()` counts system
|
|
28
|
+
prompts.
|
|
29
|
+
- rn-payment: `runProviderPortConformance` in `./testing`, passed by the fake store and
|
|
30
|
+
the Play adapter.
|
|
31
|
+
- rn-ads: `runAdsEngineConformance` in `./testing`, passed by the fake engine and the
|
|
32
|
+
AdMob adapter.
|
|
33
|
+
- rn-auth: a `./testing` entrypoint with `createMemorySecureStore`,
|
|
34
|
+
`runSecureTokenStoreConformance` and `runOidcAuthClientConformance`, passed by the
|
|
35
|
+
in-memory doubles and the keychain and app-auth adapters.
|
|
36
|
+
|
|
37
|
+
- One app-lifecycle source, and a startup that needs no extra frame. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
38
|
+
|
|
39
|
+
- rn-core: `appLifecycle()`, a shared, reference-counted `AppState` subscription that
|
|
40
|
+
reports changes only (`active`, `inactive`, `background`), with `onForeground` for a
|
|
41
|
+
return from the background and `createManualLifecycle` in `./testing`.
|
|
42
|
+
- rn-camera, rn-ads and rn-notification hear the lifecycle through it instead of
|
|
43
|
+
subscribing to `AppState` each, so an app holds one native listener. The camera
|
|
44
|
+
controller's core layer no longer imports `react-native`.
|
|
45
|
+
- rn-payment: `syncOnForeground(engine)` and `<PaymentProvider syncOnForeground>` call
|
|
46
|
+
`handleForeground()` on each return from the background. Off by default; calling
|
|
47
|
+
`handleForeground()` yourself works as before.
|
|
48
|
+
- rn-i18n: `i18n.initSync()` resolves the locale before the first render when the
|
|
49
|
+
storage adapter has the new optional `getItemSync`, and returns false otherwise.
|
|
50
|
+
`localeStorageFromKeyValue(port)` builds such an adapter from rn-core's
|
|
51
|
+
`KeyValuePort` (MMKV).
|
|
52
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,256 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @chipmobilesdk/rn-core
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@chipmobilesdk/rn-core",
|
|
3
|
+
"version": "0.1.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
|
+
}
|