@fairfox/polly 0.58.0 → 0.60.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.
@@ -23,6 +23,7 @@ import { Repo, type StorageAdapterInterface } from "@automerge/automerge-repo/sl
23
23
  import type { KeyringStorage } from "./keyring-storage";
24
24
  import { type MeshKeyring, MeshNetworkAdapter } from "./mesh-network-adapter";
25
25
  import { MeshSignalingClient, type MeshSignalingClientOptions } from "./mesh-signaling-client";
26
+ import { type MeshStateLazyWrapperRecord, type MeshStateLoadedRejectionBreadcrumb } from "./mesh-state";
26
27
  import { MeshWebRTCAdapter, type MeshWebRTCAdapterOptions } from "./mesh-webrtc-adapter";
27
28
  /** Options for {@link createMeshClient}. */
28
29
  export interface CreateMeshClientOptions {
@@ -232,6 +233,42 @@ export interface MeshStateModuleDiagnostics {
232
233
  * true` (they're using a module instance no mesh client ever
233
234
  * configured). */
234
235
  wasResolved: boolean;
236
+ /** Polly#107 post-H5 instrumentation. Count of `$meshState`-family
237
+ * lazy factory invocations since module load. Once the consumer's
238
+ * pre-warm pass completes, this equals the number of distinct
239
+ * `$mesh*` wrappers whose handles the loader actually tried to
240
+ * resolve. Compared to {@link lazyReachedRepo}: gap = throws
241
+ * before any Repo work. */
242
+ lazyInvocations: number;
243
+ /** Polly#107 post-H5 instrumentation. Count of factory invocations
244
+ * that reached the Repo subsystem (`repo.handles[...]`, `repo.find`
245
+ * or `repo.import`) before returning or throwing. If
246
+ * {@link lazyInvocations} is N and this is N, every wrapper
247
+ * touched the Repo — any missing handles are downstream of Repo
248
+ * registration. If this is < N, the gap is the call site to
249
+ * instrument next. */
250
+ lazyReachedRepo: number;
251
+ /** Polly#107 post-H5 instrumentation. Breadcrumb for the most
252
+ * recent rejection from a `$meshState`-family `loaded` promise
253
+ * (or its factory). Captured even when the consumer's wrapper
254
+ * never awaits `loaded` and the rejection would otherwise vanish
255
+ * silently. `undefined` means no rejection has escaped any
256
+ * wrapper on THIS module instance since module load. */
257
+ lastLoadedRejection: MeshStateLoadedRejectionBreadcrumb | undefined;
258
+ /** Polly#107 post-v0.59 instrumentation. Per-factory-invocation
259
+ * structured log — one record per `$mesh*` wrapper's lazy handle
260
+ * factory call, ring-buffered at 64 entries. Each row names the
261
+ * exit path (`returned-cached`, `loaded-from-storage`,
262
+ * `seeded-and-imported`, `threw`) and the synchronous peek at
263
+ * `repo.handles[docId]` taken in the factory's `finally`. The
264
+ * v0.59.0 fingerprint had `lazyInvocations === lazyReachedRepo`
265
+ * and no `lastLoadedRejection`; this field disambiguates which
266
+ * exit path each "successful" invocation actually took and
267
+ * whether the Repo registered the handle in spite of the lack of
268
+ * a thrown error. The smoking gun for H-Q1 is rows with
269
+ * `exitReason: "seeded-and-imported"` and
270
+ * `handleRegistered: false`. */
271
+ lazyWrappers: MeshStateLazyWrapperRecord[];
235
272
  }
236
273
  /** The mesh client's enriched per-peer state snapshot. Mirrors the
237
274
  * underlying {@link MeshWebRTCAdapter.getPeerStateSnapshot} shape but
@@ -113,6 +113,96 @@ export declare function isMeshStateConfigured(): boolean;
113
113
  * called against this module instance. See
114
114
  * {@link meshStateEverResolved}. */
115
115
  export declare function wasMeshStateResolved(): boolean;
116
+ /** Returns the count of factory invocations since module load. See
117
+ * {@link lazyInvocations}. */
118
+ export declare function getLazyInvocations(): number;
119
+ /** Returns the count of factory invocations that reached
120
+ * `repo.find` / `repo.import` since module load. See
121
+ * {@link lazyReachedRepo}. */
122
+ export declare function getLazyReachedRepo(): number;
123
+ /** Polly#107 post-H5 instrumentation. Records the most recent
124
+ * rejection (or synchronous throw) escaping the factory body — the
125
+ * `loaded` promise's rejection path. Today an unawaited rejection
126
+ * inside a consumer wrapper vanishes without trace; capturing the
127
+ * message + stack here on the module and exposing it via the
128
+ * snapshot leaves a breadcrumb the operator can read in one paste.
129
+ *
130
+ * The format is the JSON-safe `{ name, message, stack, at }` shape
131
+ * so the snapshot read does not have to traffic in `Error`
132
+ * instances. `at` is `Date.now()` at the time the rejection was
133
+ * captured. */
134
+ export interface MeshStateLoadedRejectionBreadcrumb {
135
+ name: string;
136
+ message: string;
137
+ stack: string | undefined;
138
+ at: number;
139
+ }
140
+ /** Returns the most recent rejection escaping a `$meshState` factory
141
+ * invocation since module load. See {@link lastLoadedRejection}. */
142
+ export declare function getLastLoadedRejection(): MeshStateLoadedRejectionBreadcrumb | undefined;
143
+ /** Polly#107 post-v0.59 instrumentation. Categorises the exit path
144
+ * a `$mesh*` lazy factory invocation took. The v0.59.0 fingerprint
145
+ * showed `lazyInvocations === lazyReachedRepo === 17` and no
146
+ * `lastLoadedRejection` — every factory reached the first
147
+ * `repo.handles[docId]` access, none rejected loudly, yet 16 of 17
148
+ * handles never landed in `repo.handles`. Without per-exit detail
149
+ * the snapshot cannot disambiguate "factory returned the cached
150
+ * handle", "factory hydrated from storage", "factory seeded and
151
+ * imported a fresh doc", or "factory took the cached branch but
152
+ * `whenReady` resolved to `unavailable` and the code fell
153
+ * through". Each exit reason corresponds to a single line in
154
+ * {@link buildHandleFactory}; the snapshot record names it
155
+ * verbatim so the operator can grep the source from one read. */
156
+ export type LazyWrapperExitReason = "returned-cached" | "loaded-from-storage" | "seeded-and-imported" | "threw";
157
+ /** Polly#107 post-v0.59 instrumentation. One record per factory
158
+ * invocation, ring-buffered on the module and surfaced through
159
+ * `MeshClient.getPeerStateSnapshot()` as
160
+ * `meshStateModule.lazyWrappers`. The five fields together answer
161
+ * the post-v0.59 question "if every factory reached the Repo and
162
+ * nothing rejected, why are 16 of 17 handles absent from
163
+ * `repo.handles`?" — `exitReason` names which of the three success
164
+ * paths each took, `handleRegistered` is the synchronous peek at
165
+ * `repo.handles[docId]` taken in the factory's `finally` (i.e. at
166
+ * the moment of would-be return), and `handleState` is the
167
+ * lifecycle state observed in that peek. A row where
168
+ * `exitReason: "seeded-and-imported"` and `handleRegistered: false`
169
+ * is the smoking gun for H-Q1 (`repo.import` returned without
170
+ * registering); rows where `exitReason: "returned-cached"` repeats
171
+ * for sixteen of seventeen distinct keys indicates collisions or
172
+ * that the factory is being called multiple times against a
173
+ * Repo-state-machine that has already filled the slot. */
174
+ export interface MeshStateLazyWrapperRecord {
175
+ /** The logical application key passed to `$meshState(key, ...)`. */
176
+ key: string;
177
+ /** Stringified Automerge `DocumentId` derived from the key. */
178
+ docId: string;
179
+ /** `Date.now()` captured at the moment the factory was about to
180
+ * return (or rethrow). */
181
+ at: number;
182
+ /** Which of the four exit paths the factory took. See
183
+ * {@link LazyWrapperExitReason}. */
184
+ exitReason: LazyWrapperExitReason;
185
+ /** Snapshot peek `repo.handles[docId] !== undefined` taken in the
186
+ * factory's `finally` clause. `true` means the local Repo
187
+ * registered the handle; `false` means the factory thinks it
188
+ * succeeded but the Repo does not hold the handle for this
189
+ * documentId. The "polly#107 post-v0.59" smoking gun is
190
+ * `exitReason: "seeded-and-imported", handleRegistered: false`. */
191
+ handleRegistered: boolean;
192
+ /** Lifecycle state of `repo.handles[docId]` at the synchronous
193
+ * peek time. `undefined` when the handle was not registered.
194
+ * Useful for distinguishing the "registered as loading/unavailable"
195
+ * case from the "registered as ready" case. */
196
+ handleState: string | undefined;
197
+ /** Error message if `exitReason === "threw"`; `undefined`
198
+ * otherwise. The full rejection still flows into
199
+ * {@link lastLoadedRejection}; this field is the row-local
200
+ * summary so the snapshot can show the failure cause inline. */
201
+ errorMessage: string | undefined;
202
+ }
203
+ /** Returns a copy of the lazy-wrapper invocation log. See
204
+ * {@link MeshStateLazyWrapperRecord}. */
205
+ export declare function getLazyWrappers(): MeshStateLazyWrapperRecord[];
116
206
  /**
117
207
  * Create a peer-replicated state primitive backed by Automerge with a mesh
118
208
  * transport. Every device holds a full replica; no central server holds a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairfox/polly",
3
- "version": "0.58.0",
3
+ "version": "0.60.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Multi-execution-context framework with reactive state and cross-context messaging for Chrome extensions, PWAs, and worker-based applications",