@interfere/react 0.0.1 → 0.0.2-alpha.1

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 (198) hide show
  1. package/dist/client.d.mts +15 -0
  2. package/dist/client.d.mts.map +1 -0
  3. package/dist/client.mjs +75 -0
  4. package/dist/client.mjs.map +1 -0
  5. package/dist/core/events/event-registry.d.mts +23 -0
  6. package/dist/core/events/event-registry.d.mts.map +1 -0
  7. package/dist/core/events/event-registry.mjs +32 -0
  8. package/dist/core/events/event-registry.mjs.map +1 -0
  9. package/dist/core/events/plugin-event-types.d.mts +92 -0
  10. package/dist/core/events/plugin-event-types.d.mts.map +1 -0
  11. package/dist/core/events/plugin-event-types.mjs +25 -0
  12. package/dist/core/events/plugin-event-types.mjs.map +1 -0
  13. package/dist/core/plugins/dom-utils.d.mts +9 -0
  14. package/dist/core/plugins/dom-utils.d.mts.map +1 -0
  15. package/dist/core/plugins/dom-utils.mjs +25 -0
  16. package/dist/core/plugins/dom-utils.mjs.map +1 -0
  17. package/dist/core/plugins/impl/ai-summary.d.mts +6 -0
  18. package/dist/core/plugins/impl/ai-summary.d.mts.map +1 -0
  19. package/dist/core/plugins/impl/ai-summary.mjs +122 -0
  20. package/dist/core/plugins/impl/ai-summary.mjs.map +1 -0
  21. package/dist/core/plugins/impl/errors.d.mts +9 -0
  22. package/dist/core/plugins/impl/errors.d.mts.map +1 -0
  23. package/dist/core/plugins/impl/errors.mjs +153 -0
  24. package/dist/core/plugins/impl/errors.mjs.map +1 -0
  25. package/dist/core/plugins/impl/page-events.d.mts +15 -0
  26. package/dist/core/plugins/impl/page-events.d.mts.map +1 -0
  27. package/dist/core/plugins/impl/page-events.mjs +131 -0
  28. package/dist/core/plugins/impl/page-events.mjs.map +1 -0
  29. package/dist/core/plugins/impl/rage-click.d.mts +6 -0
  30. package/dist/core/plugins/impl/rage-click.d.mts.map +1 -0
  31. package/dist/core/plugins/impl/rage-click.mjs +53 -0
  32. package/dist/core/plugins/impl/rage-click.mjs.map +1 -0
  33. package/dist/core/plugins/impl/replay.d.mts +9 -0
  34. package/dist/core/plugins/impl/replay.d.mts.map +1 -0
  35. package/dist/core/plugins/impl/replay.mjs +144 -0
  36. package/dist/core/plugins/impl/replay.mjs.map +1 -0
  37. package/dist/core/plugins/impl/server-tracing.d.mts +7 -0
  38. package/dist/core/plugins/impl/server-tracing.d.mts.map +1 -0
  39. package/dist/core/plugins/impl/server-tracing.mjs +160 -0
  40. package/dist/core/plugins/impl/server-tracing.mjs.map +1 -0
  41. package/dist/core/plugins/plugin-event-system.d.mts +47 -0
  42. package/dist/core/plugins/plugin-event-system.d.mts.map +1 -0
  43. package/dist/core/plugins/plugin-event-system.mjs +75 -0
  44. package/dist/core/plugins/plugin-event-system.mjs.map +1 -0
  45. package/dist/core/plugins/plugin-loader.d.mts +22 -0
  46. package/dist/core/plugins/plugin-loader.d.mts.map +1 -0
  47. package/dist/core/plugins/plugin-loader.mjs +142 -0
  48. package/dist/core/plugins/plugin-loader.mjs.map +1 -0
  49. package/dist/core/runtime/config.d.mts +14 -0
  50. package/dist/core/runtime/config.d.mts.map +1 -0
  51. package/dist/core/runtime/config.mjs +39 -0
  52. package/dist/core/runtime/config.mjs.map +1 -0
  53. package/dist/core/runtime/context.d.mts +25 -0
  54. package/dist/core/runtime/context.d.mts.map +1 -0
  55. package/dist/core/runtime/context.mjs +48 -0
  56. package/dist/core/runtime/context.mjs.map +1 -0
  57. package/dist/core/runtime/ingest-target.d.mts +10 -0
  58. package/dist/core/runtime/ingest-target.d.mts.map +1 -0
  59. package/dist/core/runtime/ingest-target.mjs +15 -0
  60. package/dist/core/runtime/ingest-target.mjs.map +1 -0
  61. package/dist/core/schemas.d.mts +85 -0
  62. package/dist/core/schemas.d.mts.map +1 -0
  63. package/dist/core/schemas.mjs +1 -0
  64. package/dist/effect/build-envelope.d.mts +9 -0
  65. package/dist/effect/build-envelope.d.mts.map +1 -0
  66. package/dist/effect/build-envelope.mjs +29 -0
  67. package/dist/effect/build-envelope.mjs.map +1 -0
  68. package/dist/effect/errors.d.mts +36 -0
  69. package/dist/effect/errors.d.mts.map +1 -0
  70. package/dist/effect/errors.mjs +10 -0
  71. package/dist/effect/errors.mjs.map +1 -0
  72. package/dist/effect/layers/config.layer.d.mts +13 -0
  73. package/dist/effect/layers/config.layer.d.mts.map +1 -0
  74. package/dist/effect/layers/config.layer.mjs +21 -0
  75. package/dist/effect/layers/config.layer.mjs.map +1 -0
  76. package/dist/effect/layers/context.layer.d.mts +12 -0
  77. package/dist/effect/layers/context.layer.d.mts.map +1 -0
  78. package/dist/effect/layers/context.layer.mjs +14 -0
  79. package/dist/effect/layers/context.layer.mjs.map +1 -0
  80. package/dist/effect/layers/http.layer.d.mts +21 -0
  81. package/dist/effect/layers/http.layer.d.mts.map +1 -0
  82. package/dist/effect/layers/http.layer.mjs +113 -0
  83. package/dist/effect/layers/http.layer.mjs.map +1 -0
  84. package/dist/effect/layers/queue.layer.d.mts +30 -0
  85. package/dist/effect/layers/queue.layer.d.mts.map +1 -0
  86. package/dist/effect/layers/queue.layer.mjs +232 -0
  87. package/dist/effect/layers/queue.layer.mjs.map +1 -0
  88. package/dist/effect/layers/session.layer.d.mts +26 -0
  89. package/dist/effect/layers/session.layer.d.mts.map +1 -0
  90. package/dist/effect/layers/session.layer.mjs +126 -0
  91. package/dist/effect/layers/session.layer.mjs.map +1 -0
  92. package/dist/effect/layers/storage.layer.d.mts +19 -0
  93. package/dist/effect/layers/storage.layer.d.mts.map +1 -0
  94. package/dist/effect/layers/storage.layer.mjs +200 -0
  95. package/dist/effect/layers/storage.layer.mjs.map +1 -0
  96. package/dist/effect/layers/tracer.layer.d.mts +9 -0
  97. package/dist/effect/layers/tracer.layer.d.mts.map +1 -0
  98. package/dist/effect/layers/tracer.layer.mjs +11 -0
  99. package/dist/effect/layers/tracer.layer.mjs.map +1 -0
  100. package/dist/effect/runtime-services.d.mts +22 -0
  101. package/dist/effect/runtime-services.d.mts.map +1 -0
  102. package/dist/effect/runtime-services.mjs +76 -0
  103. package/dist/effect/runtime-services.mjs.map +1 -0
  104. package/dist/effect/tags.d.mts +50 -0
  105. package/dist/effect/tags.d.mts.map +1 -0
  106. package/dist/effect/tags.mjs +7 -0
  107. package/dist/effect/tags.mjs.map +1 -0
  108. package/dist/hooks/use-runtime-and-plugins.d.mts +7 -0
  109. package/dist/hooks/use-runtime-and-plugins.d.mts.map +1 -0
  110. package/dist/hooks/use-runtime-and-plugins.mjs +153 -0
  111. package/dist/hooks/use-runtime-and-plugins.mjs.map +1 -0
  112. package/dist/hooks/use-session.d.mts +40 -0
  113. package/dist/hooks/use-session.d.mts.map +1 -0
  114. package/dist/hooks/use-session.mjs +96 -0
  115. package/dist/hooks/use-session.mjs.map +1 -0
  116. package/dist/package.mjs +100 -0
  117. package/dist/package.mjs.map +1 -0
  118. package/dist/provider.d.mts +17 -0
  119. package/dist/provider.d.mts.map +1 -0
  120. package/dist/provider.mjs +26 -0
  121. package/dist/provider.mjs.map +1 -0
  122. package/dist/server/auth.d.mts +11 -0
  123. package/dist/server/auth.d.mts.map +1 -0
  124. package/dist/server/auth.mjs +36 -0
  125. package/dist/server/auth.mjs.map +1 -0
  126. package/dist/server/capture.d.mts +18 -0
  127. package/dist/server/capture.d.mts.map +1 -0
  128. package/dist/server/capture.mjs +105 -0
  129. package/dist/server/capture.mjs.map +1 -0
  130. package/package.json +60 -27
  131. package/dist/__tests__/client.test.d.ts +0 -2
  132. package/dist/__tests__/client.test.d.ts.map +0 -1
  133. package/dist/__tests__/client.test.js +0 -447
  134. package/dist/__tests__/client.test.js.map +0 -1
  135. package/dist/__tests__/lib/core/error-handlers.test.d.ts +0 -2
  136. package/dist/__tests__/lib/core/error-handlers.test.d.ts.map +0 -1
  137. package/dist/__tests__/lib/core/error-handlers.test.js +0 -596
  138. package/dist/__tests__/lib/core/error-handlers.test.js.map +0 -1
  139. package/dist/__tests__/lib/core/event-queue.test.d.ts +0 -2
  140. package/dist/__tests__/lib/core/event-queue.test.d.ts.map +0 -1
  141. package/dist/__tests__/lib/core/event-queue.test.js +0 -290
  142. package/dist/__tests__/lib/core/event-queue.test.js.map +0 -1
  143. package/dist/__tests__/lib/core/runtime.test.d.ts +0 -2
  144. package/dist/__tests__/lib/core/runtime.test.d.ts.map +0 -1
  145. package/dist/__tests__/lib/core/runtime.test.js +0 -133
  146. package/dist/__tests__/lib/core/runtime.test.js.map +0 -1
  147. package/dist/__tests__/lib/core/session-manager.test.d.ts +0 -2
  148. package/dist/__tests__/lib/core/session-manager.test.d.ts.map +0 -1
  149. package/dist/__tests__/lib/core/session-manager.test.js +0 -356
  150. package/dist/__tests__/lib/core/session-manager.test.js.map +0 -1
  151. package/dist/__tests__/provider.test.d.ts +0 -2
  152. package/dist/__tests__/provider.test.d.ts.map +0 -1
  153. package/dist/__tests__/provider.test.js +0 -143
  154. package/dist/__tests__/provider.test.js.map +0 -1
  155. package/dist/client.d.ts +0 -78
  156. package/dist/client.d.ts.map +0 -1
  157. package/dist/client.js +0 -219
  158. package/dist/client.js.map +0 -1
  159. package/dist/index.d.ts +0 -6
  160. package/dist/index.d.ts.map +0 -1
  161. package/dist/index.js +0 -5
  162. package/dist/index.js.map +0 -1
  163. package/dist/lib/core/error-handlers.d.ts +0 -14
  164. package/dist/lib/core/error-handlers.d.ts.map +0 -1
  165. package/dist/lib/core/error-handlers.js +0 -191
  166. package/dist/lib/core/error-handlers.js.map +0 -1
  167. package/dist/lib/core/event-queue.d.ts +0 -90
  168. package/dist/lib/core/event-queue.d.ts.map +0 -1
  169. package/dist/lib/core/event-queue.js +0 -286
  170. package/dist/lib/core/event-queue.js.map +0 -1
  171. package/dist/lib/core/runtime.d.ts +0 -7
  172. package/dist/lib/core/runtime.d.ts.map +0 -1
  173. package/dist/lib/core/runtime.js +0 -16
  174. package/dist/lib/core/runtime.js.map +0 -1
  175. package/dist/lib/core/session-manager.d.ts +0 -96
  176. package/dist/lib/core/session-manager.d.ts.map +0 -1
  177. package/dist/lib/core/session-manager.js +0 -431
  178. package/dist/lib/core/session-manager.js.map +0 -1
  179. package/dist/lib/persistence/storage.d.ts +0 -5
  180. package/dist/lib/persistence/storage.d.ts.map +0 -1
  181. package/dist/lib/persistence/storage.js +0 -67
  182. package/dist/lib/persistence/storage.js.map +0 -1
  183. package/dist/lib/session/rage-click.d.ts +0 -2
  184. package/dist/lib/session/rage-click.d.ts.map +0 -1
  185. package/dist/lib/session/rage-click.js +0 -51
  186. package/dist/lib/session/rage-click.js.map +0 -1
  187. package/dist/lib/session/replay.d.ts +0 -3
  188. package/dist/lib/session/replay.d.ts.map +0 -1
  189. package/dist/lib/session/replay.js +0 -106
  190. package/dist/lib/session/replay.js.map +0 -1
  191. package/dist/lib/session/session-summary.d.ts +0 -3
  192. package/dist/lib/session/session-summary.d.ts.map +0 -1
  193. package/dist/lib/session/session-summary.js +0 -79
  194. package/dist/lib/session/session-summary.js.map +0 -1
  195. package/dist/provider.d.ts +0 -76
  196. package/dist/provider.d.ts.map +0 -1
  197. package/dist/provider.js +0 -138
  198. package/dist/provider.js.map +0 -1
@@ -0,0 +1,232 @@
1
+ import { ConfigTag } from "../tags.mjs";
2
+ import { HttpServiceTag } from "./http.layer.mjs";
3
+ import { createStorageAdapter } from "./storage.layer.mjs";
4
+ import { Chunk, Context, Duration, Effect, Layer, Queue, Ref, Schedule } from "effect";
5
+ import { CircuitBreakerOpen, DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreaker } from "@interfere/effect-utils/circuit-breaker";
6
+ import { InterfereLogger } from "@interfere/effect-utils/observability";
7
+ import { DEFAULT_RETRY_INITIAL_MS, DEFAULT_RETRY_MAX_MS, DEFAULT_RETRY_RECURS, createBackoffSchedule } from "@interfere/effect-utils/retry";
8
+
9
+ //#region src/effect/layers/queue.layer.ts
10
+ const MAX_RETRY_ATTEMPTS = 3;
11
+ const DEFER_SEND_DELAY_MS = 250;
12
+ const BREAKER_RETRY_BASE_MS = 5e3;
13
+ const BREAKER_RETRY_MAX_MS = 3e4;
14
+ const BREAKER_JITTER_FACTOR = .3;
15
+ const BREAKER_COALESCE_REMAINING_MS = 250;
16
+ const TEST_OFFLINE_REQUEUE_LIMIT = 3;
17
+ const OFFLINE_RETRY_BASE_MS = DEFAULT_RETRY_INITIAL_MS;
18
+ const OFFLINE_RETRY_MAX_MS = DEFAULT_RETRY_MAX_MS;
19
+ const OFFLINE_JITTER_FACTOR = .3;
20
+ var QueueServiceTag = class extends Context.Tag("@interfere/react/Queue")() {};
21
+ /**
22
+ * Creates the retry schedule for failed batches
23
+ */
24
+ const createRetrySchedule = () => createBackoffSchedule(DEFAULT_RETRY_INITIAL_MS, DEFAULT_RETRY_MAX_MS, MAX_RETRY_ATTEMPTS ?? DEFAULT_RETRY_RECURS);
25
+ /**
26
+ * Checks if the browser is online
27
+ */
28
+ const isOnline = () => Effect.sync(() => typeof navigator !== "undefined" ? navigator.onLine : true);
29
+ /**
30
+ * Creates the queue service layer
31
+ */
32
+ const QueueServiceLive = Layer.scoped(QueueServiceTag, Effect.gen(function* () {
33
+ const config = yield* ConfigTag;
34
+ const http = yield* HttpServiceTag;
35
+ const storage = createStorageAdapter();
36
+ const capacity = storage.capacity();
37
+ const circuitBreaker = yield* createCircuitBreaker(DEFAULT_CIRCUIT_BREAKER_CONFIG);
38
+ const metrics = yield* Ref.make({
39
+ queueDepth: 0,
40
+ successCount: 0,
41
+ failureCount: 0,
42
+ circuitBreakerState: "closed"
43
+ });
44
+ const envelopeQueue = yield* Queue.sliding(capacity);
45
+ const readyRef = yield* Ref.make(false);
46
+ const breakerRetryCount = yield* Ref.make(0);
47
+ yield* Effect.gen(function* () {
48
+ const initial = yield* storage.load().pipe(Effect.catchAll(() => Effect.succeed([])));
49
+ const filtered = initial.filter((e) => typeof e.uuid === "string" && e.uuid.length > 0);
50
+ if (filtered.length !== initial.length) {
51
+ yield* storage.save(filtered).pipe(Effect.ignore);
52
+ yield* Effect.logWarning("Purged persisted envelopes missing UUID").pipe(Effect.annotateLogs({
53
+ before: initial.length,
54
+ after: filtered.length,
55
+ removed: initial.length - filtered.length
56
+ }));
57
+ }
58
+ if (filtered.length > 0) {
59
+ const seed = filtered.length > capacity ? filtered.slice(filtered.length - capacity) : filtered;
60
+ yield* Queue.offerAll(envelopeQueue, Chunk.fromIterable(seed));
61
+ if (seed.length !== filtered.length) yield* storage.save(seed).pipe(Effect.ignore);
62
+ yield* Ref.update(metrics, (m) => ({
63
+ ...m,
64
+ queueDepth: seed.length
65
+ }));
66
+ yield* Effect.logDebug("Loaded persisted envelopes").pipe(Effect.annotateLogs({
67
+ count: seed.length,
68
+ truncated: seed.length !== filtered.length
69
+ }));
70
+ }
71
+ });
72
+ const requeueWithLog = (batch, message, extra) => Effect.gen(function* () {
73
+ yield* Queue.offerAll(envelopeQueue, batch);
74
+ yield* Ref.update(metrics, (m) => ({
75
+ ...m,
76
+ queueDepth: m.queueDepth + Chunk.size(batch)
77
+ }));
78
+ yield* Effect.logWarning(message).pipe(Effect.annotateLogs({
79
+ batchSize: Chunk.size(batch),
80
+ ...extra ?? {}
81
+ }));
82
+ });
83
+ const performHttpSend = (envelopes, ids) => http.postEnvelopes(envelopes).pipe(Effect.tap(() => storage.removeByIds(ids).pipe(Effect.ignore)), Effect.tap(() => Ref.update(metrics, (m) => ({
84
+ ...m,
85
+ successCount: m.successCount + envelopes.length
86
+ }))), Effect.tap(() => Effect.logDebug("Batch sent successfully").pipe(Effect.annotateLogs({ batchSize: envelopes.length }))), Effect.catchTag("HttpError", (error) => Effect.gen(function* () {
87
+ if (error.status === 422) {
88
+ yield* storage.removeByIds(ids).pipe(Effect.ignore);
89
+ yield* Ref.update(metrics, (m) => ({
90
+ ...m,
91
+ failureCount: m.failureCount + envelopes.length
92
+ }));
93
+ yield* Effect.logWarning("Dropping invalid envelopes after 422 response").pipe(Effect.annotateLogs({
94
+ batchSize: envelopes.length,
95
+ status: error.status,
96
+ url: error.url,
97
+ body: error.body
98
+ }));
99
+ return;
100
+ }
101
+ yield* Effect.all([Effect.logWarning("Failed to send batch").pipe(Effect.annotateLogs({
102
+ batchSize: envelopes.length,
103
+ status: error.status,
104
+ url: error.url,
105
+ body: error.body,
106
+ error: String(error)
107
+ })), Ref.update(metrics, (m) => ({
108
+ ...m,
109
+ failureCount: m.failureCount + 1
110
+ }))]);
111
+ return yield* Effect.fail(error);
112
+ })), Effect.retry(createRetrySchedule()));
113
+ const sendWithBreaker = (envelopes, ids, batch) => circuitBreaker.protect(performHttpSend(envelopes, ids)).pipe(Effect.catchIf((error) => error instanceof CircuitBreakerOpen, () => Effect.gen(function* () {
114
+ const retryCount = yield* Ref.getAndUpdate(breakerRetryCount, (n) => n + 1);
115
+ const backoffMs = Math.min(BREAKER_RETRY_BASE_MS * 2 ** retryCount, BREAKER_RETRY_MAX_MS);
116
+ const jitter = Math.random() * BREAKER_JITTER_FACTOR * backoffMs;
117
+ const sleepMs = Math.floor(backoffMs + jitter);
118
+ const data = yield* circuitBreaker.breakerData.get;
119
+ const now = Date.now();
120
+ const since = data.lastFailureTime ? now - data.lastFailureTime : 0;
121
+ yield* requeueWithLog(batch, "Circuit breaker prevented batch send - requeueing", {
122
+ remainingMs: Math.max(0, DEFAULT_CIRCUIT_BREAKER_CONFIG.resetMs - since),
123
+ retryCount,
124
+ nextRetryMs: sleepMs
125
+ });
126
+ yield* Effect.sleep(Duration.millis(sleepMs));
127
+ })), Effect.tap(() => Ref.set(breakerRetryCount, 0)), Effect.catchAll((error) => requeueWithLog(batch, "Failed to send batch after all retries", { error: String(error) })), Effect.tap(() => Effect.gen(function* () {
128
+ const state = yield* circuitBreaker.getState;
129
+ yield* Ref.update(metrics, (m) => ({
130
+ ...m,
131
+ circuitBreakerState: state
132
+ }));
133
+ })), Effect.withSpan("queue.sendBatch", { attributes: { batchSize: envelopes.length } }));
134
+ const offlineAttemptsRef = yield* Ref.make(0);
135
+ const handleOffline = (batch) => Effect.gen(function* () {
136
+ const attempts = yield* Ref.getAndUpdate(offlineAttemptsRef, (n) => n + 1);
137
+ if (typeof process !== "undefined" && process.env?.VITEST && attempts >= TEST_OFFLINE_REQUEUE_LIMIT) {
138
+ yield* Effect.logWarning(`Offline - dropping batch after ${TEST_OFFLINE_REQUEUE_LIMIT} attempts (test mode)`);
139
+ return;
140
+ }
141
+ yield* requeueWithLog(batch, "Offline - requeueing batch");
142
+ const base = OFFLINE_RETRY_BASE_MS;
143
+ const capped = Math.min(base * 2 ** attempts, OFFLINE_RETRY_MAX_MS);
144
+ const jitter = Math.random() * OFFLINE_JITTER_FACTOR * capped;
145
+ const sleepMs = Math.floor(capped + jitter);
146
+ yield* Effect.sleep(Duration.millis(sleepMs));
147
+ });
148
+ const sendBatch = (batch) => Effect.gen(function* () {
149
+ if (Chunk.isEmpty(batch)) return;
150
+ if (!(yield* isOnline())) {
151
+ yield* handleOffline(batch);
152
+ return;
153
+ }
154
+ const envelopes = Chunk.toArray(batch);
155
+ yield* sendWithBreaker(envelopes, envelopes.map((e) => e.uuid).filter((v) => typeof v === "string"), batch);
156
+ });
157
+ const flush = () => Effect.gen(function* () {
158
+ const items = yield* Queue.takeAll(envelopeQueue);
159
+ yield* Ref.update(metrics, (m) => ({
160
+ ...m,
161
+ queueDepth: 0
162
+ }));
163
+ if (Chunk.isNonEmpty(items)) yield* sendBatch(items).pipe(Effect.catchAll(() => Effect.void));
164
+ }).pipe(Effect.withSpan("queue.flush"));
165
+ const processBatch = Effect.gen(function* () {
166
+ if (!(yield* Ref.get(readyRef))) {
167
+ yield* Effect.sleep(Duration.millis(DEFER_SEND_DELAY_MS));
168
+ return;
169
+ }
170
+ const breakerState = yield* circuitBreaker.getState;
171
+ if (breakerState === "open") {
172
+ const data = yield* circuitBreaker.breakerData.get;
173
+ const now = Date.now();
174
+ const since = data.lastFailureTime ? now - data.lastFailureTime : 0;
175
+ const remainingMs = Math.max(0, DEFAULT_CIRCUIT_BREAKER_CONFIG.resetMs - since);
176
+ yield* Ref.update(metrics, (m) => ({
177
+ ...m,
178
+ circuitBreakerState: breakerState
179
+ }));
180
+ if (remainingMs > BREAKER_COALESCE_REMAINING_MS) {
181
+ yield* Effect.logDebug("Circuit breaker open - coalescing batches until cooldown is small").pipe(Effect.annotateLogs({ remainingMs }));
182
+ yield* Effect.sleep(Duration.millis(remainingMs - BREAKER_COALESCE_REMAINING_MS));
183
+ return;
184
+ }
185
+ }
186
+ const batchSize = config.batch.size;
187
+ const batchMs = config.batch.ms;
188
+ const pending = yield* Queue.takeBetween(envelopeQueue, 1, batchSize).pipe(Effect.timeout(Duration.millis(batchMs)), Effect.catchTag("TimeoutException", () => Queue.takeAll(envelopeQueue).pipe(Effect.map((items) => Chunk.isEmpty(items) ? Chunk.empty() : items))));
189
+ yield* Ref.update(metrics, (m) => ({
190
+ ...m,
191
+ queueDepth: Math.max(0, m.queueDepth - Chunk.size(pending))
192
+ }));
193
+ if (Chunk.isNonEmpty(pending)) {
194
+ yield* Effect.logDebug("Processing batch").pipe(Effect.annotateLogs({ batchSize: Chunk.size(pending) }));
195
+ yield* sendBatch(pending);
196
+ }
197
+ });
198
+ const startBatchProcessor = () => processBatch.pipe(Effect.catchAllCause((cause) => Effect.logError("Batch processor error", { cause: String(cause) })), Effect.repeat(Schedule.forever), Effect.provide(InterfereLogger.layer()), Effect.forkScoped, Effect.as(Effect.void));
199
+ const add = (envelope) => Effect.gen(function* () {
200
+ const size$1 = yield* Queue.size(envelopeQueue);
201
+ if (size$1 >= capacity) {
202
+ const toDrop = Math.max(1, size$1 - capacity + 1);
203
+ yield* storage.dropOldest(toDrop).pipe(Effect.ignore);
204
+ }
205
+ yield* storage.append([envelope]).pipe(Effect.ignore);
206
+ yield* Queue.offer(envelopeQueue, envelope);
207
+ yield* Ref.update(metrics, (m) => ({
208
+ ...m,
209
+ queueDepth: m.queueDepth + 1
210
+ }));
211
+ yield* Effect.logDebug(`[${envelope.type}] Envelope '${envelope.uuid}' added to queue`).pipe(Effect.annotateLogs({
212
+ envelopeId: envelope.uuid,
213
+ queueSize: size$1 + 1
214
+ }));
215
+ }).pipe(Effect.withSpan("queue.add"));
216
+ const size = () => Ref.get(metrics).pipe(Effect.map((m) => m.queueDepth));
217
+ const boundedFlush = (timeoutMs) => Effect.race(flush(), Effect.sleep(Duration.millis(timeoutMs))).pipe(Effect.asVoid);
218
+ const setReady = (ready) => Ref.set(readyRef, ready).pipe(Effect.zipRight(ready ? Effect.logDebug("Queue ready - enabling batch sends") : Effect.logDebug("Queue not ready - deferring batch sends")));
219
+ const getMetrics = () => Ref.get(metrics);
220
+ return {
221
+ add,
222
+ size,
223
+ flush,
224
+ boundedFlush,
225
+ startBatchProcessor,
226
+ setReady,
227
+ getMetrics
228
+ };
229
+ }));
230
+
231
+ //#endregion
232
+ export { QueueServiceLive, QueueServiceTag };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.layer.mjs","names":["storage: StorageAdapter","size"],"sources":["../../../src/effect/layers/queue.layer.ts"],"sourcesContent":["import {\n CircuitBreakerOpen,\n type CircuitBreakerState,\n createCircuitBreaker,\n DEFAULT_CIRCUIT_BREAKER_CONFIG,\n} from \"@interfere/effect-utils/circuit-breaker\";\nimport { InterfereLogger } from \"@interfere/effect-utils/observability\";\nimport {\n createBackoffSchedule,\n DEFAULT_RETRY_INITIAL_MS,\n DEFAULT_RETRY_MAX_MS,\n DEFAULT_RETRY_RECURS,\n} from \"@interfere/effect-utils/retry\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport {\n Chunk,\n Context,\n Duration,\n Effect,\n Layer,\n Queue,\n Ref,\n Schedule,\n type Scope,\n} from \"effect\";\n\nimport type { HttpError } from \"../errors.js\";\nimport { ConfigTag } from \"../tags.js\";\nimport { HttpServiceTag } from \"./http.layer.js\";\nimport { createStorageAdapter, type StorageAdapter } from \"./storage.layer.js\";\n\n// Configuration constants\nconst MAX_RETRY_ATTEMPTS = 3; // Short-term retries for transient failures\nconst DEFER_SEND_DELAY_MS = 250; // Delay before sending when not ready\nconst BREAKER_RETRY_BASE_MS = 5000; // Base retry delay when breaker is open\nconst BREAKER_RETRY_MAX_MS = 30_000; // Max retry delay when breaker is open\nconst BREAKER_JITTER_FACTOR = 0.3; // 30% jitter for exponential backoff\nconst BREAKER_COALESCE_REMAINING_MS = 250; // Skip sending while breaker has more than this remaining\nconst TEST_OFFLINE_REQUEUE_LIMIT = 3; // Test-only cap to avoid infinite loops when offline\nconst OFFLINE_RETRY_BASE_MS = DEFAULT_RETRY_INITIAL_MS; // Base offline backoff\nconst OFFLINE_RETRY_MAX_MS = DEFAULT_RETRY_MAX_MS; // Cap offline backoff\nconst OFFLINE_JITTER_FACTOR = 0.3; // 30% jitter for offline backoff\n\nexport interface QueueMetrics {\n readonly queueDepth: number;\n readonly successCount: number;\n readonly failureCount: number;\n readonly circuitBreakerState: CircuitBreakerState;\n}\n\nexport interface QueueService {\n readonly add: (envelope: Envelope) => Effect.Effect<void>;\n readonly size: () => Effect.Effect<number>;\n readonly flush: () => Effect.Effect<void>;\n readonly boundedFlush: (timeoutMs: number) => Effect.Effect<void>;\n readonly startBatchProcessor: () => Effect.Effect<void, never, Scope.Scope>;\n readonly setReady: (ready: boolean) => Effect.Effect<void>;\n readonly getMetrics: () => Effect.Effect<QueueMetrics>;\n}\n\nexport class QueueServiceTag extends Context.Tag(\"@interfere/react/Queue\")<\n QueueServiceTag,\n QueueService\n>() {}\n\n/**\n * Creates the retry schedule for failed batches\n */\nconst createRetrySchedule = () =>\n createBackoffSchedule(\n DEFAULT_RETRY_INITIAL_MS,\n DEFAULT_RETRY_MAX_MS,\n MAX_RETRY_ATTEMPTS ?? DEFAULT_RETRY_RECURS\n );\n\n/**\n * Checks if the browser is online\n */\nconst isOnline = () =>\n Effect.sync(() =>\n typeof navigator !== \"undefined\" ? navigator.onLine : true\n );\n\n/**\n * Creates the queue service layer\n */\nexport const QueueServiceLive = Layer.scoped(\n QueueServiceTag,\n Effect.gen(function* () {\n const config = yield* ConfigTag;\n const http = yield* HttpServiceTag;\n\n // Create storage first to get its capacity\n const storage: StorageAdapter = createStorageAdapter();\n const capacity = storage.capacity();\n\n // Create the circuit breaker with default config\n const circuitBreaker = yield* createCircuitBreaker(\n DEFAULT_CIRCUIT_BREAKER_CONFIG\n );\n\n const metrics = yield* Ref.make<QueueMetrics>({\n queueDepth: 0,\n successCount: 0,\n failureCount: 0,\n circuitBreakerState: \"closed\",\n });\n\n const envelopeQueue = yield* Queue.sliding<Envelope>(capacity);\n const readyRef = yield* Ref.make<boolean>(false);\n const breakerRetryCount = yield* Ref.make<number>(0);\n\n yield* Effect.gen(function* () {\n const initial = yield* storage\n .load()\n .pipe(Effect.catchAll(() => Effect.succeed<Envelope[]>([])));\n\n // Filter out any entries missing required identifiers\n const filtered = initial.filter(\n (e) => typeof e.uuid === \"string\" && e.uuid.length > 0\n );\n\n if (filtered.length !== initial.length) {\n // Persist the cleaned set best-effort\n yield* storage.save(filtered).pipe(Effect.ignore);\n yield* Effect.logWarning(\n \"Purged persisted envelopes missing UUID\"\n ).pipe(\n Effect.annotateLogs({\n before: initial.length,\n after: filtered.length,\n removed: initial.length - filtered.length,\n })\n );\n }\n\n if (filtered.length > 0) {\n const seed =\n filtered.length > capacity\n ? filtered.slice(filtered.length - capacity)\n : filtered;\n\n yield* Queue.offerAll(envelopeQueue, Chunk.fromIterable(seed));\n\n // Ensure storage mirrors in-memory when trimming\n if (seed.length !== filtered.length) {\n yield* storage.save(seed).pipe(Effect.ignore);\n }\n\n yield* Ref.update(metrics, (m) => ({ ...m, queueDepth: seed.length }));\n\n yield* Effect.logDebug(\"Loaded persisted envelopes\").pipe(\n Effect.annotateLogs({\n count: seed.length,\n truncated: seed.length !== filtered.length,\n })\n );\n }\n });\n\n const requeueWithLog = (\n batch: Chunk.Chunk<Envelope>,\n message: string,\n extra?: Record<string, unknown>\n ) =>\n Effect.gen(function* () {\n yield* Queue.offerAll(envelopeQueue, batch);\n yield* Ref.update(metrics, (m) => ({\n ...m,\n queueDepth: m.queueDepth + Chunk.size(batch),\n }));\n yield* Effect.logWarning(message).pipe(\n Effect.annotateLogs({\n batchSize: Chunk.size(batch),\n ...(extra ?? {}),\n })\n );\n });\n\n const performHttpSend = (envelopes: Envelope[], ids: string[]) =>\n http.postEnvelopes(envelopes).pipe(\n Effect.tap(() => storage.removeByIds(ids).pipe(Effect.ignore)),\n Effect.tap(() =>\n Ref.update(metrics, (m) => ({\n ...m,\n successCount: m.successCount + envelopes.length,\n }))\n ),\n Effect.tap(() =>\n Effect.logDebug(\"Batch sent successfully\").pipe(\n Effect.annotateLogs({\n batchSize: envelopes.length,\n })\n )\n ),\n Effect.catchTag(\"HttpError\", (error: HttpError) =>\n Effect.gen(function* () {\n // If the server says the envelopes are invalid (schema mismatch),\n // drop this batch permanently instead of retrying.\n if (error.status === 422) {\n yield* storage.removeByIds(ids).pipe(Effect.ignore);\n yield* Ref.update(metrics, (m) => ({\n ...m,\n failureCount: m.failureCount + envelopes.length,\n }));\n yield* Effect.logWarning(\n \"Dropping invalid envelopes after 422 response\"\n ).pipe(\n Effect.annotateLogs({\n batchSize: envelopes.length,\n status: error.status,\n url: error.url,\n body: error.body,\n })\n );\n return;\n }\n\n // For all other HTTP errors, log and retry as before\n yield* Effect.all([\n Effect.logWarning(\"Failed to send batch\").pipe(\n Effect.annotateLogs({\n batchSize: envelopes.length,\n status: error.status,\n url: error.url,\n body: error.body,\n error: String(error),\n })\n ),\n Ref.update(metrics, (m) => ({\n ...m,\n failureCount: m.failureCount + 1,\n })),\n ]);\n\n // Re‑fail so the retry logic still kicks in\n return yield* Effect.fail(error);\n })\n ),\n Effect.retry(createRetrySchedule())\n );\n\n const sendWithBreaker = (\n envelopes: Envelope[],\n ids: string[],\n batch: Chunk.Chunk<Envelope>\n ) =>\n circuitBreaker.protect(performHttpSend(envelopes, ids)).pipe(\n Effect.catchIf(\n (error): error is CircuitBreakerOpen =>\n error instanceof CircuitBreakerOpen,\n () =>\n Effect.gen(function* () {\n const retryCount = yield* Ref.getAndUpdate(\n breakerRetryCount,\n (n) => n + 1\n );\n\n // Calculate exponential backoff with jitter\n const backoffMs = Math.min(\n BREAKER_RETRY_BASE_MS * 2 ** retryCount,\n BREAKER_RETRY_MAX_MS\n );\n const jitter = Math.random() * BREAKER_JITTER_FACTOR * backoffMs;\n const sleepMs = Math.floor(backoffMs + jitter);\n\n // Get remaining time until breaker might reset\n const data = yield* circuitBreaker.breakerData.get;\n const now = Date.now();\n const since = data.lastFailureTime\n ? now - data.lastFailureTime\n : 0;\n const remainingMs = Math.max(\n 0,\n DEFAULT_CIRCUIT_BREAKER_CONFIG.resetMs - since\n );\n\n yield* requeueWithLog(\n batch,\n \"Circuit breaker prevented batch send - requeueing\",\n {\n remainingMs,\n retryCount,\n nextRetryMs: sleepMs,\n }\n );\n\n yield* Effect.sleep(Duration.millis(sleepMs));\n })\n ),\n Effect.tap(() =>\n // Reset retry count on successful send or after final failure\n Ref.set(breakerRetryCount, 0)\n ),\n Effect.catchAll((error) =>\n requeueWithLog(batch, \"Failed to send batch after all retries\", {\n error: String(error),\n })\n ),\n // Always update circuit breaker state in metrics\n Effect.tap(() =>\n Effect.gen(function* () {\n const state = yield* circuitBreaker.getState;\n yield* Ref.update(metrics, (m) => ({\n ...m,\n circuitBreakerState: state,\n }));\n })\n ),\n Effect.withSpan(\"queue.sendBatch\", {\n attributes: { batchSize: envelopes.length },\n })\n );\n\n // Track offline requeue attempts in test environments\n const offlineAttemptsRef = yield* Ref.make(0);\n\n const handleOffline = (batch: Chunk.Chunk<Envelope>) =>\n Effect.gen(function* () {\n // Increment attempts and compute backoff\n const attempts = yield* Ref.getAndUpdate(\n offlineAttemptsRef,\n (n) => n + 1\n );\n if (\n typeof process !== \"undefined\" &&\n process.env?.VITEST &&\n attempts >= TEST_OFFLINE_REQUEUE_LIMIT\n ) {\n yield* Effect.logWarning(\n `Offline - dropping batch after ${TEST_OFFLINE_REQUEUE_LIMIT} attempts (test mode)`\n );\n return;\n }\n\n yield* requeueWithLog(batch, \"Offline - requeueing batch\");\n\n const base = OFFLINE_RETRY_BASE_MS;\n\n const capped = Math.min(base * 2 ** attempts, OFFLINE_RETRY_MAX_MS);\n const jitter = Math.random() * OFFLINE_JITTER_FACTOR * capped;\n const sleepMs = Math.floor(capped + jitter);\n\n yield* Effect.sleep(Duration.millis(sleepMs));\n });\n\n const sendBatch = (batch: Chunk.Chunk<Envelope>) =>\n Effect.gen(function* () {\n if (Chunk.isEmpty(batch)) {\n return;\n }\n\n const online = yield* isOnline();\n if (!online) {\n yield* handleOffline(batch);\n return;\n }\n\n const envelopes = Chunk.toArray(batch);\n const ids = envelopes\n .map((e) => e.uuid)\n .filter((v): v is string => typeof v === \"string\");\n\n yield* sendWithBreaker(envelopes, ids, batch);\n });\n\n const flush = () =>\n Effect.gen(function* () {\n const items = yield* Queue.takeAll(envelopeQueue);\n\n yield* Ref.update(metrics, (m) => ({\n ...m,\n queueDepth: 0,\n }));\n\n if (Chunk.isNonEmpty(items)) {\n yield* sendBatch(items).pipe(Effect.catchAll(() => Effect.void));\n }\n }).pipe(Effect.withSpan(\"queue.flush\"));\n\n const processBatch = Effect.gen(function* () {\n const isReady = yield* Ref.get(readyRef);\n\n if (!isReady) {\n yield* Effect.sleep(Duration.millis(DEFER_SEND_DELAY_MS));\n\n return;\n }\n\n // If the circuit breaker is open, coalesce batches by skipping sends\n // until the remaining cooldown is small. This prevents a retry storm.\n const breakerState = yield* circuitBreaker.getState;\n if (breakerState === \"open\") {\n const data = yield* circuitBreaker.breakerData.get;\n const now = Date.now();\n const since = data.lastFailureTime ? now - data.lastFailureTime : 0;\n const remainingMs = Math.max(\n 0,\n DEFAULT_CIRCUIT_BREAKER_CONFIG.resetMs - since\n );\n\n // Reflect breaker state in metrics\n yield* Ref.update(metrics, (m) => ({\n ...m,\n circuitBreakerState: breakerState,\n }));\n\n if (remainingMs > BREAKER_COALESCE_REMAINING_MS) {\n yield* Effect.logDebug(\n \"Circuit breaker open - coalescing batches until cooldown is small\"\n ).pipe(Effect.annotateLogs({ remainingMs }));\n // Sleep until near the reset time to allow one consolidated send attempt\n yield* Effect.sleep(\n Duration.millis(remainingMs - BREAKER_COALESCE_REMAINING_MS)\n );\n return;\n }\n }\n\n const batchSize = config.batch.size;\n const batchMs = config.batch.ms;\n\n const pending = yield* Queue.takeBetween(\n envelopeQueue,\n 1,\n batchSize\n ).pipe(\n Effect.timeout(Duration.millis(batchMs)),\n Effect.catchTag(\"TimeoutException\", () =>\n Queue.takeAll(envelopeQueue).pipe(\n Effect.map((items) =>\n Chunk.isEmpty(items) ? Chunk.empty<Envelope>() : items\n )\n )\n )\n );\n\n yield* Ref.update(metrics, (m) => ({\n ...m,\n queueDepth: Math.max(0, m.queueDepth - Chunk.size(pending)),\n }));\n\n if (Chunk.isNonEmpty(pending)) {\n yield* Effect.logDebug(\"Processing batch\").pipe(\n Effect.annotateLogs({ batchSize: Chunk.size(pending) })\n );\n yield* sendBatch(pending);\n }\n });\n\n const startBatchProcessor = () =>\n processBatch.pipe(\n Effect.catchAllCause((cause) =>\n Effect.logError(\"Batch processor error\", { cause: String(cause) })\n ),\n Effect.repeat(Schedule.forever),\n // Ensure the forked fiber uses the same logger configuration\n Effect.provide(InterfereLogger.layer()),\n Effect.forkScoped,\n Effect.as(Effect.void)\n );\n\n const add = (envelope: Envelope) =>\n Effect.gen(function* () {\n const size = yield* Queue.size(envelopeQueue);\n\n if (size >= capacity) {\n const toDrop = Math.max(1, size - capacity + 1);\n\n yield* storage.dropOldest(toDrop).pipe(Effect.ignore);\n }\n\n yield* storage.append([envelope]).pipe(Effect.ignore);\n yield* Queue.offer(envelopeQueue, envelope);\n yield* Ref.update(metrics, (m) => ({\n ...m,\n queueDepth: m.queueDepth + 1,\n }));\n yield* Effect.logDebug(\n `[${envelope.type}] Envelope '${envelope.uuid}' added to queue`\n ).pipe(\n Effect.annotateLogs({\n envelopeId: envelope.uuid,\n queueSize: size + 1,\n })\n );\n }).pipe(Effect.withSpan(\"queue.add\"));\n\n const size = () => Ref.get(metrics).pipe(Effect.map((m) => m.queueDepth));\n\n const boundedFlush = (timeoutMs: number) =>\n Effect.race(flush(), Effect.sleep(Duration.millis(timeoutMs))).pipe(\n Effect.asVoid\n );\n\n const setReady = (ready: boolean) =>\n Ref.set(readyRef, ready).pipe(\n Effect.zipRight(\n ready\n ? Effect.logDebug(\"Queue ready - enabling batch sends\")\n : Effect.logDebug(\"Queue not ready - deferring batch sends\")\n )\n );\n\n const getMetrics = () => Ref.get(metrics);\n\n return {\n add,\n size,\n flush,\n boundedFlush,\n startBatchProcessor,\n setReady,\n getMetrics,\n } satisfies QueueService;\n })\n);\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { Context, Effect, Layer, Option } from "effect";
2
+
3
+ //#region src/effect/layers/session.layer.d.ts
4
+ interface SessionService {
5
+ readonly getSessionId: () => Effect.Effect<Option.Option<string>>;
6
+ readonly ensureWindowId: () => Effect.Effect<string | null>;
7
+ readonly updateActivity: () => Effect.Effect<void>;
8
+ readonly resetSession: () => Effect.Effect<void>;
9
+ readonly whenSessionReady: (options?: {
10
+ timeout?: number;
11
+ signal?: AbortSignal;
12
+ }) => Effect.Effect<string, Error>;
13
+ readonly getSessionDebug: () => Effect.Effect<{
14
+ sessionId: string | null;
15
+ sessionStartTimestamp: number | null;
16
+ lastActivityTimestamp: number | null;
17
+ sessionTimeoutMs: number;
18
+ sessionAge: number | undefined;
19
+ timeSinceLastActivity: number | undefined;
20
+ }>;
21
+ }
22
+ declare const SessionServiceTag_base: Context.TagClass<SessionServiceTag, "@interfere/react/Session", SessionService>;
23
+ declare class SessionServiceTag extends SessionServiceTag_base {}
24
+ declare const SessionServiceLive: Layer.Layer<SessionServiceTag, never, never>;
25
+ //#endregion
26
+ export { SessionService, SessionServiceLive, SessionServiceTag };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.layer.d.mts","names":[],"sources":["../../../src/effect/layers/session.layer.ts"],"sourcesContent":[],"mappings":";;;UA+CiB,cAAA;+BACc,MAAA,CAAO,OAAO,MAAA,CAAO;EADnC,SAAA,cAAc,EAAA,GAAA,GAEE,MAAA,CAAO,MAFT,CAAA,MAAA,GAAA,IAAA,CAAA;EACc,SAAO,cAAA,EAAA,GAAA,GAEnB,MAAA,CAAO,MAFY,CAAA,IAAA,CAAA;EAArB,SAAO,YAAA,EAAA,GAAA,GAGP,MAAA,CAAO,MAHA,CAAA,IAAA,CAAA;EACL,SAAO,gBAAA,EAAA,CAAA,OAK3B,CAL2B,EAAA;IACA,OAAA,CAAA,EAAA,MAAA;IACF,MAAA,CAAA,EAGzB,WAHyB;EAGzB,CAAA,EAAA,GACL,MAAA,CAAO,MADF,CAAA,MAAA,EACiB,KADjB,CAAA;EACiB,SAAA,eAAA,EAAA,GAAA,GACI,MAAA,CAAO,MADX,CAAA;IAAf,SAAA,EAAA,MAAA,GAAA,IAAA;IAC0B,qBAAA,EAAA,MAAA,GAAA,IAAA;IAAM,qBAAA,EAAA,MAAA,GAAA,IAAA;IAQ9C,gBAAA,EAAA,MAAA;;;;;AAED,cAFC,sBAEsC,kBAAA,kBAGpC,EAAA,0BAAA,gBAAA,CAAA;AAwCU,cA3CA,iBAAA,SAA0B,sBAAA,CA2CR;cAAlB,oBAAkB,KAAA,CAAA,MAAA"}
@@ -0,0 +1,126 @@
1
+ import { Context, Duration, Effect, Layer, Option } from "effect";
2
+ import { v7 } from "uuid";
3
+ import { nanoid } from "nanoid";
4
+
5
+ //#region src/effect/layers/session.layer.ts
6
+ const STORAGE_KEYS = {
7
+ sessionId: "__interfere_session_id__",
8
+ sessionStart: "__interfere_session_start__",
9
+ lastActivity: "__interfere_last_activity__",
10
+ windowId: "__interfere_window_id__"
11
+ };
12
+ const SESSION_TIMEOUT_MS = 18e5;
13
+ const DEFAULT_TIMEOUT_MS = 3e4;
14
+ const WINDOW_ID_LENGTH = 10;
15
+ const PROBE_KEY = "__interfere_probe__";
16
+ const PROBE_VALUE = "1";
17
+ let sessionId = null;
18
+ let windowId = null;
19
+ let sessionStartTimestamp = null;
20
+ let lastActivityTimestamp = null;
21
+ function safeNow() {
22
+ return Date.now();
23
+ }
24
+ function canUseStorage() {
25
+ try {
26
+ if (typeof window === "undefined" || !window.localStorage) return false;
27
+ window.localStorage.setItem(PROBE_KEY, PROBE_VALUE);
28
+ window.localStorage.removeItem(PROBE_KEY);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ function newSessionId() {
35
+ return v7();
36
+ }
37
+ var SessionServiceTag = class extends Context.Tag("@interfere/react/Session")() {};
38
+ const getSessionIdImpl = () => Effect.sync(() => {
39
+ if (typeof window === "undefined") return Option.none();
40
+ if (sessionId) return Option.some(sessionId);
41
+ const now = safeNow();
42
+ const storage = canUseStorage() ? window.localStorage : null;
43
+ const stored = storage?.getItem(STORAGE_KEYS.sessionId) ?? null;
44
+ const start = Number(storage?.getItem(STORAGE_KEYS.sessionStart) ?? "0");
45
+ const last = Number(storage?.getItem(STORAGE_KEYS.lastActivity) ?? "0");
46
+ const expired = last > 0 && now - last > SESSION_TIMEOUT_MS;
47
+ if (!stored || expired) {
48
+ sessionId = newSessionId();
49
+ sessionStartTimestamp = now;
50
+ lastActivityTimestamp = now;
51
+ storage?.setItem(STORAGE_KEYS.sessionId, sessionId);
52
+ storage?.setItem(STORAGE_KEYS.sessionStart, String(sessionStartTimestamp));
53
+ storage?.setItem(STORAGE_KEYS.lastActivity, String(lastActivityTimestamp));
54
+ return Option.some(sessionId);
55
+ }
56
+ sessionId = stored;
57
+ sessionStartTimestamp = start || now;
58
+ lastActivityTimestamp = last || now;
59
+ return Option.some(sessionId);
60
+ });
61
+ const SessionServiceLive = Layer.succeed(SessionServiceTag, {
62
+ ensureWindowId: () => Effect.sync(() => {
63
+ if (typeof window === "undefined") return null;
64
+ if (windowId) return windowId;
65
+ const storage = canUseStorage() ? window.localStorage : null;
66
+ windowId = storage?.getItem(STORAGE_KEYS.windowId) ?? null ?? `win_${nanoid(WINDOW_ID_LENGTH)}`;
67
+ storage?.setItem(STORAGE_KEYS.windowId, windowId);
68
+ return windowId;
69
+ }),
70
+ getSessionId: getSessionIdImpl,
71
+ updateActivity: () => Effect.sync(() => {
72
+ if (typeof window === "undefined") return;
73
+ const storage = canUseStorage() ? window.localStorage : null;
74
+ lastActivityTimestamp = safeNow();
75
+ storage?.setItem(STORAGE_KEYS.lastActivity, String(lastActivityTimestamp));
76
+ }),
77
+ resetSession: () => Effect.sync(() => {
78
+ if (typeof window === "undefined") return;
79
+ const storage = canUseStorage() ? window.localStorage : null;
80
+ sessionId = newSessionId();
81
+ sessionStartTimestamp = safeNow();
82
+ lastActivityTimestamp = sessionStartTimestamp;
83
+ storage?.setItem(STORAGE_KEYS.sessionId, sessionId);
84
+ storage?.setItem(STORAGE_KEYS.sessionStart, String(sessionStartTimestamp));
85
+ storage?.setItem(STORAGE_KEYS.lastActivity, String(lastActivityTimestamp));
86
+ }),
87
+ whenSessionReady: (options = {}) => {
88
+ const POLL_INTERVAL_MS = 250;
89
+ const MAX_TIMEOUT = options.timeout ?? DEFAULT_TIMEOUT_MS;
90
+ const abortEffect = options.signal ? Effect.async((resume) => {
91
+ const signal = options.signal;
92
+ if (!signal) return;
93
+ if (signal.aborted) {
94
+ resume(Effect.die(/* @__PURE__ */ new Error("Aborted")));
95
+ return;
96
+ }
97
+ const handler = () => resume(Effect.die(/* @__PURE__ */ new Error("Aborted")));
98
+ signal.addEventListener("abort", handler, { once: true });
99
+ return Effect.sync(() => {
100
+ signal.removeEventListener("abort", handler);
101
+ });
102
+ }) : Effect.never;
103
+ const pollOnce = Effect.suspend(() => Effect.gen(function* () {
104
+ const sidOpt = yield* getSessionIdImpl();
105
+ if (Option.isSome(sidOpt)) return sidOpt.value;
106
+ yield* Effect.sleep(Duration.millis(POLL_INTERVAL_MS));
107
+ return yield* pollOnce;
108
+ }));
109
+ const poll = pollOnce.pipe(Effect.timeoutFail({
110
+ onTimeout: () => /* @__PURE__ */ new Error("Session ID not available within timeout"),
111
+ duration: Duration.millis(MAX_TIMEOUT)
112
+ }));
113
+ return Effect.race(poll, abortEffect);
114
+ },
115
+ getSessionDebug: () => Effect.sync(() => ({
116
+ sessionId,
117
+ sessionStartTimestamp,
118
+ lastActivityTimestamp,
119
+ sessionTimeoutMs: SESSION_TIMEOUT_MS,
120
+ sessionAge: sessionStartTimestamp ? safeNow() - sessionStartTimestamp : void 0,
121
+ timeSinceLastActivity: lastActivityTimestamp ? safeNow() - lastActivityTimestamp : void 0
122
+ }))
123
+ });
124
+
125
+ //#endregion
126
+ export { SessionServiceLive, SessionServiceTag };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.layer.mjs","names":["sessionId: string | null","windowId: string | null","sessionStartTimestamp: number | null","lastActivityTimestamp: number | null","uuidv7","pollOnce: Effect.Effect<string>"],"sources":["../../../src/effect/layers/session.layer.ts"],"sourcesContent":["import { Context, Duration, Effect, Layer, Option } from \"effect\";\nimport { nanoid } from \"nanoid\";\nimport { v7 as uuidv7 } from \"uuid\";\n\n// Constants\nconst STORAGE_KEYS = {\n sessionId: \"__interfere_session_id__\",\n sessionStart: \"__interfere_session_start__\",\n lastActivity: \"__interfere_last_activity__\",\n windowId: \"__interfere_window_id__\",\n} as const;\n\nconst SESSION_TIMEOUT_MS = 1_800_000; // 30 minutes\nconst DEFAULT_TIMEOUT_MS = 30_000; // 30 seconds\nconst WINDOW_ID_LENGTH = 10;\nconst PROBE_KEY = \"__interfere_probe__\";\nconst PROBE_VALUE = \"1\";\n\n// In-memory state\nlet sessionId: string | null = null;\nlet windowId: string | null = null;\nlet sessionStartTimestamp: number | null = null;\nlet lastActivityTimestamp: number | null = null;\n\n// Helpers\nfunction safeNow() {\n return Date.now();\n}\n\nfunction canUseStorage() {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) {\n return false;\n }\n window.localStorage.setItem(PROBE_KEY, PROBE_VALUE);\n window.localStorage.removeItem(PROBE_KEY);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction newSessionId(): string {\n return uuidv7();\n}\n\n// Service type\nexport interface SessionService {\n readonly getSessionId: () => Effect.Effect<Option.Option<string>>;\n readonly ensureWindowId: () => Effect.Effect<string | null>;\n readonly updateActivity: () => Effect.Effect<void>;\n readonly resetSession: () => Effect.Effect<void>;\n readonly whenSessionReady: (options?: {\n timeout?: number;\n signal?: AbortSignal;\n }) => Effect.Effect<string, Error>;\n readonly getSessionDebug: () => Effect.Effect<{\n sessionId: string | null;\n sessionStartTimestamp: number | null;\n lastActivityTimestamp: number | null;\n sessionTimeoutMs: number;\n sessionAge: number | undefined;\n timeSinceLastActivity: number | undefined;\n }>;\n}\n\nexport class SessionServiceTag extends Context.Tag(\"@interfere/react/Session\")<\n SessionServiceTag,\n SessionService\n>() {}\n\n// Internal implementation functions\nconst getSessionIdImpl = () =>\n Effect.sync(() => {\n if (typeof window === \"undefined\") {\n return Option.none();\n }\n if (sessionId) {\n return Option.some(sessionId);\n }\n const now = safeNow();\n const storage = canUseStorage() ? window.localStorage : null;\n const stored = storage?.getItem(STORAGE_KEYS.sessionId) ?? null;\n const start = Number(storage?.getItem(STORAGE_KEYS.sessionStart) ?? \"0\");\n const last = Number(storage?.getItem(STORAGE_KEYS.lastActivity) ?? \"0\");\n const expired = last > 0 && now - last > SESSION_TIMEOUT_MS;\n\n if (!stored || expired) {\n sessionId = newSessionId();\n sessionStartTimestamp = now;\n lastActivityTimestamp = now;\n storage?.setItem(STORAGE_KEYS.sessionId, sessionId);\n storage?.setItem(\n STORAGE_KEYS.sessionStart,\n String(sessionStartTimestamp)\n );\n storage?.setItem(\n STORAGE_KEYS.lastActivity,\n String(lastActivityTimestamp)\n );\n return Option.some(sessionId);\n }\n\n sessionId = stored;\n sessionStartTimestamp = start || now;\n lastActivityTimestamp = last || now;\n return Option.some(sessionId);\n });\n\nexport const SessionServiceLive = Layer.succeed(SessionServiceTag, {\n ensureWindowId: () =>\n Effect.sync(() => {\n if (typeof window === \"undefined\") {\n return null;\n }\n if (windowId) {\n return windowId;\n }\n const storage = canUseStorage() ? window.localStorage : null;\n const existing = storage?.getItem(STORAGE_KEYS.windowId) ?? null;\n windowId = existing ?? `win_${nanoid(WINDOW_ID_LENGTH)}`;\n storage?.setItem(STORAGE_KEYS.windowId, windowId);\n return windowId;\n }),\n\n getSessionId: getSessionIdImpl,\n\n updateActivity: () =>\n Effect.sync(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n const storage = canUseStorage() ? window.localStorage : null;\n lastActivityTimestamp = safeNow();\n storage?.setItem(\n STORAGE_KEYS.lastActivity,\n String(lastActivityTimestamp)\n );\n }),\n\n resetSession: () =>\n Effect.sync(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n const storage = canUseStorage() ? window.localStorage : null;\n sessionId = newSessionId();\n sessionStartTimestamp = safeNow();\n lastActivityTimestamp = sessionStartTimestamp;\n storage?.setItem(STORAGE_KEYS.sessionId, sessionId);\n storage?.setItem(\n STORAGE_KEYS.sessionStart,\n String(sessionStartTimestamp)\n );\n storage?.setItem(\n STORAGE_KEYS.lastActivity,\n String(lastActivityTimestamp)\n );\n }),\n\n whenSessionReady: (options = {}) => {\n const POLL_INTERVAL_MS = 250;\n const MAX_TIMEOUT = options.timeout ?? DEFAULT_TIMEOUT_MS;\n\n const abortEffect = options.signal\n ? Effect.async<never>((resume) => {\n const signal = options.signal;\n if (!signal) {\n return;\n }\n\n if (signal.aborted) {\n resume(Effect.die(new Error(\"Aborted\")));\n return;\n }\n\n const handler = () => resume(Effect.die(new Error(\"Aborted\")));\n signal.addEventListener(\"abort\", handler, { once: true });\n return Effect.sync(() => {\n signal.removeEventListener(\"abort\", handler);\n });\n })\n : Effect.never;\n\n const pollOnce: Effect.Effect<string> = Effect.suspend(() =>\n Effect.gen(function* () {\n const sidOpt = yield* getSessionIdImpl();\n if (Option.isSome(sidOpt)) {\n return sidOpt.value;\n }\n yield* Effect.sleep(Duration.millis(POLL_INTERVAL_MS));\n return yield* pollOnce;\n })\n );\n\n const poll = pollOnce.pipe(\n Effect.timeoutFail({\n onTimeout: () => new Error(\"Session ID not available within timeout\"),\n duration: Duration.millis(MAX_TIMEOUT),\n })\n );\n\n return Effect.race(poll, abortEffect);\n },\n\n getSessionDebug: () =>\n Effect.sync(() => ({\n sessionId,\n sessionStartTimestamp,\n lastActivityTimestamp,\n sessionTimeoutMs: SESSION_TIMEOUT_MS,\n sessionAge: sessionStartTimestamp\n ? safeNow() - sessionStartTimestamp\n : undefined,\n timeSinceLastActivity: lastActivityTimestamp\n ? safeNow() - lastActivityTimestamp\n : undefined,\n })),\n} satisfies SessionService);\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { Effect } from "effect";
2
+ import { Envelope } from "@interfere/types/sdk/envelope";
3
+
4
+ //#region src/effect/layers/storage.layer.d.ts
5
+ interface StorageAdapter {
6
+ load: () => Effect.Effect<Envelope[]>;
7
+ save: (envelopes: Envelope[]) => Effect.Effect<void>;
8
+ append: (envelopes: Envelope[]) => Effect.Effect<void>;
9
+ removeByIds: (ids: string[]) => Effect.Effect<void>;
10
+ dropOldest: (n: number) => Effect.Effect<void>;
11
+ clear: () => Effect.Effect<void>;
12
+ capacity: () => number;
13
+ }
14
+ declare function createLocalStorageAdapter(key: string): StorageAdapter;
15
+ declare function createIndexedDBAdapter(dbName: string, storeName: string): StorageAdapter;
16
+ declare function createMemoryStorageAdapter(): StorageAdapter;
17
+ declare function createStorageAdapter(): StorageAdapter;
18
+ //#endregion
19
+ export { StorageAdapter, createIndexedDBAdapter, createLocalStorageAdapter, createMemoryStorageAdapter, createStorageAdapter };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.layer.d.mts","names":[],"sources":["../../../src/effect/layers/storage.layer.ts"],"sourcesContent":[],"mappings":";;;;UAOiB,cAAA;cACH,MAAA,CAAO,OAAO;EADX,IAAA,EAAA,CAAA,SAAA,EAEG,QAFW,EAAA,EAAA,GAEI,MAAA,CAAO,MAFX,CAAA,IAAA,CAAA;EACH,MAAA,EAAA,CAAA,SAAA,EAEN,QAFM,EAAA,EAAA,GAES,MAAA,CAAO,MAFhB,CAAA,IAAA,CAAA;EAAd,WAAO,EAAA,CAAA,GAAA,EAAA,MAAA,EAAA,EAAA,GAGa,MAAA,CAAO,MAHpB,CAAA,IAAA,CAAA;EACD,UAAA,EAAA,CAAA,CAAA,EAAA,MAAA,EAAA,GAGS,MAAA,CAAO,MAHhB,CAAA,IAAA,CAAA;EAAe,KAAO,EAAA,GAAA,GAI3B,MAAA,CAAO,MAJoB,CAAA,IAAA,CAAA;EACpB,QAAA,EAAA,GAAA,GAAA,MAAA;;AACY,iBAMlB,yBAAA,CANyB,GAAA,EAAA,MAAA,CAAA,EAMe,cANf;AACZ,iBAmFb,sBAAA,CAnFoB,MAAA,EAAA,MAAA,EAAA,SAAA,EAAA,MAAA,CAAA,EAsFjC,cAtFiC;AACrB,iBA0SC,0BAAA,CAAA,CA1SM,EA0SwB,cA1SxB;AAAM,iBAgVZ,oBAAA,CAAA,CAhVY,EAgVY,cAhVZ"}