skillprint-js-sdk 1.1.0-beta.8 → 1.1.0-beta.9

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
@@ -201,7 +201,7 @@ manager.logEvent('ROTATE_CLOCKWISE');
201
201
  manager.logEvent(GameEvent.LEVEL_COMPLETE, { level: 3, score: 1200 });
202
202
  ```
203
203
 
204
- Events are queued and sent with the next screenshot upload, or on their own when there is no screenshot to send. `stopGameSession()` sends the rest with the closing upload, so they arrive before the session closes. `logEvent` never throws, and with no active session it only logs a warning.
204
+ Events are queued, and each screenshot upload carries the ones logged up to its last screenshot. Later events wait for the upload with the screenshots after them, and `stopGameSession()` sends the rest with the closing upload. The SDK decides which events go with which upload; the server keeps each upload's events with its screenshots and does no time arithmetic. If no screenshots come for a long while, events only go on their own once more than one upload's worth (1000) has piled up. `logEvent` never throws, and with no active session it only logs a warning.
205
205
 
206
206
  ### Universal and game-specific events
207
207
 
@@ -232,7 +232,7 @@ Each event can carry data (`{ level: 3, score: 1200 }`). `event` and `timestamp`
232
232
 
233
233
  Each screenshot upload can carry, beside the images:
234
234
  - `offset_ms<n>`: when the n-th screenshot was captured, in ms from session start
235
- - `events`: a JSON array of the events since the last upload (at most 1000), e.g. `[{"event": "ROTATE_CLOCKWISE", "timestamp": 1250}]`, where `timestamp` is ms from session start
235
+ - `events`: a JSON array of the events logged up to the upload's last screenshot (at most 1000), e.g. `[{"event": "ROTATE_CLOCKWISE", "timestamp": 1250}]`, where `timestamp` is ms from session start
236
236
 
237
237
  ### Pages that upload their own frames
238
238
 
@@ -250,7 +250,7 @@ timeline.record('ROTATE_CLOCKWISE'); // when the player acts
250
250
 
251
251
  await client.postScreenshots(sessionId, [frame], false, {
252
252
  offsetsMs: [offset],
253
- events: timeline.takeEvents()
253
+ events: timeline.eventsForUpload(offset) // null for the closing upload
254
254
  });
255
255
  ```
256
256
 
package/dist/index.cjs CHANGED
@@ -516,7 +516,7 @@ var SkillprintAPIClient = class {
516
516
  formData.append(`offset_ms${i}`, String(Math.round(offset)));
517
517
  }
518
518
  });
519
- if (events.length > 0) {
519
+ if (options.events !== void 0) {
520
520
  formData.append("events", JSON.stringify(events));
521
521
  }
522
522
  try {
@@ -851,6 +851,7 @@ var SessionTimeline = class {
851
851
  constructor(now = defaultNow, maxQueued = 5e3) {
852
852
  this.startedAt = null;
853
853
  this.queue = [];
854
+ this.recordedAny = false;
854
855
  this.now = now;
855
856
  this.maxQueued = maxQueued;
856
857
  }
@@ -858,11 +859,13 @@ var SessionTimeline = class {
858
859
  start() {
859
860
  this.startedAt = this.now();
860
861
  this.queue = [];
862
+ this.recordedAny = false;
861
863
  }
862
864
  /** Stops the clock and empties the queue. */
863
865
  reset() {
864
866
  this.startedAt = null;
865
867
  this.queue = [];
868
+ this.recordedAny = false;
866
869
  }
867
870
  get isStarted() {
868
871
  return this.startedAt !== null;
@@ -889,15 +892,47 @@ var SessionTimeline = class {
889
892
  if (this.queue.length >= this.maxQueued) return null;
890
893
  const entry = { ...data, event, timestamp: this.offsetMs(at) };
891
894
  this.queue.push(entry);
895
+ this.recordedAny = true;
892
896
  return entry;
893
897
  }
894
898
  get pendingEvents() {
895
899
  return this.queue.length;
896
900
  }
897
- /** Removes and returns the oldest queued events, at most `max` of them. */
901
+ /** True once this session has recorded any event. */
902
+ get hasRecorded() {
903
+ return this.recordedAny;
904
+ }
905
+ /** Removes and returns the earliest queued events, at most `max`, in session-time order. */
898
906
  takeEvents(max = MAX_EVENTS_PER_UPLOAD) {
907
+ this.sortQueue();
899
908
  return this.queue.splice(0, max);
900
909
  }
910
+ /**
911
+ * Removes and returns the queued events logged up to `offsetMs`
912
+ * (inclusive), at most `max`, in session-time order. Later ones stay
913
+ * queued for the upload that carries the screenshots after them.
914
+ */
915
+ takeEventsThrough(offsetMs, max = MAX_EVENTS_PER_UPLOAD) {
916
+ this.sortQueue();
917
+ let n = 0;
918
+ while (n < this.queue.length && n < max && this.queue[n].timestamp <= offsetMs) n++;
919
+ return this.queue.splice(0, n);
920
+ }
921
+ /**
922
+ * The `events` for an upload whose last screenshot was taken at
923
+ * `lastFrameOffsetMs`: the events logged up to then, or every queued
924
+ * event for the closing upload (pass null). An empty list once the
925
+ * session has recorded any event, so the server can tell a quiet stretch
926
+ * from a game that doesn't log events; undefined before that.
927
+ */
928
+ eventsForUpload(lastFrameOffsetMs) {
929
+ if (!this.recordedAny) return void 0;
930
+ return lastFrameOffsetMs === null ? this.takeEvents() : this.takeEventsThrough(lastFrameOffsetMs);
931
+ }
932
+ // Events stamped with `at` can arrive out of order; uploads send them in order.
933
+ sortQueue() {
934
+ this.queue.sort((a, b) => a.timestamp - b.timestamp);
935
+ }
901
936
  /** Puts events back at the front of the queue, e.g. after a failed upload. */
902
937
  requeue(events) {
903
938
  const room = Math.max(0, this.maxQueued - this.queue.length);
@@ -1200,6 +1235,7 @@ var _SkillprintManager = class _SkillprintManager {
1200
1235
  }
1201
1236
  const sessionId = this.currentSessionId;
1202
1237
  const remaining = this.screenshotQueue;
1238
+ const recorded = this.timeline.hasRecorded;
1203
1239
  const events = this.timeline.takeEvents(Number.MAX_SAFE_INTEGER);
1204
1240
  const inFlightPost = this.inFlightPost;
1205
1241
  const apiClient = this.apiClient;
@@ -1222,22 +1258,34 @@ var _SkillprintManager = class _SkillprintManager {
1222
1258
  if (inFlightPost) await inFlightPost;
1223
1259
  const batchSize = this.postBatchSize();
1224
1260
  try {
1225
- while (events.length > MAX_EVENTS_PER_UPLOAD) {
1226
- await apiClient.postScreenshots(sessionId, [], false, { events: events.splice(0, MAX_EVENTS_PER_UPLOAD) });
1227
- }
1261
+ const eventsUpTo = (lastFrameOffsetMs) => {
1262
+ if (!recorded) return void 0;
1263
+ const n = lastFrameOffsetMs === null ? events.length : events.findIndex((e) => e.timestamp > lastFrameOffsetMs);
1264
+ return events.splice(0, Math.min(n === -1 ? events.length : n, MAX_EVENTS_PER_UPLOAD));
1265
+ };
1228
1266
  if (remaining.length === 0) {
1229
- await apiClient.postScreenshots(sessionId, [], true, { events });
1267
+ while (events.length > MAX_EVENTS_PER_UPLOAD) {
1268
+ await apiClient.postScreenshots(sessionId, [], false, { events: events.splice(0, MAX_EVENTS_PER_UPLOAD) });
1269
+ }
1270
+ await apiClient.postScreenshots(sessionId, [], true, { events: eventsUpTo(null) });
1230
1271
  } else {
1231
1272
  for (let i = 0; i < remaining.length; i += batchSize) {
1232
1273
  const batch = remaining.slice(i, i + batchSize);
1233
1274
  const isLastChunk = i + batchSize >= remaining.length;
1275
+ if (isLastChunk) {
1276
+ while (events.length > MAX_EVENTS_PER_UPLOAD) {
1277
+ await apiClient.postScreenshots(sessionId, [], false, { events: events.splice(0, MAX_EVENTS_PER_UPLOAD) });
1278
+ }
1279
+ }
1280
+ const lastOffset = Math.max(...batch.map((s) => s.offsetMs));
1234
1281
  await apiClient.postScreenshots(sessionId, batch.map((s) => s.blob), isLastChunk, {
1235
1282
  offsetsMs: batch.map((s) => s.offsetMs),
1236
- events: isLastChunk ? events : []
1283
+ events: eventsUpTo(isLastChunk ? null : lastOffset)
1237
1284
  });
1238
1285
  }
1239
1286
  }
1240
- this.log(`Skillprint session stopped. Flushed ${remaining.length} queued screenshots and ${events.length} events.`);
1287
+ const flushed = remaining.length;
1288
+ this.log(`Skillprint session stopped. Flushed ${flushed} queued screenshots.`);
1241
1289
  } catch (error) {
1242
1290
  const message = error instanceof Error ? error.message : String(error);
1243
1291
  this.log(`Failed to flush screenshots on session stop: ${message}`, "error" /* ERROR */);
@@ -1287,23 +1335,23 @@ var _SkillprintManager = class _SkillprintManager {
1287
1335
  startScreenshotPostLoop() {
1288
1336
  this.screenshotPostTimer = setInterval(async () => {
1289
1337
  if (!this.isSessionActive || !this.apiClient || !this.currentSessionId) return;
1290
- if (this.screenshotQueue.length === 0 && this.timeline.pendingEvents === 0) return;
1338
+ if (this.screenshotQueue.length === 0 && this.timeline.pendingEvents <= MAX_EVENTS_PER_UPLOAD) return;
1291
1339
  const batch = this.screenshotQueue.splice(0, Math.min(this.screenshotQueue.length, this.postBatchSize()));
1292
- const events = this.timeline.takeEvents();
1340
+ const events = batch.length > 0 ? this.timeline.eventsForUpload(Math.max(...batch.map((s) => s.offsetMs))) : this.timeline.takeEvents();
1293
1341
  const apiClient = this.apiClient;
1294
1342
  const sessionId = this.currentSessionId;
1295
- this.log(`Posting ${batch.length} screenshots and ${events.length} events...`);
1343
+ this.log(`Posting ${batch.length} screenshots and ${events?.length ?? 0} events...`);
1296
1344
  const post = (async () => {
1297
1345
  try {
1298
1346
  const result = await apiClient.postScreenshots(sessionId, batch.map((s) => s.blob), false, {
1299
1347
  offsetsMs: batch.map((s) => s.offsetMs),
1300
1348
  events
1301
1349
  });
1302
- this.log(`Successfully posted ${batch.length} screenshots and ${events.length} events. Response: ${result.data}`);
1350
+ this.log(`Successfully posted ${batch.length} screenshots and ${events?.length ?? 0} events. Response: ${result.data}`);
1303
1351
  } catch (error) {
1304
1352
  const message = error instanceof Error ? error.message : String(error);
1305
1353
  this.log(`Failed to post screenshots: ${message}`, "error" /* ERROR */);
1306
- this.requeueEvents(events, error, sessionId);
1354
+ this.requeueEvents(events ?? [], error, sessionId);
1307
1355
  }
1308
1356
  })();
1309
1357
  this.inFlightPost = post;