@keepsnappy/react-native 0.1.0-alpha.15 → 0.1.0-alpha.17

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/js/sdk/push.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  // rewritten to the live queue whenever it outgrows twice the queue caps, so it
12
12
  // stays bounded while the ingest is unreachable.
13
13
 
14
+ import { MAX_BATCH_BYTES, utf8Length } from './bytes';
15
+
14
16
  export interface DeviceInfo {
15
17
  model: string | null;
16
18
  os: string | null;
@@ -113,6 +115,8 @@ const serverClock = async (res: { json?: () => Promise<unknown> }, bound: Promis
113
115
  interface Item {
114
116
  line: string;
115
117
  bytes: number;
118
+ // The line's size inside the request body: a JSON string (escaped) plus its comma.
119
+ wire: number;
116
120
  token: string | null;
117
121
  // The process that recorded the line (its bootId) and its sequence there.
118
122
  origin: string;
@@ -125,7 +129,7 @@ interface Batch {
125
129
  id: string;
126
130
  items: Item[];
127
131
  token: string | null;
128
- bytes: number;
132
+ wire: number;
129
133
  }
130
134
 
131
135
  interface SpoolAdd {
@@ -208,31 +212,15 @@ export const readSpool = (content: string | null): { pending: PendingLine[]; dro
208
212
  export const pendingFromSpool = (content: string | null): PendingLine[] =>
209
213
  readSpool(content).pending;
210
214
 
211
- const utf8Length = (text: string) => {
212
- let n = 0;
213
- for (let i = 0; i < text.length; i++) {
214
- const c = text.charCodeAt(i);
215
- if (c < 0x80) {
216
- n += 1;
217
- } else if (c < 0x800) {
218
- n += 2;
219
- } else if (c >= 0xd800 && c <= 0xdbff) {
220
- n += 4;
221
- i++;
222
- } else {
223
- n += 3;
224
- }
225
- }
226
- return n;
227
- };
228
-
229
215
  // Retrying cannot fix these; the batch is dropped instead of blocking the queue.
216
+ const wireBytes = (line: string) => utf8Length(JSON.stringify(line)) + 1;
217
+
230
218
  const isPermanent = (status: number) =>
231
219
  status >= 400 && status < 500 && status !== 408 && status !== 429;
232
220
 
233
221
  export const createPushClient = (deps: PushDeps) => {
234
222
  const idleFlushMs = deps.idleFlushMs ?? 10_000;
235
- const maxBatchBytes = deps.maxBatchBytes ?? 256 * 1024;
223
+ const maxBatchBytes = deps.maxBatchBytes ?? MAX_BATCH_BYTES;
236
224
  const maxBatchItems = deps.maxBatchItems ?? 400;
237
225
  const maxQueueLines = deps.maxQueueLines ?? 2000;
238
226
  const maxQueueBytes = deps.maxQueueBytes ?? 8 * 1024 * 1024;
@@ -277,6 +265,11 @@ export const createPushClient = (deps: PushDeps) => {
277
265
  let journalRecords = 0;
278
266
  // Lines dropped before delivery, by this process and (from the note) earlier ones.
279
267
  let undelivered = 0;
268
+ // Lines of a run dropped before delivery (queue cap, line over the batch limit), by run token, until its next flush.
269
+ const droppedOfRun = new Map<string, number>();
270
+ const noteDropped = (token: string | null) => {
271
+ if (token !== null) droppedOfRun.set(token, (droppedOfRun.get(token) ?? 0) + 1);
272
+ };
280
273
  // Round trips not reported yet, each with the run token it was measured under.
281
274
  let clockPending: { token: string; sample: ClockSample }[] = [];
282
275
  const clockOf = (token: string | null) =>
@@ -440,6 +433,7 @@ export const createPushClient = (deps: PushDeps) => {
440
433
  }
441
434
  }
442
435
  undelivered += dropped.length;
436
+ for (const item of dropped) noteDropped(item.token);
443
437
  return dropped;
444
438
  };
445
439
 
@@ -448,12 +442,14 @@ export const createPushClient = (deps: PushDeps) => {
448
442
  if (bytes > maxBatchBytes) {
449
443
  stats.dropped++;
450
444
  undelivered++;
445
+ noteDropped(deps.getRunToken());
451
446
  stats.lastError = `line of ${bytes} bytes exceeds batch limit`;
452
447
  return;
453
448
  }
454
449
  const item: Item = {
455
450
  line,
456
451
  bytes,
452
+ wire: wireBytes(line),
457
453
  token: deps.getRunToken(),
458
454
  origin: bootId,
459
455
  seq: nextSeq++,
@@ -502,11 +498,13 @@ export const createPushClient = (deps: PushDeps) => {
502
498
  if (bytes > maxBatchBytes) {
503
499
  stats.resumeDropped++;
504
500
  undelivered++;
501
+ noteDropped(record.t);
505
502
  continue;
506
503
  }
507
504
  earlier.push({
508
505
  line: record.l,
509
506
  bytes,
507
+ wire: wireBytes(record.l),
510
508
  token: record.t,
511
509
  origin: record.o,
512
510
  seq: record.s,
@@ -544,16 +542,16 @@ export const createPushClient = (deps: PushDeps) => {
544
542
  }
545
543
  const first = queue[0] as Item;
546
544
  const items: Item[] = [];
547
- let bytes = 0;
545
+ let wire = 0;
548
546
  if (first.batch !== undefined) {
549
547
  // Already sent once (by a killed process): resend exactly that batch.
550
548
  while (queue.length > 0 && queue[0]?.batch === first.batch) {
551
549
  const next = queue.shift() as Item;
552
550
  queuedBytes -= next.bytes;
553
551
  items.push(next);
554
- bytes += next.bytes;
552
+ wire += next.wire;
555
553
  }
556
- return { id: first.batch, items, token: first.token, bytes };
554
+ return { id: first.batch, items, token: first.token, wire };
557
555
  }
558
556
  while (queue.length > 0 && items.length < maxBatchItems) {
559
557
  const next = queue[0];
@@ -562,16 +560,16 @@ export const createPushClient = (deps: PushDeps) => {
562
560
  next.batch !== undefined ||
563
561
  next.token !== first.token ||
564
562
  next.origin !== first.origin ||
565
- (items.length > 0 && bytes + next.bytes > maxBatchBytes)
563
+ (items.length > 0 && wire + next.wire > maxBatchBytes)
566
564
  ) {
567
565
  break;
568
566
  }
569
567
  queue.shift();
570
568
  queuedBytes -= next.bytes;
571
569
  items.push(next);
572
- bytes += next.bytes;
570
+ wire += next.wire;
573
571
  }
574
- return { id: '', items, token: first.token, bytes };
572
+ return { id: '', items, token: first.token, wire };
575
573
  };
576
574
 
577
575
  let inflight: Promise<void> | null = null;
@@ -617,29 +615,48 @@ export const createPushClient = (deps: PushDeps) => {
617
615
  bound.clear();
618
616
  }
619
617
  }
618
+ const runToken = batch.token;
619
+ const reported = clockOf(runToken);
620
+ const sentDevice = device;
621
+ const bodyOf = (id: string) => JSON.stringify({
622
+ batch_id: id,
623
+ device: sentDevice,
624
+ items: batch.items.map(item => item.line),
625
+ ...(runToken ? { run_token: runToken } : {}),
626
+ ...(reported.length ? { clock: reported } : {}),
627
+ });
628
+ // Names exactly these lines (process, first and last sequence, count),
629
+ // so a different set of lines is always a different batch.
630
+ const idOf = () => {
631
+ const first = batch.items[0] as Item;
632
+ const last = batch.items[batch.items.length - 1] as Item;
633
+ return `${sentDevice.install_id}-${first.origin}-${first.seq}-${last.seq}-${batch.items.length}`;
634
+ };
635
+ let body = bodyOf(batch.id || idOf());
620
636
  if (!batch.id) {
621
- // Names exactly these lines (process, first and last sequence, count),
622
- // so a different set of lines is always a different batch. It is
623
- // journalled before the first send: a later process that resends after
637
+ // The cap is on the body as sent (escaped lines, device, clock samples): lines over it go back to the queue
638
+ // front for the next batch. A lone line still goes; a line alone over the cap was dropped at enqueue.
639
+ for (let over = utf8Length(body) - maxBatchBytes; over > 0 && batch.items.length > 1; over = utf8Length(body) - maxBatchBytes) {
640
+ const back: Item[] = [];
641
+ for (let freed = 0; freed < over && batch.items.length > 1;) {
642
+ const item = batch.items.pop() as Item;
643
+ freed += item.wire;
644
+ back.unshift(item);
645
+ }
646
+ queue.unshift(...back);
647
+ for (const item of back) queuedBytes += item.bytes;
648
+ body = bodyOf(idOf());
649
+ }
650
+ // Journalled before the first send: a later process that resends after
624
651
  // a kill sends the same lines under the same id, and the server's
625
652
  // replay check can only ever drop lines it already has.
653
+ batch.id = idOf();
626
654
  const first = batch.items[0] as Item;
627
- const last = batch.items[batch.items.length - 1] as Item;
628
- batch.id = `${device.install_id}-${first.origin}-${first.seq}-${last.seq}-${batch.items.length}`;
629
655
  for (const item of batch.items) {
630
656
  item.batch = batch.id;
631
657
  }
632
658
  journal({ b: batch.id, o: first.origin, s: batch.items.map(item => item.seq) });
633
659
  }
634
- const runToken = batch.token;
635
- const reported = clockOf(runToken);
636
- const body = JSON.stringify({
637
- batch_id: batch.id,
638
- device,
639
- items: batch.items.map(item => item.line),
640
- ...(runToken ? { run_token: runToken } : {}),
641
- ...(reported.length ? { clock: reported } : {}),
642
- });
643
660
  const headers: Record<string, string> = {
644
661
  'content-type': 'application/json',
645
662
  'x-keepsnappy-key': deps.key,
@@ -820,16 +837,23 @@ export const createPushClient = (deps: PushDeps) => {
820
837
  await sendClock(deadline);
821
838
  }
822
839
  const refused = refusedRunBatches - refusedBefore;
823
- const ok = sentAll && refused === 0;
840
+ // A run that lost lines before they were sent is missing windows, whatever went up after.
841
+ const token = deps.getRunToken();
842
+ const lost = token === null ? 0 : (droppedOfRun.get(token) ?? 0);
843
+ if (token !== null) droppedOfRun.delete(token);
844
+ const ok = sentAll && refused === 0 && lost === 0;
845
+ const error = ok
846
+ ? null
847
+ : !sentAll
848
+ ? (stats.lastError ?? 'flush deadline')
849
+ : refused > 0
850
+ ? `rejected ${lastRefusedStatus}: ${refused} ${refused === 1 ? 'batch' : 'batches'} of the run dropped`
851
+ : 'lines of the run dropped before sending';
824
852
  return {
825
853
  ok,
826
854
  batches: stats.batchesSent - sentBefore,
827
855
  lines: stats.linesSent - linesBefore,
828
- error: ok
829
- ? null
830
- : !sentAll
831
- ? (stats.lastError ?? 'flush deadline')
832
- : `rejected ${lastRefusedStatus}: ${refused} ${refused === 1 ? 'batch' : 'batches'} of the run dropped`,
856
+ error: error !== null && lost > 0 ? `${error} dropped=${lost}` : error,
833
857
  };
834
858
  } finally {
835
859
  flushLimit = null;
package/js/sdk/sampler.ts CHANGED
@@ -132,7 +132,7 @@ export const estimateOffset = (before: number, native: number, after: number) =>
132
132
  // late after its t0 a window can start and still take one. It gates only screen windows (a screen window opened by a
133
133
  // press is detected after the new screen rendered and takes the press's open sample, taken the touch delivery delay
134
134
  // past the press's t0) and the reuse of a close sample for a restarted key. Press windows do not depend on it: they
135
- // take the previous window's close sample by contiguity. Evidence for 50: idle lab event timing of 4.9-19.1 ms
135
+ // take the previous window's close sample by contiguity, else a sample of their own however late it is. Evidence for 50: idle lab event timing of 4.9-19.1 ms
136
136
  // (packages/derive/test/fixtures/p37-*.log); no loaded-device data. Measure the touchstart delivery delay p99 per
137
137
  // device from farm data (docs/log/followups.md).
138
138
  export const OPEN_SAMPLE_TOLERANCE_MS = 50;
package/js/sdk/screens.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  } from './core';
27
27
  import { retainFloorOf } from './retain-floor';
28
28
  import { type CauseRoot, currentCause, latestActionAt, launchedAt, launchRootId } from './causes';
29
- import { routeOfScreenHost, routes } from './fiber';
29
+ import { buildPlaceId, routeOfScreenHost, routes, screenKeyForRoute, setScreenKeyProvider, currentFiber, type ScreenRouteResolution, type Fiber } from './fiber';
30
30
  import {
31
31
  lastPressStart,
32
32
  type NativeScreenEvent,
@@ -64,6 +64,12 @@ interface Visit {
64
64
  chainKey: string;
65
65
  keyChain: string;
66
66
  visit: number;
67
+ screenKey: string | null;
68
+ legacyName: string;
69
+ // The screen component is not known yet: its scene has not mounted, or it is suspended.
70
+ pending: boolean;
71
+ resolution: ScreenRouteResolution;
72
+ lazyBoundary: Fiber | null;
67
73
  t0: number;
68
74
  openedAt: number;
69
75
  endedAt: number | null;
@@ -89,6 +95,7 @@ interface Resolution {
89
95
  let current: Visit | null = null;
90
96
  let seq = 0;
91
97
  const visitsByChain = new Map<string, number>();
98
+ const visitsByScreen = new Map<string, number>();
92
99
  let lastActionAt = -Infinity;
93
100
  interface AppLink {
94
101
  t: number;
@@ -248,6 +255,7 @@ const endVisit = (reason: string, at?: number) => {
248
255
  if (!visit || visit.endedAt !== null) {
249
256
  return;
250
257
  }
258
+ if (visit.pending) resolveVisit(visit, true);
251
259
  const t = Math.max(visit.t0, at ?? now());
252
260
  visit.endedAt = t;
253
261
  stats.ends[reason] = (stats.ends[reason] ?? 0) + 1;
@@ -262,7 +270,9 @@ const endVisit = (reason: string, at?: number) => {
262
270
  // state event.
263
271
  const chooseStart = (snapshot: NavSnapshot, floor: number) => {
264
272
  const inSpan = (t: number) => t >= floor && t <= snapshot.stateT;
265
- const presses = pressesSinceVisit.filter(p => inSpan(p.t0));
273
+ // Touches delivered since the previous visit opened, whatever their finger time: a finger that landed while the
274
+ // previous screen was mounting reaches JS after its state was read.
275
+ const presses = pressesSinceVisit.filter(p => p.t0 >= windowsEndedAt && p.t0 <= snapshot.stateT);
266
276
  const link = pendingLink && inSpan(pendingLink.t) ? pendingLink : null;
267
277
  const action = snapshot.action && inSpan(snapshot.action.t) ? snapshot.action : null;
268
278
  const touchStart = (p: Press) => ({
@@ -332,6 +342,28 @@ const isStrictExtension = (prev: RoutePart[], next: RoutePart[]) =>
332
342
  (part.key ?? null) === (next[i]?.key ?? null),
333
343
  );
334
344
 
345
+ const screenResolution = (chain: RoutePart[]) => {
346
+ const leaf = chain[chain.length - 1]!;
347
+ return screenKeyForRoute(leaf.key ?? null, leaf.name);
348
+ };
349
+
350
+ const moveScreenCounter = (visit: Visit, screen: string) => {
351
+ if (visit.screenKey === screen) return;
352
+ if (visit.screenKey !== null) visitsByScreen.set(visit.screenKey, (visitsByScreen.get(visit.screenKey) ?? 1) - 1);
353
+ visit.visit = (visitsByScreen.get(screen) ?? 0) + 1;
354
+ visitsByScreen.set(screen, visit.visit);
355
+ visit.screenKey = screen;
356
+ };
357
+
358
+ const resolveVisit = (visit: Visit, final = false) => {
359
+ const resolved = visit.resolution;
360
+ if (resolved.screenPending && !final) return;
361
+ moveScreenCounter(visit, resolved.screen);
362
+ visit.name = buildPlaceId({ kind: 'screen', screen: resolved.screen, visit: visit.visit });
363
+ visit.pending = false;
364
+ markers.rename(visit.key, visit.name, visit.legacyName);
365
+ };
366
+
335
367
  const extend = (visit: Visit, chain: RoutePart[], chainKey: string) => {
336
368
  const previous = visitsByChain.get(visit.chainKey) ?? 1;
337
369
  visitsByChain.set(visit.chainKey, previous - 1);
@@ -341,9 +373,19 @@ const extend = (visit: Visit, chain: RoutePart[], chainKey: string) => {
341
373
  visit.chain = chain;
342
374
  visit.chainKey = chainKey;
343
375
  visit.keyChain = keyChainOf(chain);
344
- visit.visit = n;
345
- visit.name = nameOf(chainKey, n);
346
- markers.rename(visit.key, visit.name);
376
+ visit.legacyName = nameOf(chainKey, n);
377
+ const resolution = screenResolution(chain);
378
+ visit.resolution = resolution;
379
+ visit.lazyBoundary = resolution.suspended ?? null;
380
+ if (resolution.screenPending) {
381
+ if (visit.screenKey !== null) {
382
+ visitsByScreen.set(visit.screenKey, (visitsByScreen.get(visit.screenKey) ?? 1) - 1);
383
+ visit.screenKey = null;
384
+ }
385
+ visit.name = visit.legacyName;
386
+ visit.pending = true;
387
+ markers.rename(visit.key, visit.name, visit.legacyName);
388
+ } else resolveVisit(visit);
347
389
  stats.extended++;
348
390
  };
349
391
 
@@ -398,6 +440,7 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
398
440
  const n = (visitsByChain.get(chainKey) ?? 0) + 1;
399
441
  visitsByChain.set(chainKey, n);
400
442
  seq++;
443
+ const resolved = screenResolution(chain);
401
444
  const visit: Visit = {
402
445
  key: `screen#${seq}`,
403
446
  name: nameOf(chainKey, n),
@@ -405,6 +448,11 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
405
448
  chainKey,
406
449
  keyChain,
407
450
  visit: n,
451
+ legacyName: nameOf(chainKey, n),
452
+ screenKey: null,
453
+ pending: true,
454
+ resolution: resolved,
455
+ lazyBoundary: resolved.suspended ?? null,
408
456
  t0: start.t0,
409
457
  openedAt: detectedAt,
410
458
  endedAt: null,
@@ -417,6 +465,11 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
417
465
  launch,
418
466
  cause: snapshot.cause,
419
467
  };
468
+ if (!resolved.screenPending) {
469
+ moveScreenCounter(visit, resolved.screen);
470
+ visit.name = buildPlaceId({ kind: 'screen', screen: resolved.screen, visit: visit.visit });
471
+ visit.pending = false;
472
+ }
420
473
  current = visit;
421
474
  pressesSinceVisit = [];
422
475
  stats.visits++;
@@ -429,6 +482,12 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
429
482
  markers.start(visit.name, {
430
483
  key: visit.key,
431
484
  kind: 'screen',
485
+ legacyName: visit.legacyName,
486
+ beforeEnd: () => {
487
+ if (visit.pending) resolveVisit(visit, true);
488
+ visit.lazyBoundary = null;
489
+ visit.resolution = { screen: visit.screenKey ?? visit.resolution.screen };
490
+ },
432
491
  at: visit.t0,
433
492
  details: details(visit),
434
493
  // The launch window belongs to the launch, whatever action came before the first state (REQ-MEAS-22).
@@ -470,12 +529,14 @@ export const noteAppLink = (url: string, via: string) => {
470
529
  }
471
530
  };
472
531
 
532
+ // Every touch delivered while a visit is open is an action after the one that opened it, even when its finger landed
533
+ // before the SDK read the navigation state (the JS thread was busy with the mount): order decides, not the clocks.
473
534
  const onPress = (t0: number) => {
474
535
  const press = lastPressStart();
475
536
  if (press) {
476
537
  pressesSinceVisit.push(press);
477
538
  }
478
- if (current && current.endedAt === null && t0 > current.openedAt) {
539
+ if (current && current.endedAt === null) {
479
540
  endVisit('next-action', t0);
480
541
  }
481
542
  };
@@ -530,6 +591,7 @@ export const screenStats = () => ({
530
591
  visitsByChain: Object.fromEntries(
531
592
  [...visitsByChain].filter(([, n]) => n > 0).slice(0, MAX_LISTED),
532
593
  ),
594
+ visitsByScreen: Object.fromEntries([...visitsByScreen].filter(([, n]) => n > 0).slice(0, MAX_LISTED)),
533
595
  trackedRoutes: routes.size,
534
596
  nav: navStats(),
535
597
  native: {
@@ -569,6 +631,10 @@ export const installScreens = () => {
569
631
  return;
570
632
  }
571
633
  installed = true;
634
+ setScreenKeyProvider(() => current ? {
635
+ screen: current.screenKey ?? current.resolution.screen,
636
+ ...(current.pending || current.lazyBoundary ? { screenPending: true } : {}),
637
+ } : { screen: '(no screen)' });
572
638
  setRetainFloor(screenRetainFloor);
573
639
  onWindowsEnd(onWindowsEnded);
574
640
  onNavSnapshot(snapshot => onSnapshot(snapshot, 'state'));
@@ -578,6 +644,25 @@ export const installScreens = () => {
578
644
  setTimeout(() => logNativeEvent(event), RESOLVE_DELAY_MS);
579
645
  });
580
646
  onAfterCommit(() => {
647
+ const visit = current;
648
+ if (visit && isWindowOpen(visit.key)) {
649
+ // A pending visit is resolved when its scene mounts, however many commits come first; it falls back to the route
650
+ // name only when it ends.
651
+ if (visit.pending && !visit.lazyBoundary) {
652
+ visit.resolution = screenResolution(visit.chain);
653
+ visit.lazyBoundary = visit.resolution.suspended ?? null;
654
+ resolveVisit(visit);
655
+ } else if (visit.lazyBoundary) {
656
+ const boundary = currentFiber(visit.lazyBoundary);
657
+ if (!boundary || boundary.memoizedState == null) {
658
+ visit.lazyBoundary = null;
659
+ if (boundary) {
660
+ visit.resolution = screenResolution(visit.chain);
661
+ resolveVisit(visit, true);
662
+ }
663
+ }
664
+ }
665
+ }
581
666
  if (!checkScheduled && containerCount() === 0) {
582
667
  checkScheduled = true;
583
668
  setTimeout(checkRouteProps, 0);
package/js/sdk/stream.ts CHANGED
@@ -39,6 +39,12 @@ export const createStream = (processId: string, maxFibers = MAX_SEGMENT_FIBERS)
39
39
  /** The segment the next commit goes into: windows name it so the backend can tell a stream is complete. */
40
40
  seq: () => seq,
41
41
  size: () => commits.length,
42
+ /** Sets the fiber cap of the segments from now on; returns the previous cap. */
43
+ setMaxFibers(n: number) {
44
+ const previous = maxFibers;
45
+ maxFibers = n;
46
+ return previous;
47
+ },
42
48
  noteRoot(root: CauseRoot) {
43
49
  roots.push(root);
44
50
  },
package/metro.js CHANGED
@@ -175,7 +175,7 @@ const resolveEntry = projectRoot => {
175
175
  return null;
176
176
  };
177
177
 
178
- function withKeepSnappy(input) {
178
+ function withKeepSnappy(input, { clientPackages = [] } = {}) {
179
179
  if (!isEnabled()) {
180
180
  return isProfiling() ? withReactProfiling(input) : input;
181
181
  }
@@ -201,13 +201,23 @@ function withKeepSnappy(input) {
201
201
  process.env[ENV_UPSTREAM] = upstream;
202
202
  process.env[ENV_ENTRY] = entry;
203
203
  process.env[ENV_BOOT] = BOOT_PATH;
204
+ const packages = [...new Set(clientPackages)].sort();
205
+ process.env.KEEPSNAPPY_CLIENT_PACKAGES = JSON.stringify(packages);
206
+ process.env.KEEPSNAPPY_PROJECT_ROOT = projectRoot;
207
+ process.env.KEEPSNAPPY_COMPONENT_CACHE_KEY = require('./js/component-stamps.js').configure(projectRoot, packages);
204
208
 
205
209
  const serializer = config.serializer ?? {};
206
210
  const upstreamPolyfills = serializer.getPolyfills;
207
211
  const upstreamPreMain = serializer.getModulesRunBeforeMainModule;
212
+ const resolver = config.resolver ?? {};
213
+ const blockList = resolver.blockList ? (Array.isArray(resolver.blockList) ? resolver.blockList : [resolver.blockList]) : [];
214
+ const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
215
+ const roots = [...new Set([path.resolve(projectRoot), fs.realpathSync(projectRoot)])];
216
+ const inventory = new RegExp(`^(?:${roots.map(escape).join('|')})[/\\\\]\\.keepsnappy(?:[/\\\\]|$)`);
208
217
 
209
218
  return {
210
219
  ...config,
220
+ resolver: { ...resolver, blockList: [...blockList, inventory] },
211
221
  serializer: {
212
222
  ...serializer,
213
223
  getPolyfills: options => [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepsnappy/react-native",
3
- "version": "0.1.0-alpha.15",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "KeepSnappy in-app SDK for React Native / Expo: zero-code measurement windows, React commit hook, native signposts and log file",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {