connect-the-dots / cloud-js / InitLevelStats.js
InitLevelStats.js
Raw
const { DataApi } = require("@unity-services/cloud-save-1.4");

module.exports = async ({ params, context, logger }) => {
  const { projectId } = context;
  const totalLevels = Number(params.totalLevels);

  if (!Number.isInteger(totalLevels) || totalLevels < 1) {
    throw new Error("Parameter 'totalLevels' must be a positive integer.");
  }

  const cloudSave = new DataApi(context);
  const COLLECTION = "level-stats";
  const KEY = "level-completion";
  const MAX_RETRIES = 5;
  const BASE_DELAY = 50;

  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      const read = await cloudSave.getPrivateCustomItems(projectId, COLLECTION, [KEY]);
      const record = read.data.results[0] ?? { value: {}, metadata: {} };
      const statsObj = record.value;
      const etag = record.metadata?.etag;

      let created = 0;
      for (let lvl = 0; lvl <= totalLevels - 1; lvl++) {
        if (!statsObj[lvl]) {
          statsObj[lvl] = { NumCompletions: 0 };
          created++;
        }
      }
      if (created === 0) {
        return { Initialized: 0, AlreadyPresent: totalLevels };
      }

      await cloudSave.setPrivateCustomItemBatch(projectId, COLLECTION, {
        data: [{ key: KEY, value: statsObj, writeLock: etag }]
      });

      return { Initialized: created, AlreadyPresent: totalLevels - created };
    } catch (err) {
      if (err.response?.status === 412 && attempt < MAX_RETRIES) {
        const backoff = BASE_DELAY * 2 ** (attempt - 1) + Math.random() * BASE_DELAY;
        logger.warn(`ETag conflict, retry ${attempt}/${MAX_RETRIES} in ${Math.round(backoff)} ms`);
        await new Promise(r => setTimeout(r, backoff));
        continue;
      }
      logger.error("InitAllLevelStats failed", { message: err.message });
      throw err;
    }
  }
  throw new Error(`Failed after ${MAX_RETRIES} retries`);
};