@lessonkit/core 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # `@lessonkit/core`
2
2
 
3
+ [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml)
4
+ [![npm](https://img.shields.io/npm/v/@lessonkit/core.svg)](https://www.npmjs.com/package/@lessonkit/core)
5
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
6
+
3
7
  Core types and runtime primitives shared across LessonKit packages.
4
8
 
5
9
  ## Install
@@ -8,8 +12,9 @@ Core types and runtime primitives shared across LessonKit packages.
8
12
  npm install @lessonkit/core
9
13
  ```
10
14
 
11
- ## What’s inside (0.1.x)
15
+ ## What’s inside (0.2.1)
12
16
 
13
17
  - Telemetry event types (`TelemetryEvent`)
14
- - A minimal tracking client (`createTrackingClient`)
18
+ - A minimal tracking client (`createTrackingClient`) with optional batching
19
+ - Session id helper (`createSessionId`)
15
20
 
package/dist/index.cjs CHANGED
@@ -20,23 +20,90 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ createSessionId: () => createSessionId,
23
24
  createTrackingClient: () => createTrackingClient,
24
25
  nowIso: () => nowIso
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
  function createTrackingClient(opts) {
28
29
  const sink = opts?.sink;
30
+ const batchSink = opts?.batchSink;
31
+ const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
32
+ const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
33
+ const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
34
+ if (!batchEnabled) {
35
+ let disposed2 = false;
36
+ return {
37
+ track: (event) => {
38
+ if (disposed2) return;
39
+ void sink?.(event);
40
+ },
41
+ dispose: () => {
42
+ disposed2 = true;
43
+ }
44
+ };
45
+ }
46
+ if (!sink && !batchSink) {
47
+ return { track: () => {
48
+ } };
49
+ }
50
+ const buffer = [];
51
+ let flushInFlight = null;
52
+ let disposed = false;
53
+ let intervalId;
54
+ const flush = () => {
55
+ if (disposed || flushInFlight) return;
56
+ if (!buffer.length) return;
57
+ const events = buffer.splice(0, buffer.length);
58
+ let sent = 0;
59
+ let succeeded = false;
60
+ flushInFlight = Promise.resolve().then(async () => {
61
+ if (batchSink) {
62
+ await batchSink(events);
63
+ } else {
64
+ for (const e of events) {
65
+ await sink?.(e);
66
+ sent += 1;
67
+ }
68
+ }
69
+ succeeded = true;
70
+ }).catch(() => {
71
+ buffer.unshift(...events.slice(sent));
72
+ }).finally(() => {
73
+ flushInFlight = null;
74
+ if (succeeded && !disposed && buffer.length > 0) flush();
75
+ });
76
+ };
77
+ intervalId = flushIntervalMs > 0 ? globalThis.setInterval(flush, flushIntervalMs) : void 0;
29
78
  return {
30
79
  track: (event) => {
31
- void sink?.(event);
80
+ if (disposed) return;
81
+ buffer.push(event);
82
+ if (buffer.length >= maxBatchSize) flush();
83
+ },
84
+ flush,
85
+ dispose: () => {
86
+ if (disposed) return;
87
+ if (intervalId !== void 0) {
88
+ globalThis.clearInterval(intervalId);
89
+ intervalId = void 0;
90
+ }
91
+ flush();
92
+ disposed = true;
32
93
  }
33
94
  };
34
95
  }
35
96
  function nowIso() {
36
97
  return (/* @__PURE__ */ new Date()).toISOString();
37
98
  }
99
+ function createSessionId() {
100
+ const g = globalThis;
101
+ if (g.crypto?.randomUUID) return g.crypto.randomUUID();
102
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
103
+ }
38
104
  // Annotate the CommonJS export names for ESM import in node:
39
105
  0 && (module.exports = {
106
+ createSessionId,
40
107
  createTrackingClient,
41
108
  nowIso
42
109
  });
package/dist/index.d.cts CHANGED
@@ -1,20 +1,39 @@
1
1
  type CourseId = string;
2
2
  type LessonId = string;
3
- type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "quiz_answered" | "quiz_completed" | "interaction";
3
+ type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
4
+ type TelemetryUser = {
5
+ id?: string;
6
+ email?: string;
7
+ name?: string;
8
+ [key: string]: unknown;
9
+ };
4
10
  type TelemetryEvent = {
5
11
  name: TelemetryEventName;
6
12
  timestamp: string;
7
13
  courseId?: CourseId;
8
14
  lessonId?: LessonId;
15
+ sessionId?: string;
16
+ attemptId?: string;
17
+ user?: TelemetryUser;
9
18
  data?: Record<string, unknown>;
10
19
  };
11
20
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
21
+ type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
12
22
  type TrackingClient = {
13
23
  track: (event: TelemetryEvent) => void;
24
+ flush?: () => void;
25
+ dispose?: () => void;
14
26
  };
15
27
  declare function createTrackingClient(opts?: {
16
28
  sink?: TelemetrySink;
29
+ batch?: {
30
+ enabled?: boolean;
31
+ flushIntervalMs?: number;
32
+ maxBatchSize?: number;
33
+ };
34
+ batchSink?: TelemetryBatchSink;
17
35
  }): TrackingClient;
18
36
  declare function nowIso(): string;
37
+ declare function createSessionId(): string;
19
38
 
20
- export { type CourseId, type LessonId, type TelemetryEvent, type TelemetryEventName, type TelemetrySink, type TrackingClient, createTrackingClient, nowIso };
39
+ export { type CourseId, type LessonId, type TelemetryBatchSink, type TelemetryEvent, type TelemetryEventName, type TelemetrySink, type TelemetryUser, type TrackingClient, createSessionId, createTrackingClient, nowIso };
package/dist/index.d.ts CHANGED
@@ -1,20 +1,39 @@
1
1
  type CourseId = string;
2
2
  type LessonId = string;
3
- type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "quiz_answered" | "quiz_completed" | "interaction";
3
+ type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
4
+ type TelemetryUser = {
5
+ id?: string;
6
+ email?: string;
7
+ name?: string;
8
+ [key: string]: unknown;
9
+ };
4
10
  type TelemetryEvent = {
5
11
  name: TelemetryEventName;
6
12
  timestamp: string;
7
13
  courseId?: CourseId;
8
14
  lessonId?: LessonId;
15
+ sessionId?: string;
16
+ attemptId?: string;
17
+ user?: TelemetryUser;
9
18
  data?: Record<string, unknown>;
10
19
  };
11
20
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
21
+ type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
12
22
  type TrackingClient = {
13
23
  track: (event: TelemetryEvent) => void;
24
+ flush?: () => void;
25
+ dispose?: () => void;
14
26
  };
15
27
  declare function createTrackingClient(opts?: {
16
28
  sink?: TelemetrySink;
29
+ batch?: {
30
+ enabled?: boolean;
31
+ flushIntervalMs?: number;
32
+ maxBatchSize?: number;
33
+ };
34
+ batchSink?: TelemetryBatchSink;
17
35
  }): TrackingClient;
18
36
  declare function nowIso(): string;
37
+ declare function createSessionId(): string;
19
38
 
20
- export { type CourseId, type LessonId, type TelemetryEvent, type TelemetryEventName, type TelemetrySink, type TrackingClient, createTrackingClient, nowIso };
39
+ export { type CourseId, type LessonId, type TelemetryBatchSink, type TelemetryEvent, type TelemetryEventName, type TelemetrySink, type TelemetryUser, type TrackingClient, createSessionId, createTrackingClient, nowIso };
package/dist/index.js CHANGED
@@ -1,16 +1,82 @@
1
1
  // src/index.ts
2
2
  function createTrackingClient(opts) {
3
3
  const sink = opts?.sink;
4
+ const batchSink = opts?.batchSink;
5
+ const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
6
+ const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
7
+ const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
8
+ if (!batchEnabled) {
9
+ let disposed2 = false;
10
+ return {
11
+ track: (event) => {
12
+ if (disposed2) return;
13
+ void sink?.(event);
14
+ },
15
+ dispose: () => {
16
+ disposed2 = true;
17
+ }
18
+ };
19
+ }
20
+ if (!sink && !batchSink) {
21
+ return { track: () => {
22
+ } };
23
+ }
24
+ const buffer = [];
25
+ let flushInFlight = null;
26
+ let disposed = false;
27
+ let intervalId;
28
+ const flush = () => {
29
+ if (disposed || flushInFlight) return;
30
+ if (!buffer.length) return;
31
+ const events = buffer.splice(0, buffer.length);
32
+ let sent = 0;
33
+ let succeeded = false;
34
+ flushInFlight = Promise.resolve().then(async () => {
35
+ if (batchSink) {
36
+ await batchSink(events);
37
+ } else {
38
+ for (const e of events) {
39
+ await sink?.(e);
40
+ sent += 1;
41
+ }
42
+ }
43
+ succeeded = true;
44
+ }).catch(() => {
45
+ buffer.unshift(...events.slice(sent));
46
+ }).finally(() => {
47
+ flushInFlight = null;
48
+ if (succeeded && !disposed && buffer.length > 0) flush();
49
+ });
50
+ };
51
+ intervalId = flushIntervalMs > 0 ? globalThis.setInterval(flush, flushIntervalMs) : void 0;
4
52
  return {
5
53
  track: (event) => {
6
- void sink?.(event);
54
+ if (disposed) return;
55
+ buffer.push(event);
56
+ if (buffer.length >= maxBatchSize) flush();
57
+ },
58
+ flush,
59
+ dispose: () => {
60
+ if (disposed) return;
61
+ if (intervalId !== void 0) {
62
+ globalThis.clearInterval(intervalId);
63
+ intervalId = void 0;
64
+ }
65
+ flush();
66
+ disposed = true;
7
67
  }
8
68
  };
9
69
  }
10
70
  function nowIso() {
11
71
  return (/* @__PURE__ */ new Date()).toISOString();
12
72
  }
73
+ function createSessionId() {
74
+ const g = globalThis;
75
+ if (g.crypto?.randomUUID) return g.crypto.randomUUID();
76
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
77
+ }
13
78
  export {
79
+ createSessionId,
14
80
  createTrackingClient,
15
81
  nowIso
16
82
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -40,6 +40,7 @@
40
40
  "prepublishOnly": "npm run build",
41
41
  "typecheck": "tsc -p tsconfig.json",
42
42
  "test": "vitest run --passWithNoTests",
43
+ "test:coverage": "vitest run --coverage --passWithNoTests=false",
43
44
  "lint": "echo \"(no lint configured yet)\""
44
45
  },
45
46
  "devDependencies": {