effect-browser 0.2.0-beta.9 → 0.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +849 -1161
- package/dist/Browser-S4kvpmqa.mjs +605 -0
- package/dist/Browser-S4kvpmqa.mjs.map +1 -0
- package/dist/Browser.d.mts +127 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +211 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +297 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-iMlcuzVD.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-B5cjaUp5.mjs +618 -0
- package/dist/Page-B5cjaUp5.mjs.map +1 -0
- package/dist/Page-BlPjfibv.d.mts +595 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-BI8XGvDu.d.mts +209 -0
- package/dist/Plan-D1wrJVs8.mjs +508 -0
- package/dist/Plan-D1wrJVs8.mjs.map +1 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +64 -0
- package/dist/Stage.mjs +237 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-B6xr8nVi.mjs +261 -0
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-B8tUQELg.mjs +5073 -0
- package/dist/page-B8tUQELg.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-Cd04Okid.mjs +264 -0
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +666 -0
- package/src/BrowserError.ts +234 -0
- package/src/BrowserEvent.ts +296 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +522 -0
- package/src/Plan.ts +663 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +467 -0
- package/src/Supervisor.ts +599 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +610 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +184 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +376 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +195 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +312 -0
- package/src/internal/page/lane.ts +311 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +165 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +776 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/counts.ts +92 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +164 -0
- package/src/internal/reading/ready.ts +111 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +416 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +94 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's clock read on the owner's: estimates of their offset from timed probes, how an
|
|
3
|
+
* estimate ages, and the one mapping a browser's pages share. `pictures.ts` takes the probes.
|
|
4
|
+
*/
|
|
5
|
+
import { Duration, Effect, Option } from "effect";
|
|
6
|
+
|
|
7
|
+
export interface Probe {
|
|
8
|
+
readonly hostStart: number;
|
|
9
|
+
readonly hostEnd: number;
|
|
10
|
+
readonly browserTime: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Estimate {
|
|
14
|
+
/** Browser epoch milliseconds minus the owner's host monotonic milliseconds. */
|
|
15
|
+
readonly offsetMillis: number;
|
|
16
|
+
/** Either transport direction can own the entire round trip; symmetry is not assumed. */
|
|
17
|
+
readonly uncertaintyMillis: number;
|
|
18
|
+
readonly roundTripMillis: number;
|
|
19
|
+
readonly sampledAt: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The fastest complete probe limits uncertainty without mistaking transit delay for clock offset. */
|
|
23
|
+
export const estimate = (probes: ReadonlyArray<Probe>): Option.Option<Estimate> => {
|
|
24
|
+
let best: Estimate | undefined;
|
|
25
|
+
|
|
26
|
+
for (const probe of probes) {
|
|
27
|
+
const roundTripMillis = probe.hostEnd - probe.hostStart;
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
!Number.isFinite(probe.hostStart) ||
|
|
31
|
+
!Number.isFinite(probe.hostEnd) ||
|
|
32
|
+
!Number.isFinite(probe.browserTime) ||
|
|
33
|
+
!Number.isFinite(roundTripMillis) ||
|
|
34
|
+
roundTripMillis < 0
|
|
35
|
+
)
|
|
36
|
+
continue;
|
|
37
|
+
const sampledAt = probe.hostStart + roundTripMillis / 2;
|
|
38
|
+
const offsetMillis = probe.browserTime - sampledAt;
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
!Number.isFinite(offsetMillis) ||
|
|
42
|
+
!Number.isFinite(probe.browserTime - probe.hostStart) ||
|
|
43
|
+
!Number.isFinite(probe.browserTime - probe.hostEnd)
|
|
44
|
+
)
|
|
45
|
+
continue;
|
|
46
|
+
if (best === undefined || roundTripMillis < best.roundTripMillis)
|
|
47
|
+
best = {
|
|
48
|
+
offsetMillis,
|
|
49
|
+
uncertaintyMillis: roundTripMillis / 2,
|
|
50
|
+
roundTripMillis,
|
|
51
|
+
sampledAt,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Option.fromUndefinedOr(best);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Two machines' clocks drift apart by up to about 100 parts per million, so an estimate says this
|
|
59
|
+
// much less about the offset for each millisecond of its age.
|
|
60
|
+
const driftPerMillis = 1e-4;
|
|
61
|
+
|
|
62
|
+
/** An estimate's uncertainty at `hostTime`, wider the further that is from when it was sampled. */
|
|
63
|
+
export const uncertaintyAt = (estimate: Estimate, hostTime: number): number =>
|
|
64
|
+
estimate.uncertaintyMillis + Math.abs(hostTime - estimate.sampledAt) * driftPerMillis;
|
|
65
|
+
|
|
66
|
+
// Ten seconds widen an estimate by a millisecond, far less than a remote round trip, so it is
|
|
67
|
+
// measured again no more often than this.
|
|
68
|
+
const renewAfter = Duration.seconds(10);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The browser's one epoch mapping, shared by its pages, input runs and capture generations. It is
|
|
72
|
+
* measured on first need, by the first capture, never ahead of it.
|
|
73
|
+
*/
|
|
74
|
+
export interface Mapping {
|
|
75
|
+
/** The latest estimate, without measuring: input uses it, and never waits for one. */
|
|
76
|
+
readonly latest: () => Estimate | undefined;
|
|
77
|
+
/** The latest estimate, measured through `measure` only while the browser has none. */
|
|
78
|
+
readonly current: <E>(measure: Effect.Effect<Estimate, E>) => Effect.Effect<Estimate, E>;
|
|
79
|
+
/**
|
|
80
|
+
* Measure again once ten seconds have passed since the latest measurement began. A failed
|
|
81
|
+
* measurement keeps the estimate, which keeps widening with its age.
|
|
82
|
+
*/
|
|
83
|
+
readonly renew: <E>(measure: Effect.Effect<Estimate, E>) => Effect.Effect<void>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a new measurement should replace the current estimate. Each interval contains the true
|
|
88
|
+
* offset, so overlapping intervals agree and the narrower one is kept, the current one widened by
|
|
89
|
+
* its age: a probe delayed behind a busy page cannot widen every tab's stamps. A disjoint
|
|
90
|
+
* measurement means the clocks moved (a wall clock step, or drift between a remote browser and
|
|
91
|
+
* this host), so the newer evidence wins.
|
|
92
|
+
*/
|
|
93
|
+
export const supersedes = (next: Estimate, current: Estimate): boolean => {
|
|
94
|
+
const aged = uncertaintyAt(current, next.sampledAt);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
next.uncertaintyMillis <= aged ||
|
|
98
|
+
Math.abs(next.offsetMillis - current.offsetMillis) > next.uncertaintyMillis + aged
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Every renderer of a browser reads the same host wall clock, so the offset belongs to the
|
|
104
|
+
* browser rather than a page. A busy page cannot fail work that an earlier estimate can serve.
|
|
105
|
+
*/
|
|
106
|
+
export const mapping = (now: () => number): Mapping => {
|
|
107
|
+
let latest: Estimate | undefined;
|
|
108
|
+
let measuredAt = Number.NEGATIVE_INFINITY;
|
|
109
|
+
|
|
110
|
+
// Called where a measurement is decided, so no other can be decided before this one begins.
|
|
111
|
+
const adopt = <E>(measure: Effect.Effect<Estimate, E>) => {
|
|
112
|
+
measuredAt = now();
|
|
113
|
+
|
|
114
|
+
return measure.pipe(
|
|
115
|
+
Effect.map((estimate) => {
|
|
116
|
+
if (latest === undefined || supersedes(estimate, latest)) latest = estimate;
|
|
117
|
+
|
|
118
|
+
return latest;
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
latest: () => latest,
|
|
125
|
+
current: (measure) =>
|
|
126
|
+
Effect.suspend(() => (latest === undefined ? adopt(measure) : Effect.succeed(latest))),
|
|
127
|
+
renew: (measure) =>
|
|
128
|
+
Effect.suspend(() =>
|
|
129
|
+
latest === undefined || now() - measuredAt < Duration.toMillis(renewAfter)
|
|
130
|
+
? Effect.void
|
|
131
|
+
: Effect.ignore(adopt(measure)),
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const toHostTime = (calibration: Estimate, browserEpochMillis: number): number =>
|
|
137
|
+
browserEpochMillis - calibration.offsetMillis;
|
|
138
|
+
|
|
139
|
+
/** CDP input timestamps use seconds, unlike browser frame timestamps and the owner's clock. */
|
|
140
|
+
export const toBrowserSeconds = (calibration: Estimate, hostMillis: number): number =>
|
|
141
|
+
(hostMillis + calibration.offsetMillis) / 1000;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Bounded capture-loss and paint-gap statistics on the owning host clock. */
|
|
2
|
+
import { Duration } from "effect";
|
|
3
|
+
|
|
4
|
+
// Paint-time gaps between consecutive delivered frames.
|
|
5
|
+
const paintGaps = () => {
|
|
6
|
+
let count = 0;
|
|
7
|
+
let totalMillis = 0;
|
|
8
|
+
let minMillis: number | null = null;
|
|
9
|
+
let maxMillis: number | null = null;
|
|
10
|
+
let lastMillis: number | null = null;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
add: (gap: number) => {
|
|
14
|
+
count++;
|
|
15
|
+
totalMillis += gap;
|
|
16
|
+
minMillis = minMillis === null ? gap : Math.min(minMillis, gap);
|
|
17
|
+
maxMillis = maxMillis === null ? gap : Math.max(maxMillis, gap);
|
|
18
|
+
lastMillis = gap;
|
|
19
|
+
},
|
|
20
|
+
snapshot: () => ({ count, totalMillis, minMillis, maxMillis, lastMillis }),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const none = () => ({
|
|
25
|
+
received: 0,
|
|
26
|
+
accepted: 0,
|
|
27
|
+
late: 0,
|
|
28
|
+
missingTimestamp: 0,
|
|
29
|
+
foreignSize: 0,
|
|
30
|
+
duringPictures: 0,
|
|
31
|
+
lost: 0,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type Counted = keyof ReturnType<typeof none>;
|
|
35
|
+
|
|
36
|
+
/** How far back a window of capture counts can reach. */
|
|
37
|
+
export const countsKept = Duration.minutes(1);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A capture's counts and paint gaps over its page's life, and each with when it happened on the
|
|
41
|
+
* owner's clock for the latest minute, so a window of them can be read.
|
|
42
|
+
*/
|
|
43
|
+
export const tally = (now: () => number) => {
|
|
44
|
+
const totals = none();
|
|
45
|
+
const gaps = paintGaps();
|
|
46
|
+
|
|
47
|
+
let recent: Array<{
|
|
48
|
+
readonly at: number;
|
|
49
|
+
readonly counted: Counted | "gap";
|
|
50
|
+
readonly n: number;
|
|
51
|
+
}> = [];
|
|
52
|
+
|
|
53
|
+
let oldest = 0;
|
|
54
|
+
|
|
55
|
+
const note = (counted: Counted | "gap", n: number) => {
|
|
56
|
+
const at = now();
|
|
57
|
+
|
|
58
|
+
recent.push({ at, counted, n });
|
|
59
|
+
while ((recent[oldest]?.at ?? at) < at - Duration.toMillis(countsKept)) oldest++;
|
|
60
|
+
// Shed what is out of reach once it is half the log.
|
|
61
|
+
if (oldest > recent.length / 2) {
|
|
62
|
+
recent = recent.slice(oldest);
|
|
63
|
+
oldest = 0;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
count: (counted: Counted, n = 1) => {
|
|
69
|
+
totals[counted] += n;
|
|
70
|
+
note(counted, n);
|
|
71
|
+
},
|
|
72
|
+
gap: (millis: number) => {
|
|
73
|
+
gaps.add(millis);
|
|
74
|
+
note("gap", millis);
|
|
75
|
+
},
|
|
76
|
+
/** Lifetime counts, or those of the latest `windowMillis`, at most a minute. */
|
|
77
|
+
read: (windowMillis?: number) => {
|
|
78
|
+
if (windowMillis === undefined) return { ...totals, gaps: gaps.snapshot() };
|
|
79
|
+
const counts = none();
|
|
80
|
+
const windowed = paintGaps();
|
|
81
|
+
const since = now() - windowMillis;
|
|
82
|
+
|
|
83
|
+
for (const { at, counted, n } of recent.slice(oldest)) {
|
|
84
|
+
if (at < since) continue;
|
|
85
|
+
if (counted === "gap") windowed.add(n);
|
|
86
|
+
else counts[counted] += n;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ...counts, gaps: windowed.snapshot() };
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|