@rocapine/rocalytics-sdk 0.0.0-stage → 0.2.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +447 -2
  4. package/client/package.json +1 -0
  5. package/core/package.json +4 -0
  6. package/dist/client/client.d.ts +136 -0
  7. package/dist/client/client.d.ts.map +1 -0
  8. package/dist/client/client.js +373 -0
  9. package/dist/client/client.js.map +1 -0
  10. package/dist/client/index.d.ts +13 -0
  11. package/dist/client/index.d.ts.map +1 -0
  12. package/dist/client/index.js +35 -0
  13. package/dist/client/index.js.map +1 -0
  14. package/dist/client/native.d.ts +68 -0
  15. package/dist/client/native.d.ts.map +1 -0
  16. package/dist/client/native.js +64 -0
  17. package/dist/client/native.js.map +1 -0
  18. package/dist/client/onboardingSink.d.ts +63 -0
  19. package/dist/client/onboardingSink.d.ts.map +1 -0
  20. package/dist/client/onboardingSink.js +126 -0
  21. package/dist/client/onboardingSink.js.map +1 -0
  22. package/dist/client/paywallSink.d.ts +13 -0
  23. package/dist/client/paywallSink.d.ts.map +1 -0
  24. package/dist/client/paywallSink.js +26 -0
  25. package/dist/client/paywallSink.js.map +1 -0
  26. package/dist/client/requests.d.ts +66 -0
  27. package/dist/client/requests.d.ts.map +1 -0
  28. package/dist/client/requests.js +90 -0
  29. package/dist/client/requests.js.map +1 -0
  30. package/dist/client/types.d.ts +180 -0
  31. package/dist/client/types.d.ts.map +1 -0
  32. package/dist/client/types.js +4 -0
  33. package/dist/client/types.js.map +1 -0
  34. package/dist/core/context.d.ts +39 -0
  35. package/dist/core/context.d.ts.map +1 -0
  36. package/dist/core/context.js +45 -0
  37. package/dist/core/context.js.map +1 -0
  38. package/dist/core/delivery.d.ts +63 -0
  39. package/dist/core/delivery.d.ts.map +1 -0
  40. package/dist/core/delivery.js +133 -0
  41. package/dist/core/delivery.js.map +1 -0
  42. package/dist/core/diagnostics.d.ts +16 -0
  43. package/dist/core/diagnostics.d.ts.map +1 -0
  44. package/dist/core/diagnostics.js +24 -0
  45. package/dist/core/diagnostics.js.map +1 -0
  46. package/dist/core/httpSink.d.ts +35 -0
  47. package/dist/core/httpSink.d.ts.map +1 -0
  48. package/dist/core/httpSink.js +65 -0
  49. package/dist/core/httpSink.js.map +1 -0
  50. package/dist/core/ids.d.ts +16 -0
  51. package/dist/core/ids.d.ts.map +1 -0
  52. package/dist/core/ids.js +51 -0
  53. package/dist/core/ids.js.map +1 -0
  54. package/dist/core/index.d.ts +17 -0
  55. package/dist/core/index.d.ts.map +1 -0
  56. package/dist/core/index.js +32 -0
  57. package/dist/core/index.js.map +1 -0
  58. package/dist/core/sink.d.ts +39 -0
  59. package/dist/core/sink.d.ts.map +1 -0
  60. package/dist/core/sink.js +16 -0
  61. package/dist/core/sink.js.map +1 -0
  62. package/dist/core/storage.d.ts +52 -0
  63. package/dist/core/storage.d.ts.map +1 -0
  64. package/dist/core/storage.js +79 -0
  65. package/dist/core/storage.js.map +1 -0
  66. package/dist/core/text.d.ts +9 -0
  67. package/dist/core/text.d.ts.map +1 -0
  68. package/dist/core/text.js +47 -0
  69. package/dist/core/text.js.map +1 -0
  70. package/dist/core/time.d.ts +18 -0
  71. package/dist/core/time.d.ts.map +1 -0
  72. package/dist/core/time.js +18 -0
  73. package/dist/core/time.js.map +1 -0
  74. package/dist/onboarding/contract.d.ts +138 -0
  75. package/dist/onboarding/contract.d.ts.map +1 -0
  76. package/dist/onboarding/contract.js +11 -0
  77. package/dist/onboarding/contract.js.map +1 -0
  78. package/dist/onboarding/index.d.ts +15 -0
  79. package/dist/onboarding/index.d.ts.map +1 -0
  80. package/dist/onboarding/index.js +22 -0
  81. package/dist/onboarding/index.js.map +1 -0
  82. package/dist/onboarding/input.d.ts +102 -0
  83. package/dist/onboarding/input.d.ts.map +1 -0
  84. package/dist/onboarding/input.js +216 -0
  85. package/dist/onboarding/input.js.map +1 -0
  86. package/dist/onboarding/persisted.d.ts +20 -0
  87. package/dist/onboarding/persisted.d.ts.map +1 -0
  88. package/dist/onboarding/persisted.js +75 -0
  89. package/dist/onboarding/persisted.js.map +1 -0
  90. package/dist/onboarding/state.d.ts +65 -0
  91. package/dist/onboarding/state.d.ts.map +1 -0
  92. package/dist/onboarding/state.js +207 -0
  93. package/dist/onboarding/state.js.map +1 -0
  94. package/dist/onboarding/tracker.d.ts +135 -0
  95. package/dist/onboarding/tracker.d.ts.map +1 -0
  96. package/dist/onboarding/tracker.js +734 -0
  97. package/dist/onboarding/tracker.js.map +1 -0
  98. package/dist/paywall/contract.d.ts +67 -0
  99. package/dist/paywall/contract.d.ts.map +1 -0
  100. package/dist/paywall/contract.js +3 -0
  101. package/dist/paywall/contract.js.map +1 -0
  102. package/dist/paywall/index.d.ts +14 -0
  103. package/dist/paywall/index.d.ts.map +1 -0
  104. package/dist/paywall/index.js +19 -0
  105. package/dist/paywall/index.js.map +1 -0
  106. package/dist/paywall/observer.d.ts +38 -0
  107. package/dist/paywall/observer.d.ts.map +1 -0
  108. package/dist/paywall/observer.js +6 -0
  109. package/dist/paywall/observer.js.map +1 -0
  110. package/dist/paywall/state.d.ts +26 -0
  111. package/dist/paywall/state.d.ts.map +1 -0
  112. package/dist/paywall/state.js +132 -0
  113. package/dist/paywall/state.js.map +1 -0
  114. package/dist/paywall/tracker.d.ts +36 -0
  115. package/dist/paywall/tracker.d.ts.map +1 -0
  116. package/dist/paywall/tracker.js +180 -0
  117. package/dist/paywall/tracker.js.map +1 -0
  118. package/dist/version.d.ts +3 -0
  119. package/dist/version.d.ts.map +1 -0
  120. package/dist/version.js +6 -0
  121. package/dist/version.js.map +1 -0
  122. package/docs/onboarding-run-contract.md +1013 -0
  123. package/docs/onboarding-run.examples.ts +306 -0
  124. package/docs/onboarding-run.schema.json +224 -0
  125. package/docs/onboarding-run.types.ts +144 -0
  126. package/docs/paywall-presentation-contract.md +139 -0
  127. package/docs/paywall-presentation.examples.ts +42 -0
  128. package/docs/paywall-presentation.schema.json +268 -0
  129. package/docs/paywall-presentation.types.ts +73 -0
  130. package/onboarding/package.json +4 -0
  131. package/package.json +120 -4
  132. package/paywall/package.json +1 -0
  133. package/src/client/client.ts +405 -0
  134. package/src/client/index.ts +54 -0
  135. package/src/client/native.ts +109 -0
  136. package/src/client/onboardingSink.ts +121 -0
  137. package/src/client/paywallSink.ts +25 -0
  138. package/src/client/requests.ts +126 -0
  139. package/src/client/types.ts +198 -0
  140. package/src/core/context.ts +69 -0
  141. package/src/core/delivery.ts +181 -0
  142. package/src/core/diagnostics.ts +30 -0
  143. package/src/core/httpSink.ts +70 -0
  144. package/src/core/ids.ts +46 -0
  145. package/src/core/index.ts +20 -0
  146. package/src/core/sink.ts +46 -0
  147. package/src/core/storage.ts +112 -0
  148. package/src/core/text.ts +37 -0
  149. package/src/core/time.ts +26 -0
  150. package/src/onboarding/contract.ts +144 -0
  151. package/src/onboarding/index.ts +39 -0
  152. package/src/onboarding/input.ts +246 -0
  153. package/src/onboarding/persisted.ts +93 -0
  154. package/src/onboarding/state.ts +233 -0
  155. package/src/onboarding/tracker.ts +855 -0
  156. package/src/paywall/contract.ts +73 -0
  157. package/src/paywall/index.ts +36 -0
  158. package/src/paywall/observer.ts +43 -0
  159. package/src/paywall/state.ts +125 -0
  160. package/src/paywall/tracker.ts +200 -0
  161. package/src/version.ts +2 -0
@@ -0,0 +1,734 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onboardingRun = void 0;
4
+ exports.createOnboardingRunTracker = createOnboardingRunTracker;
5
+ const context_1 = require("../core/context");
6
+ const delivery_1 = require("../core/delivery");
7
+ const diagnostics_1 = require("../core/diagnostics");
8
+ const ids_1 = require("../core/ids");
9
+ const storage_1 = require("../core/storage");
10
+ const time_1 = require("../core/time");
11
+ const version_1 = require("../version");
12
+ const input_1 = require("./input");
13
+ const persisted_1 = require("./persisted");
14
+ const state_1 = require("./state");
15
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
16
+ const NOTE_MESSAGES = {
17
+ truncated: "the run reached a recording limit (500 entries or 261,120 bytes): recording has stopped",
18
+ "too-many-answers": "an entry holds at most 50 answers: the extra answer was dropped",
19
+ };
20
+ function inertRun(runId) {
21
+ return {
22
+ runId,
23
+ currentStepKey: null,
24
+ enterStep() { },
25
+ exitStep() { },
26
+ setProperties() { },
27
+ complete() { },
28
+ background() { },
29
+ };
30
+ }
31
+ // Per storage key, process-wide, so they hold whichever storage object a host
32
+ // passes (a fresh adapter on every configure() included):
33
+ // - the last disposed tracker's writes: `drain`, bounded by its idle(), which a
34
+ // new tracker's first read waits for, so it sees what the old one staged;
35
+ // `landed`, unbounded; and each queued write, its predecessors' still to land
36
+ // included, so a new tracker that read before one landed writes its own
37
+ // state again;
38
+ // - what the last disposed tracker hands over, with storage or without, taken
39
+ // by the next tracker. Known at dispose(), and newer than any write still to
40
+ // land: its run, when it started or resumed one (null once that run is
41
+ // completed or replaced), or else the run it was handed; and its unsent
42
+ // snapshots, which the next tracker holds (and writes) from the start. Then,
43
+ // once its last attempts are over, what is still unsent and what was settled.
44
+ // A tracker disposed without a run passes on what it was handed;
45
+ // - how many live trackers use the key: two at once overwrite each other.
46
+ const lastDrainOnKey = new Map();
47
+ // Only a tracker whose sink has the same destination takes it: one that
48
+ // sends elsewhere takes nothing, and discards the stored state it reads;
49
+ const handoffOnKey = new Map();
50
+ const liveOnKey = new Map();
51
+ function createOnboardingRunTracker(config) {
52
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
53
+ const clock = (_a = config.clock) !== null && _a !== void 0 ? _a : time_1.systemClock;
54
+ const timers = (_b = config.timers) !== null && _b !== void 0 ? _b : time_1.systemTimers;
55
+ const report = (0, diagnostics_1.safeDiagnostics)(config.onDiagnostic);
56
+ const fallbackUuid = (0, ids_1.createUuid)(clock);
57
+ const debounceMs = (_c = config.debounceMs) !== null && _c !== void 0 ? _c : 500;
58
+ // Set only when the stored value was READ and found unparseable or malformed,
59
+ // so it is rewritten without the bad part. A failed read never sets it.
60
+ let rewrite = false;
61
+ // Set when the stored value must not be touched: storage could not be read
62
+ // (twice), or it holds a format this version does not know. The session then
63
+ // runs without persistence, and the stored value is left as it is.
64
+ let persistenceOff = false;
65
+ // The slow read was reported (once).
66
+ let readSlow = false;
67
+ const storageKey = (_d = config.storageKey) !== null && _d !== void 0 ? _d : "rocalytics-sdk:onboarding-run";
68
+ const store = config.storage
69
+ ? (0, storage_1.createSerialStore)(config.storage, storageKey, (e) => report({ code: "storage", message: `storage failed: ${String(e)}` }))
70
+ : null;
71
+ if (store) {
72
+ const others = (_e = liveOnKey.get(storageKey)) !== null && _e !== void 0 ? _e : 0;
73
+ if (others > 0) {
74
+ report({
75
+ code: "storage",
76
+ message: `another live tracker already uses storageKey "${storageKey}": trackers running at once must each use their own storageKey, or they overwrite each other's unsent snapshots`,
77
+ });
78
+ }
79
+ liveOnKey.set(storageKey, others + 1);
80
+ }
81
+ const deliveries = new Map();
82
+ const outboxes = new Map();
83
+ // Per run, the highest seq a sink settled, here or in the predecessor.
84
+ const settledSeqs = new Map();
85
+ // Sends waiting for their snapshot's write (at most persistTimeoutMs).
86
+ const staging = new Set();
87
+ // Writes queued and not yet done.
88
+ const writing = new Set();
89
+ // Where this tracker's snapshots go; stamped on what it stores.
90
+ const destination = typeof ((_f = config.sink) === null || _f === void 0 ? void 0 : _f.destination) === "string" ? config.sink.destination : undefined;
91
+ // Taken once: a later tracker on this key gets this one's handoff, not the predecessor's.
92
+ const offered = handoffOnKey.get(storageKey);
93
+ handoffOnKey.delete(storageKey);
94
+ // A predecessor that sent elsewhere: nothing it left is taken, in memory or from storage.
95
+ const destinationChanged = !!offered &&
96
+ (offered.destination !== undefined && destination !== undefined ? offered.destination !== destination : offered.sink !== config.sink);
97
+ // Reported once, and only when something is discarded: a switch that leaves nothing is not a loss.
98
+ let discardReported = false;
99
+ const reportDiscarded = (message) => {
100
+ if (discardReported)
101
+ return;
102
+ discardReported = true;
103
+ report({ code: "destination-changed", message });
104
+ };
105
+ const replacedElsewhere = `the tracker this one replaces on storageKey "${storageKey}" sent to another destination: what it left is discarded, not sent here`;
106
+ if (offered && destinationChanged) {
107
+ if ((_g = offered.run) === null || _g === void 0 ? void 0 : _g.current)
108
+ reportDiscarded(replacedElsewhere); // a run in progress
109
+ // Its unsent snapshots are known once its last attempts are answered: those delivered are not lost.
110
+ void offered.left.then((left) => left.unsent.size > 0 && reportDiscarded(replacedElsewhere)).catch(() => undefined);
111
+ }
112
+ const handed = destinationChanged ? undefined : offered;
113
+ const predecessor = handed === null || handed === void 0 ? void 0 : handed.left;
114
+ // The predecessor's run: when set, it replaces the stored one, which a late write may not have updated.
115
+ const handedRun = handed === null || handed === void 0 ? void 0 : handed.run;
116
+ const priorWrites = store ? lastDrainOnKey.get(storageKey) : undefined;
117
+ // The predecessor's unsent snapshots, and stored outboxes read before its last
118
+ // attempts were over: kept (so every write holds them), and sent after them.
119
+ const heldBack = new Map(handed === null || handed === void 0 ? void 0 : handed.outboxes);
120
+ for (const [runId, item] of heldBack)
121
+ outboxes.set(runId, item);
122
+ let inheritedDone = !predecessor;
123
+ let live = null;
124
+ // A run was started (or resumed) this session: the previous launch's run is then abandoned,
125
+ // whether or not the run started here is still live.
126
+ let startedThisSession = false;
127
+ let persistedCurrent = (_h = handedRun === null || handedRun === void 0 ? void 0 : handedRun.current) !== null && _h !== void 0 ? _h : null;
128
+ let disposed = false;
129
+ const mintRunId = () => {
130
+ try {
131
+ const id = config.uuid ? String(config.uuid()).toLowerCase() : fallbackUuid();
132
+ if (UUID.test(id))
133
+ return id;
134
+ report({ code: "invalid-uuid", message: `the uuid option returned "${id}", not a UUID: using the default` });
135
+ }
136
+ catch (e) {
137
+ report({ code: "invalid-uuid", message: `the uuid option threw: ${String(e)}` });
138
+ }
139
+ return fallbackUuid();
140
+ };
141
+ const delivered = (runId, item) => { var _a; return item.seq <= ((_a = settledSeqs.get(runId)) !== null && _a !== void 0 ? _a : 0); };
142
+ const compose = () => {
143
+ const current = live && live.state.status === "in_progress" ? live.state : persistedCurrent;
144
+ if (!current && outboxes.size === 0)
145
+ return null;
146
+ return { format: 1, ...(destination === undefined ? {} : { destination }), current, outboxes: Object.fromEntries(outboxes) };
147
+ };
148
+ // Persisted outboxes are merged in before anything is written, so a write
149
+ // composed this session never drops a snapshot left by the previous one.
150
+ // Every stored part is validated first; an invalid one is dropped, reported,
151
+ // and the stored value rewritten without it. This promise never rejects.
152
+ const loaded = store
153
+ ? store
154
+ .read(priorWrites === null || priorWrites === void 0 ? void 0 : priorWrites.drain) // after a disposed predecessor's last writes
155
+ .then((result) => {
156
+ var _a, _b;
157
+ if (result.status === "failed") {
158
+ persistenceOff = true;
159
+ return report({
160
+ code: "storage",
161
+ message: `reading stored state failed twice (${String(result.error)}): it is left untouched, and this session runs without persistence`,
162
+ });
163
+ }
164
+ if (result.status === "invalid") {
165
+ rewrite = true;
166
+ return report({ code: "storage", message: "the stored state is not JSON: discarded" });
167
+ }
168
+ const raw = result.value;
169
+ if (raw === null)
170
+ return;
171
+ const format = raw.format;
172
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && typeof format === "number" && format !== 1) {
173
+ persistenceOff = true;
174
+ return report({
175
+ code: "storage",
176
+ message: `the stored state has format ${format}, which this version does not know: it is left untouched, and this session runs without persistence`,
177
+ });
178
+ }
179
+ const { current, outboxes: stored, problems } = (0, persisted_1.parsePersisted)(raw);
180
+ if (problems.length) {
181
+ rewrite = true;
182
+ report({ code: "storage", message: `${problems.join("; ")}: discarded` });
183
+ }
184
+ // What is left, written for another destination: never sent here, so discarded.
185
+ if (destinationChanged || raw.destination !== destination) {
186
+ if (!current && stored.length === 0)
187
+ return;
188
+ rewrite = true;
189
+ return reportDiscarded(destinationChanged
190
+ ? replacedElsewhere
191
+ : "the stored state was written for another destination than this sink's: discarded, nothing of it is sent");
192
+ }
193
+ // Merged even after dispose(), so a write still queued keeps them; but
194
+ // sent only before it, and only once the predecessor's last attempts
195
+ // are over (the snapshots they deliver are dropped, not sent again).
196
+ for (const [runId, item] of stored) {
197
+ if (((_b = (_a = outboxes.get(runId)) === null || _a === void 0 ? void 0 : _a.seq) !== null && _b !== void 0 ? _b : 0) >= item.seq)
198
+ continue;
199
+ if (delivered(runId, item))
200
+ rewrite = true;
201
+ else if (!inheritedDone) {
202
+ outboxes.set(runId, item);
203
+ heldBack.set(runId, item);
204
+ }
205
+ else if (disposed)
206
+ outboxes.set(runId, item);
207
+ else
208
+ deliveryFor(runId).enqueue(item);
209
+ }
210
+ if (current && current.status === "in_progress" && !handedRun) {
211
+ if (!startedThisSession)
212
+ persistedCurrent = current;
213
+ else
214
+ sendAbandoned(current); // a new run started before storage was read
215
+ }
216
+ })
217
+ .catch((e) => report({ code: "storage", message: `loading stored state failed: ${String(e)}` }))
218
+ .then(() => {
219
+ if (rewrite && !persistenceOff && !disposed)
220
+ void persist();
221
+ rewrite = false;
222
+ })
223
+ : Promise.resolve();
224
+ /**
225
+ * The predecessor's handoff, once its last attempts are over: what it left
226
+ * unsent is kept and sent from here, with the stored outboxes held back for
227
+ * it. Per run, only a snapshot newer than the one held here and than any the
228
+ * predecessor's sink settled. Never rejects.
229
+ */
230
+ const inherited = predecessor
231
+ ? predecessor
232
+ .then((handoff) => {
233
+ var _a;
234
+ for (const [runId, seq] of handoff.settled)
235
+ settledSeqs.set(runId, Math.max(seq, (_a = settledSeqs.get(runId)) !== null && _a !== void 0 ? _a : 0));
236
+ let dropped = false;
237
+ for (const [runId, item] of [...heldBack, ...handoff.unsent]) {
238
+ const held = outboxes.get(runId);
239
+ if (delivered(runId, item)) {
240
+ if (held === item) {
241
+ outboxes.delete(runId);
242
+ dropped = true;
243
+ }
244
+ }
245
+ else if (!held || held === item || held.seq < item.seq) {
246
+ if (disposed)
247
+ outboxes.set(runId, item); // passed on to the next tracker
248
+ else
249
+ deliveryFor(runId).enqueue(item); // persisted by onPendingChange
250
+ }
251
+ }
252
+ heldBack.clear();
253
+ if (dropped)
254
+ persist();
255
+ })
256
+ .catch((e) => report({ code: "internal-error", message: `adopting a disposed tracker's snapshots: ${String(e)}` }))
257
+ .then(() => {
258
+ inheritedDone = true;
259
+ })
260
+ : Promise.resolve();
261
+ // Each predecessor write that lands after this tracker read storage replaces
262
+ // what it wrote: this tracker writes its state again once it holds a state
263
+ // at least as new, that is once it has a run of its own or was handed the
264
+ // predecessor's. (Otherwise start() or resume() writes it anyway.)
265
+ if (priorWrites) {
266
+ let drained = false;
267
+ void priorWrites.drain.then(() => (drained = true));
268
+ for (const write of priorWrites.writes) {
269
+ void write.then(() => {
270
+ if (drained && (startedThisSession || handedRun || destinationChanged))
271
+ persist();
272
+ });
273
+ }
274
+ }
275
+ /**
276
+ * A run left in progress by the previous launch and not resumed: if it holds
277
+ * a change that never went into a send (killed inside the debounce window),
278
+ * send it now, so the latest snapshot is not lost (section 5).
279
+ */
280
+ function sendAbandoned(state) {
281
+ if (!state.dirty || state.steps.length === 0)
282
+ return;
283
+ const seq = state.lastSeq + 1;
284
+ stageAndSend(state.runId, { seq, body: (0, state_1.toSnapshot)(state, seq, Math.max(clock.now(), (0, state_1.floorMs)(state))) }, false);
285
+ }
286
+ const persistTimeoutMs = (_j = config.persistTimeoutMs) !== null && _j !== void 0 ? _j : 1000;
287
+ const storageReadTimeoutMs = (_k = config.storageReadTimeoutMs) !== null && _k !== void 0 ? _k : 5000;
288
+ /** `p`, or `false` once `ms` have passed without it settling. Never rejects. */
289
+ const within = (p, ms) => new Promise((resolve) => {
290
+ const timer = timers.setTimeout(() => resolve(false), ms);
291
+ p.then((v) => {
292
+ timers.clearTimeout(timer);
293
+ resolve(v);
294
+ }, () => {
295
+ timers.clearTimeout(timer);
296
+ resolve(false);
297
+ });
298
+ });
299
+ /**
300
+ * Waits for the stored state to be read, at most storageReadTimeoutMs, and
301
+ * reports a read that takes longer. The read itself goes on: resume() still
302
+ * waits for it, and writes, each queued behind it and composed after it, go
303
+ * on with what it found once it lands.
304
+ */
305
+ const waitLoaded = async () => {
306
+ if (!store)
307
+ return true;
308
+ const ok = await within(loaded.then(() => true), storageReadTimeoutMs);
309
+ if (!ok && !readSlow) {
310
+ readSlow = true;
311
+ report({
312
+ code: "storage",
313
+ message: `the stored state has not been read within ${storageReadTimeoutMs} ms: resume() waits for it`,
314
+ });
315
+ }
316
+ return ok;
317
+ };
318
+ /**
319
+ * Queues a write of the whole tracker state, in order, right away; its value
320
+ * is composed when its turn comes (after the stored state was read). A write
321
+ * queued before dispose() still happens: that is how dispose() keeps a
322
+ * staged snapshot. Never rejects; resolves once the write is done or failed.
323
+ */
324
+ const persist = () => {
325
+ if (!store || disposed)
326
+ return Promise.resolve();
327
+ const write = store.saveWith(async () => {
328
+ await loaded;
329
+ return persistenceOff ? undefined : compose();
330
+ });
331
+ writing.add(write);
332
+ void write.then(() => writing.delete(write));
333
+ return write;
334
+ };
335
+ /**
336
+ * Stores a snapshot as the run's unsent one, together with the state that
337
+ * assigned its seq, and only then hands it to the sink (3.2: persist on
338
+ * every change). Waits at most `persistTimeoutMs` for the write.
339
+ */
340
+ function stageAndSend(runId, item, flush) {
341
+ outboxes.set(runId, item);
342
+ // Not gated on dispose(): a snapshot staged before it is still sent (once; see deliveryFor).
343
+ const send = () => {
344
+ const d = deliveryFor(runId);
345
+ d.enqueue(item);
346
+ if (flush)
347
+ d.flush();
348
+ };
349
+ if (!store)
350
+ return send();
351
+ let done = false;
352
+ let sent = () => { };
353
+ const staged = new Promise((resolve) => (sent = resolve));
354
+ staging.add(staged);
355
+ const once = () => {
356
+ if (done)
357
+ return;
358
+ done = true;
359
+ timers.clearTimeout(timer);
360
+ staging.delete(staged);
361
+ send();
362
+ sent();
363
+ };
364
+ const timer = timers.setTimeout(once, persistTimeoutMs);
365
+ void persist().then(once);
366
+ }
367
+ function deliveryFor(runId) {
368
+ let d = deliveries.get(runId);
369
+ if (!d) {
370
+ d = (0, delivery_1.createDelivery)({
371
+ sink: config.sink,
372
+ timers,
373
+ retry: config.retry,
374
+ attemptTimeoutMs: config.attemptTimeoutMs,
375
+ onPendingChange: (pending) => {
376
+ var _a, _b;
377
+ // Only ever moves forward: a newer snapshot may already be staged for this run.
378
+ if (pending && ((_b = (_a = outboxes.get(runId)) === null || _a === void 0 ? void 0 : _a.seq) !== null && _b !== void 0 ? _b : 0) <= pending.seq) {
379
+ outboxes.set(runId, pending);
380
+ persist();
381
+ }
382
+ },
383
+ onSettled: (item, result) => {
384
+ var _a;
385
+ settledSeqs.set(runId, Math.max(item.seq, (_a = settledSeqs.get(runId)) !== null && _a !== void 0 ? _a : 0));
386
+ const staged = outboxes.get(runId);
387
+ if (staged && staged.seq <= item.seq) {
388
+ outboxes.delete(runId);
389
+ persist();
390
+ }
391
+ if (result.outcome === "rejected") {
392
+ report({
393
+ code: "rejected",
394
+ runId,
395
+ message: `snapshot seq ${item.seq} was rejected by the ingest${result.reason ? `: ${result.reason}` : ""}`,
396
+ });
397
+ }
398
+ },
399
+ });
400
+ deliveries.set(runId, d);
401
+ if (disposed)
402
+ d.close(); // after dispose(): one last attempt per snapshot, no retry timers
403
+ }
404
+ return d;
405
+ }
406
+ /** Resolves once queued storage writes are done, however long they take. */
407
+ const writesDone = async () => {
408
+ if (!store)
409
+ return;
410
+ for (let i = 0; i < 3; i++) {
411
+ await store.idle();
412
+ await Promise.resolve();
413
+ }
414
+ };
415
+ /** Waits up to storageReadTimeoutMs for the read, then as long again for `writes`. */
416
+ const idleWithin = async (writes) => {
417
+ if (!store || !(await waitLoaded()))
418
+ return;
419
+ await within(writes, storageReadTimeoutMs);
420
+ };
421
+ /**
422
+ * What this disposed tracker leaves unsent, once its last attempts are
423
+ * over: the predecessor's handoff taken in, every send that waited for its
424
+ * write handed to the sink, and no attempt in flight. Each attempt is bounded
425
+ * by attemptTimeoutMs, and each wait for a write by persistTimeoutMs.
426
+ */
427
+ const leftUnsent = async () => {
428
+ await inherited;
429
+ do {
430
+ await Promise.all(staging);
431
+ await Promise.all([...deliveries.values()].map((d) => d.idle()));
432
+ } while (staging.size > 0);
433
+ return { unsent: new Map(outboxes), settled: new Map(settledSeqs) };
434
+ };
435
+ class Controller {
436
+ constructor(state) {
437
+ this.active = true;
438
+ this.debounce = null;
439
+ this.state = state;
440
+ }
441
+ get runId() {
442
+ return this.state.runId;
443
+ }
444
+ get currentStepKey() {
445
+ var _a, _b;
446
+ return (_b = (_a = this.state.steps[this.state.steps.length - 1]) === null || _a === void 0 ? void 0 : _a.step_key) !== null && _b !== void 0 ? _b : null;
447
+ }
448
+ now() {
449
+ return Math.max(clock.now(), (0, state_1.floorMs)(this.state));
450
+ }
451
+ diag(code, message) {
452
+ report({ code, message, runId: this.state.runId });
453
+ }
454
+ /** Runs a public method body: never throws, and refuses once the run is over. */
455
+ guard(name, body, allowCompleted = false) {
456
+ try {
457
+ if (disposed)
458
+ return;
459
+ if (!this.active)
460
+ return this.diag("run-inactive", `${name}: a newer run was started; this one records nothing more`);
461
+ if (this.state.status === "completed" && !allowCompleted) {
462
+ return this.diag("run-completed", `${name}: the run is completed and records nothing more`);
463
+ }
464
+ body();
465
+ }
466
+ catch (e) {
467
+ this.diag("internal-error", `${name}: ${String(e)}`);
468
+ }
469
+ }
470
+ apply(result, send) {
471
+ for (const note of result.notes)
472
+ this.diag(note, NOTE_MESSAGES[note]);
473
+ this.state = result.changed ? { ...result.state, dirty: true } : result.state;
474
+ persist();
475
+ if (!result.changed)
476
+ return;
477
+ if (send === "now" || debounceMs <= 0)
478
+ this.sendNow();
479
+ else if (this.debounce === null)
480
+ this.debounce = timers.setTimeout(() => this.sendNow(), debounceMs);
481
+ }
482
+ /** Sends the run now. `flush` also skips a backoff in progress (completion, background). */
483
+ sendNow(flush = false) {
484
+ if (this.debounce !== null)
485
+ timers.clearTimeout(this.debounce);
486
+ this.debounce = null;
487
+ if (disposed || this.state.steps.length === 0)
488
+ return;
489
+ const now = this.now();
490
+ const seq = this.state.lastSeq + 1;
491
+ const body = (0, state_1.toSnapshot)(this.state, seq, now);
492
+ this.state = {
493
+ ...this.state,
494
+ lastSeq: seq,
495
+ lastActiveAt: this.state.status === "in_progress" ? now : this.state.lastActiveAt,
496
+ dirty: false,
497
+ };
498
+ stageAndSend(this.state.runId, { seq, body }, flush);
499
+ }
500
+ /** A change waiting on the debounce is sent now. */
501
+ flushDebounce() {
502
+ if (this.debounce !== null)
503
+ this.sendNow();
504
+ }
505
+ deactivate() {
506
+ this.flushDebounce(); // a change waiting on the debounce still goes out
507
+ this.active = false;
508
+ }
509
+ enterStep(stepKey) {
510
+ this.guard("enterStep", () => {
511
+ if (typeof stepKey !== "string" || !this.state.manifest.steps.some((s) => s.step_key === stepKey)) {
512
+ return this.diag("unknown-step", `enterStep: "${String(stepKey)}" is not in the manifest`);
513
+ }
514
+ const last = this.state.steps[this.state.steps.length - 1];
515
+ if (last && last.step_key === stepKey && last.exited_at === null) {
516
+ return this.diag("already-on-step", `enterStep: already on "${stepKey}"`);
517
+ }
518
+ this.apply((0, state_1.enter)(this.state, stepKey, this.now()), "debounced");
519
+ });
520
+ }
521
+ exitStep(stepKey, options) {
522
+ this.guard("exitStep", () => {
523
+ var _a, _b;
524
+ const steps = this.state.steps;
525
+ let index = -1;
526
+ if (((_a = steps[steps.length - 1]) === null || _a === void 0 ? void 0 : _a.step_key) === stepKey)
527
+ index = steps.length - 1;
528
+ else if (((_b = steps[steps.length - 2]) === null || _b === void 0 ? void 0 : _b.step_key) === stepKey)
529
+ index = steps.length - 2;
530
+ if (index < 0)
531
+ return this.diag("not-current-step", `exitStep: "${String(stepKey)}" is not the current step`);
532
+ const raw = options && Array.isArray(options.answers) ? options.answers : [];
533
+ if ((options === null || options === void 0 ? void 0 : options.answers) !== undefined && !Array.isArray(options.answers)) {
534
+ this.diag("invalid-answer", "exitStep: answers must be an array");
535
+ }
536
+ const answers = [];
537
+ for (const input of raw) {
538
+ const r = (0, input_1.toAnswer)(input);
539
+ if (r.ok)
540
+ answers.push(r.answer);
541
+ else
542
+ this.diag("invalid-answer", r.error);
543
+ }
544
+ this.apply((0, state_1.exit)(this.state, index, answers, this.now()), "debounced");
545
+ });
546
+ }
547
+ setProperties(properties) {
548
+ this.guard("setProperties", () => {
549
+ var _a;
550
+ const { value, problems } = (0, input_1.mergeProperties)((_a = this.state.properties) !== null && _a !== void 0 ? _a : {}, properties);
551
+ for (const [code, message] of problems)
552
+ this.diag(code, message);
553
+ this.apply((0, state_1.setProperties)(this.state, value, this.now()), "debounced");
554
+ });
555
+ }
556
+ complete() {
557
+ this.guard("complete", () => {
558
+ if (this.state.steps.length === 0)
559
+ return this.diag("no-steps", "complete: no step was entered, so there is no run to send");
560
+ this.state = (0, state_1.completeState)(this.state, this.now());
561
+ if (live === this)
562
+ live = null;
563
+ this.sendNow(true);
564
+ });
565
+ }
566
+ background() {
567
+ this.guard("background", () => {
568
+ if (this.state.status === "completed")
569
+ return deliveryFor(this.state.runId).flush();
570
+ if (this.state.steps.length === 0)
571
+ return this.diag("no-steps", "background: no step was entered yet");
572
+ this.state = { ...this.state, lastActiveAt: this.now() };
573
+ this.sendNow(true);
574
+ }, true);
575
+ }
576
+ }
577
+ return {
578
+ start(options) {
579
+ try {
580
+ if (disposed)
581
+ return inertRun(mintRunId());
582
+ let contextInput;
583
+ try {
584
+ contextInput = typeof config.context === "function" ? config.context() : config.context;
585
+ }
586
+ catch (e) {
587
+ report({ code: "invalid-start", message: `the context function threw: ${String(e)}` });
588
+ return inertRun(mintRunId());
589
+ }
590
+ const context = (0, context_1.captureRunContext)(contextInput, version_1.LIBRARY_VERSION);
591
+ const valid = (0, input_1.validateStart)(options);
592
+ const errors = [...(context.ok ? [] : context.errors), ...(valid.ok ? [] : valid.errors)];
593
+ if (!context.ok || !valid.ok) {
594
+ report({ code: "invalid-start", message: `run not started, nothing will be sent: ${errors.join("; ")}` });
595
+ return inertRun(mintRunId());
596
+ }
597
+ for (const [code, message] of valid.value.warnings)
598
+ report({ code, message });
599
+ if (live && live.state.status === "in_progress") {
600
+ report({ code: "run-replaced", runId: live.runId, message: "a new run was started; the previous one stays in progress" });
601
+ live.deactivate();
602
+ }
603
+ if (persistedCurrent)
604
+ sendAbandoned(persistedCurrent);
605
+ persistedCurrent = null;
606
+ const { onboarding, studio, manifest, properties } = valid.value;
607
+ const state = {
608
+ runId: mintRunId(),
609
+ lastSeq: 0,
610
+ onboarding,
611
+ ...(studio ? { studio } : {}),
612
+ context: context.context,
613
+ manifest,
614
+ ...(properties ? { properties } : {}),
615
+ steps: [],
616
+ provisionalExit: false,
617
+ truncated: false,
618
+ status: "in_progress",
619
+ completedAt: null,
620
+ lastActiveAt: clock.now(),
621
+ };
622
+ live = new Controller(state);
623
+ startedThisSession = true;
624
+ persist();
625
+ return live;
626
+ }
627
+ catch (e) {
628
+ report({ code: "internal-error", message: `start: ${String(e)}` });
629
+ return inertRun(fallbackUuid());
630
+ }
631
+ },
632
+ async resume() {
633
+ try {
634
+ void waitLoaded(); // reports a slow read
635
+ await loaded;
636
+ if (disposed || startedThisSession || !persistedCurrent)
637
+ return null;
638
+ const state = persistedCurrent;
639
+ persistedCurrent = null;
640
+ const now = Math.max(clock.now(), (0, state_1.floorMs)(state), state.lastActiveAt);
641
+ const controller = new Controller(state);
642
+ live = controller;
643
+ startedThisSession = true;
644
+ const result = (0, state_1.restore)(state, now);
645
+ for (const note of result.notes)
646
+ report({ code: note, runId: state.runId, message: NOTE_MESSAGES[note] });
647
+ controller.state = result.state;
648
+ persist();
649
+ controller.sendNow();
650
+ return controller;
651
+ }
652
+ catch (e) {
653
+ report({ code: "internal-error", message: `resume: ${String(e)}` });
654
+ return null;
655
+ }
656
+ },
657
+ // Waits up to storageReadTimeoutMs for the read, then as long again for queued writes.
658
+ async idle() {
659
+ try {
660
+ await idleWithin(writesDone());
661
+ }
662
+ catch {
663
+ // idle() never rejects into the host
664
+ }
665
+ },
666
+ dispose() {
667
+ var _a, _b;
668
+ if (disposed)
669
+ return;
670
+ // A change still waiting on the debounce becomes a staged snapshot first.
671
+ try {
672
+ live === null || live === void 0 ? void 0 : live.flushDebounce();
673
+ }
674
+ catch (e) {
675
+ report({ code: "internal-error", message: `dispose: ${String(e)}` });
676
+ }
677
+ // From here nothing records, nothing new starts, and no write is queued.
678
+ // What was staged before still goes through: its write is already queued,
679
+ // and its send is handed to a closed delivery (one last attempt, no retry
680
+ // timers). A snapshot the sink does not take is handed to the next
681
+ // tracker on this key, and stays in storage for the next launch.
682
+ disposed = true;
683
+ for (const d of deliveries.values())
684
+ d.close();
685
+ handoffOnKey.set(storageKey, {
686
+ sink: config.sink,
687
+ destination,
688
+ run: startedThisSession ? { current: live && live.state.status === "in_progress" ? live.state : null } : handedRun,
689
+ outboxes: new Map(outboxes),
690
+ left: leftUnsent(),
691
+ });
692
+ if (store) {
693
+ liveOnKey.set(storageKey, Math.max(0, ((_a = liveOnKey.get(storageKey)) !== null && _a !== void 0 ? _a : 1) - 1));
694
+ // The next tracker on this key reads only after these writes (bounded by idle()).
695
+ const landed = writesDone().catch(() => undefined);
696
+ const entry = { drain: idleWithin(landed).catch(() => undefined), landed, writes: [...((_b = priorWrites === null || priorWrites === void 0 ? void 0 : priorWrites.writes) !== null && _b !== void 0 ? _b : []), ...writing] };
697
+ lastDrainOnKey.set(storageKey, entry);
698
+ void Promise.all([landed, ...entry.writes]).then(() => {
699
+ if (lastDrainOnKey.get(storageKey) === entry)
700
+ lastDrainOnKey.delete(storageKey);
701
+ });
702
+ }
703
+ },
704
+ };
705
+ }
706
+ let instance = null;
707
+ /**
708
+ * The app-wide tracker. Call `configure` once at startup, then `start` (or
709
+ * `resume`) for each run. Before `configure`, `start` returns a run that
710
+ * records nothing, and warns.
711
+ */
712
+ exports.onboardingRun = {
713
+ configure(config) {
714
+ instance === null || instance === void 0 ? void 0 : instance.dispose();
715
+ instance = createOnboardingRunTracker(config);
716
+ },
717
+ start(options) {
718
+ if (instance)
719
+ return instance.start(options);
720
+ (0, diagnostics_1.consoleDiagnostics)({ code: "not-configured", message: "onboardingRun.start was called before onboardingRun.configure: nothing is tracked" });
721
+ return inertRun("00000000-0000-0000-0000-000000000000");
722
+ },
723
+ resume() {
724
+ return instance ? instance.resume() : Promise.resolve(null);
725
+ },
726
+ idle() {
727
+ return instance ? instance.idle() : Promise.resolve();
728
+ },
729
+ dispose() {
730
+ instance === null || instance === void 0 ? void 0 : instance.dispose();
731
+ instance = null;
732
+ },
733
+ };
734
+ //# sourceMappingURL=tracker.js.map