@lessonkit/core 1.3.0 → 1.3.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.
package/dist/index.cjs CHANGED
@@ -620,12 +620,14 @@ function createTrackingClient(opts) {
620
620
  }
621
621
  const buffer = [];
622
622
  let flushInFlight = null;
623
+ let inflightExitBatch = null;
623
624
  let disposed = false;
624
625
  let disposing = false;
625
626
  let intervalId;
626
627
  const runFlush = () => {
627
628
  if (!buffer.length) return Promise.resolve(true);
628
629
  const events = buffer.splice(0, buffer.length);
630
+ inflightExitBatch = events;
629
631
  let succeeded = false;
630
632
  return Promise.resolve().then(async () => {
631
633
  if (batchSink) {
@@ -650,6 +652,8 @@ function createTrackingClient(opts) {
650
652
  return runFlush();
651
653
  }
652
654
  return succeeded;
655
+ }).finally(() => {
656
+ inflightExitBatch = null;
653
657
  });
654
658
  };
655
659
  const flush = () => {
@@ -697,6 +701,22 @@ function createTrackingClient(opts) {
697
701
  if (buffer.length >= maxBatchSize) void flush();
698
702
  },
699
703
  flush,
704
+ flushOnExit: opts?.exitBatchSink ? () => {
705
+ const fromBuffer = buffer.splice(0, buffer.length);
706
+ const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
707
+ const events = [...fromInflight, ...fromBuffer];
708
+ if (!events.length) return;
709
+ try {
710
+ const result = opts.exitBatchSink(events);
711
+ if (result != null && typeof result.catch === "function") {
712
+ void result.catch(() => {
713
+ buffer.unshift(...events);
714
+ });
715
+ }
716
+ } catch {
717
+ buffer.unshift(...events);
718
+ }
719
+ } : void 0,
700
720
  dispose: () => {
701
721
  if (disposed || disposing) return Promise.resolve();
702
722
  disposing = true;
@@ -1207,16 +1227,16 @@ function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1207
1227
  return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
1208
1228
  }
1209
1229
  function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1210
- if (!courseId) return;
1211
- storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1230
+ if (!courseId) return false;
1231
+ return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1212
1232
  }
1213
1233
  function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1214
1234
  if (!courseId) return false;
1215
1235
  return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
1216
1236
  }
1217
1237
  function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1218
- if (!courseId) return;
1219
- storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1238
+ if (!courseId) return false;
1239
+ return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1220
1240
  }
1221
1241
  function resetSharedVolatileSessionIdForTests() {
1222
1242
  sharedVolatileSessionId = null;
@@ -1238,19 +1258,29 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
1238
1258
  }
1239
1259
 
1240
1260
  // src/runtime/courseLifecycle.ts
1261
+ var courseStartedEmitFlights = /* @__PURE__ */ new Set();
1241
1262
  function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1263
+ const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
1242
1264
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1243
1265
  if (alreadyEmittedToSink) {
1244
1266
  return { emitted: true, marked };
1245
1267
  }
1246
- const emitted = deps.emitCourseStartedEvent(ctx);
1247
- if (emitted && !marked) {
1248
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1268
+ if (courseStartedEmitFlights.has(flightKey)) {
1269
+ return { emitted: false, marked };
1270
+ }
1271
+ courseStartedEmitFlights.add(flightKey);
1272
+ try {
1273
+ const emitted = deps.emitCourseStartedEvent(ctx);
1274
+ if (emitted && !marked) {
1275
+ markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1276
+ }
1277
+ return {
1278
+ emitted,
1279
+ marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
1280
+ };
1281
+ } finally {
1282
+ courseStartedEmitFlights.delete(flightKey);
1249
1283
  }
1250
- return {
1251
- emitted,
1252
- marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
1253
- };
1254
1284
  }
1255
1285
  function buildCourseStartedTelemetryEvent(ctx) {
1256
1286
  return buildTelemetryEvent({
package/dist/index.d.cts CHANGED
@@ -259,6 +259,8 @@ type TrackingClient = {
259
259
  track: (event: TelemetryEvent) => void;
260
260
  /** Resolves to true when all buffered events were delivered; false when a sink failure re-queued events. */
261
261
  flush?: () => void | Promise<boolean>;
262
+ /** Best-effort synchronous flush for pagehide (keepalive batch sink when configured). */
263
+ flushOnExit?: () => void;
262
264
  dispose?: () => void | Promise<void>;
263
265
  };
264
266
 
@@ -385,6 +387,8 @@ declare function createTrackingClient(opts?: {
385
387
  batchSink?: TelemetryBatchSink;
386
388
  /** Called when an event is dropped because the batch buffer is at cap (including in production). */
387
389
  onBufferDrop?: () => void;
390
+ /** Keepalive batch delivery for pagehide (e.g. from createFetchBatchSink). */
391
+ exitBatchSink?: TelemetryBatchSink;
388
392
  }): TrackingClient;
389
393
 
390
394
  declare function createSessionId(): string;
@@ -523,9 +527,9 @@ declare function resolveSessionId(storage: StoragePort, provided?: string): stri
523
527
  declare function hasCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
524
528
  declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
525
529
  declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
526
- declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
530
+ declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
527
531
  declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
528
- declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
532
+ declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
529
533
  /** @internal Reset shared volatile session id between tests. */
530
534
  declare function resetSharedVolatileSessionIdForTests(): void;
531
535
  declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
package/dist/index.d.ts CHANGED
@@ -259,6 +259,8 @@ type TrackingClient = {
259
259
  track: (event: TelemetryEvent) => void;
260
260
  /** Resolves to true when all buffered events were delivered; false when a sink failure re-queued events. */
261
261
  flush?: () => void | Promise<boolean>;
262
+ /** Best-effort synchronous flush for pagehide (keepalive batch sink when configured). */
263
+ flushOnExit?: () => void;
262
264
  dispose?: () => void | Promise<void>;
263
265
  };
264
266
 
@@ -385,6 +387,8 @@ declare function createTrackingClient(opts?: {
385
387
  batchSink?: TelemetryBatchSink;
386
388
  /** Called when an event is dropped because the batch buffer is at cap (including in production). */
387
389
  onBufferDrop?: () => void;
390
+ /** Keepalive batch delivery for pagehide (e.g. from createFetchBatchSink). */
391
+ exitBatchSink?: TelemetryBatchSink;
388
392
  }): TrackingClient;
389
393
 
390
394
  declare function createSessionId(): string;
@@ -523,9 +527,9 @@ declare function resolveSessionId(storage: StoragePort, provided?: string): stri
523
527
  declare function hasCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
524
528
  declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
525
529
  declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
526
- declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
530
+ declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
527
531
  declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
528
- declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
532
+ declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
529
533
  /** @internal Reset shared volatile session id between tests. */
530
534
  declare function resetSharedVolatileSessionIdForTests(): void;
531
535
  declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
package/dist/index.js CHANGED
@@ -522,12 +522,14 @@ function createTrackingClient(opts) {
522
522
  }
523
523
  const buffer = [];
524
524
  let flushInFlight = null;
525
+ let inflightExitBatch = null;
525
526
  let disposed = false;
526
527
  let disposing = false;
527
528
  let intervalId;
528
529
  const runFlush = () => {
529
530
  if (!buffer.length) return Promise.resolve(true);
530
531
  const events = buffer.splice(0, buffer.length);
532
+ inflightExitBatch = events;
531
533
  let succeeded = false;
532
534
  return Promise.resolve().then(async () => {
533
535
  if (batchSink) {
@@ -552,6 +554,8 @@ function createTrackingClient(opts) {
552
554
  return runFlush();
553
555
  }
554
556
  return succeeded;
557
+ }).finally(() => {
558
+ inflightExitBatch = null;
555
559
  });
556
560
  };
557
561
  const flush = () => {
@@ -599,6 +603,22 @@ function createTrackingClient(opts) {
599
603
  if (buffer.length >= maxBatchSize) void flush();
600
604
  },
601
605
  flush,
606
+ flushOnExit: opts?.exitBatchSink ? () => {
607
+ const fromBuffer = buffer.splice(0, buffer.length);
608
+ const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
609
+ const events = [...fromInflight, ...fromBuffer];
610
+ if (!events.length) return;
611
+ try {
612
+ const result = opts.exitBatchSink(events);
613
+ if (result != null && typeof result.catch === "function") {
614
+ void result.catch(() => {
615
+ buffer.unshift(...events);
616
+ });
617
+ }
618
+ } catch {
619
+ buffer.unshift(...events);
620
+ }
621
+ } : void 0,
602
622
  dispose: () => {
603
623
  if (disposed || disposing) return Promise.resolve();
604
624
  disposing = true;
@@ -1109,16 +1129,16 @@ function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1109
1129
  return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
1110
1130
  }
1111
1131
  function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1112
- if (!courseId) return;
1113
- storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1132
+ if (!courseId) return false;
1133
+ return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1114
1134
  }
1115
1135
  function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1116
1136
  if (!courseId) return false;
1117
1137
  return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
1118
1138
  }
1119
1139
  function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1120
- if (!courseId) return;
1121
- storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1140
+ if (!courseId) return false;
1141
+ return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1122
1142
  }
1123
1143
  function resetSharedVolatileSessionIdForTests() {
1124
1144
  sharedVolatileSessionId = null;
@@ -1140,19 +1160,29 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
1140
1160
  }
1141
1161
 
1142
1162
  // src/runtime/courseLifecycle.ts
1163
+ var courseStartedEmitFlights = /* @__PURE__ */ new Set();
1143
1164
  function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1165
+ const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
1144
1166
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1145
1167
  if (alreadyEmittedToSink) {
1146
1168
  return { emitted: true, marked };
1147
1169
  }
1148
- const emitted = deps.emitCourseStartedEvent(ctx);
1149
- if (emitted && !marked) {
1150
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1170
+ if (courseStartedEmitFlights.has(flightKey)) {
1171
+ return { emitted: false, marked };
1172
+ }
1173
+ courseStartedEmitFlights.add(flightKey);
1174
+ try {
1175
+ const emitted = deps.emitCourseStartedEvent(ctx);
1176
+ if (emitted && !marked) {
1177
+ markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1178
+ }
1179
+ return {
1180
+ emitted,
1181
+ marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
1182
+ };
1183
+ } finally {
1184
+ courseStartedEmitFlights.delete(flightKey);
1151
1185
  }
1152
- return {
1153
- emitted,
1154
- marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
1155
- };
1156
1186
  }
1157
1187
  function buildCourseStartedTelemetryEvent(ctx) {
1158
1188
  return buildTelemetryEvent({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",