@iebh/vuex-tera-json
Version:
A Vuex plugin for syncing state with Tera (using JSON files)
917 lines • 39.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SaveStatus = void 0;
exports.createTeraSync = createTeraSync;
// index.ts
const nanoid_1 = require("nanoid");
const p_retry_1 = __importDefault(require("p-retry"));
const api = __importStar(require("./helpers/api"));
/**
* Represents the possible save statuses of the state.
*/
var SaveStatus;
(function (SaveStatus) {
/** Indicates that all current changes are safely stored */
SaveStatus["SAVED"] = "Saved";
/** Indicates that there are modifications pending to be saved */
SaveStatus["UNSAVED"] = "Unsaved changes";
/** Indicates that a save operation is currently in progress */
SaveStatus["SAVING"] = "Saving...";
})(SaveStatus || (exports.SaveStatus = SaveStatus = {}));
// --- Implementation ---
const DEBUG = true;
const DEFAULT_CONFIG = {
keyPrefix: '',
isSeparateStateForEachUser: false,
autoSaveIntervalMinutes: 15,
showInitialAlert: false,
enableSaveHotkey: true,
loadImmediately: true,
onBeforeSave: () => true,
resetState: () => { }, // Provide a no-op default
};
const debugLog = (...args) => {
if (DEBUG)
console.log('[TERA File Sync]:', ...args);
};
const logError = (error, context) => {
console.error(`[TERA File Sync] ${context}:`, error);
};
const validateConfig = (config) => {
if (typeof config.keyPrefix !== 'string') {
throw new Error('keyPrefix must be a string');
}
if (typeof config.isSeparateStateForEachUser !== 'boolean') {
throw new Error('isSeparateStateForEachUser must be a boolean');
}
if (typeof config.autoSaveIntervalMinutes !== 'number' || config.autoSaveIntervalMinutes < 0) {
throw new Error('autoSaveIntervalMinutes must be a non-negative number');
}
if (typeof config.showInitialAlert !== 'boolean') {
throw new Error('showInitialAlert must be a boolean');
}
if (typeof config.enableSaveHotkey !== 'boolean') {
throw new Error('enableSaveHotkey must be a boolean');
}
if (typeof config.loadImmediately !== 'boolean') {
throw new Error('loadImmediately must be a boolean');
}
if (typeof config.onBeforeSave !== 'function') {
throw new Error('onBeforeSave must be a function');
}
};
const validateVueInstance = (instance) => {
if (!instance) {
throw new Error('Vue instance is required');
}
if (!instance.$tera) {
throw new Error('Vue instance must have $tera property');
}
if (typeof instance.$tera.getUser !== 'function') {
throw new Error('$tera.getUser must be a function');
}
if (typeof instance.$tera.getProjectFile !== 'function') {
throw new Error('$tera.getProjectFile must be a function');
}
if (typeof instance.$tera.getProjectFileContents !== 'function') {
throw new Error('$tera.getProjectFileContents must be a function');
}
if (typeof instance.$tera.setProjectFileContents !== 'function') {
throw new Error('$tera.setProjectFileContents must be a function');
}
if (typeof instance.$tera.uiProgress !== 'function') {
throw new Error('$tera.uiProgress must be a function');
}
};
const mapSetToObject = (item) => {
try {
if (item instanceof Map) {
debugLog('Converting Map to object');
const obj = { __isMap: true };
item.forEach((value, key) => {
obj[key] = mapSetToObject(value);
});
return obj;
}
if (item instanceof Set) {
debugLog('Converting Set to array');
return {
__isSet: true,
values: Array.from(item).map(mapSetToObject),
};
}
if (Array.isArray(item)) {
return item.map(mapSetToObject);
}
if (item && typeof item === 'object' && !(item instanceof Date)) {
const obj = {};
Object.entries(item).forEach(([key, value]) => {
obj[key] = mapSetToObject(value);
});
return obj;
}
return item;
}
catch (error) {
logError(error, 'mapSetToObject conversion failed');
throw error;
}
};
const objectToMapSet = (obj) => {
try {
if (!obj || typeof obj !== 'object' || obj instanceof Date) {
return obj;
}
if ('__isMap' in obj) {
debugLog('Converting object back to Map');
const map = new Map();
Object.entries(obj).forEach(([key, value]) => {
if (key !== '__isMap') {
map.set(key, objectToMapSet(value));
}
});
return map;
}
if ('__isSet' in obj) {
debugLog('Converting array back to Set');
return new Set(obj.values.map(objectToMapSet));
}
if (Array.isArray(obj)) {
return obj.map(objectToMapSet);
}
const newObj = {};
Object.entries(obj).forEach(([key, value]) => {
newObj[key] = objectToMapSet(value);
});
return newObj;
}
catch (error) {
logError(error, 'objectToMapSet conversion failed');
throw error;
}
};
const showNotification = (message) => {
if (typeof window !== 'undefined' && window.alert) {
window.alert(message);
}
else {
debugLog('Alert would be shown:', message);
}
};
class StoreAdapter {
constructor(store) {
this.store = store;
this.unsubscribe = () => { };
if (this.constructor === StoreAdapter) {
throw new Error("Abstract classes can't be instantiated.");
}
}
destroy() {
this.unsubscribe();
}
}
class VuexAdapter extends StoreAdapter {
constructor(store) {
super(store);
this.store = store;
}
setup() {
debugLog('Setting up Vuex adapter.');
if (this.store.hasModule('__tera_file_sync')) {
debugLog('Vuex module __tera_file_sync already registered.');
return;
}
this.store.registerModule('__tera_file_sync', {
namespaced: true,
state: { saveStatus: SaveStatus.SAVED },
mutations: {
updateSaveStatus(state, status) {
state.saveStatus = status;
},
},
getters: {
getSaveStatus: (state) => state.saveStatus
}
});
}
getState() {
const _a = this.store.state, { __tera_file_sync } = _a, stateToSave = __rest(_a, ["__tera_file_sync"]);
return stateToSave;
}
replaceState(newState) {
this.store.replaceState(Object.assign(Object.assign({}, this.store.state), newState));
}
updateSaveStatus(status) {
this.store.commit('__tera_file_sync/updateSaveStatus', status);
}
subscribe(callback) {
this.unsubscribe = this.store.subscribe((mutation) => {
if (mutation.type.startsWith('__tera_file_sync/'))
return;
callback(mutation);
});
}
destroy() {
super.destroy();
if (this.store.hasModule('__tera_file_sync')) {
this.store.unregisterModule('__tera_file_sync');
debugLog('Unregistered Vuex module.');
}
}
}
class PiniaAdapter extends StoreAdapter {
constructor(store) {
super(store);
this.store = store;
}
setup() {
debugLog('Setting up Pinia adapter.');
if (this.store.saveStatus === undefined) {
throw new Error("Pinia store must have a 'saveStatus' property in its state for TERA File Sync to work.");
}
}
getState() {
const _a = this.store.$state, { saveStatus: _saveStatus } = _a, stateToSave = __rest(_a, ["saveStatus"]);
return stateToSave;
}
replaceState(newState) {
this.store.$patch(newState);
}
updateSaveStatus(status) {
this.store.saveStatus = status;
}
subscribe(callback) {
this.unsubscribe = this.store.$subscribe((mutation) => {
if (mutation.events && mutation.events.key === 'saveStatus')
return;
callback(mutation);
});
}
}
class PlainObjectAdapter extends StoreAdapter {
constructor(store) {
super(store);
this.store = store;
}
setup() {
debugLog('Setting up PlainObjectAdapter.');
}
getState() {
if (typeof this.store.getState !== 'function') {
throw new Error("The provided object for PlainObjectAdapter must have a 'getState' method.");
}
return this.store.getState();
}
replaceState(newState) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof this.store.replaceState !== 'function') {
throw new Error("The provided object for PlainObjectAdapter must have a 'replaceState' method.");
}
yield this.store.replaceState(newState);
});
}
updateSaveStatus(status) {
if (typeof this.store.updateSaveStatus !== 'function') {
throw new Error("The provided object for PlainObjectAdapter must have an 'updateSaveStatus' method.");
}
this.store.updateSaveStatus(status);
}
subscribe(callback) {
if (typeof this.store.subscribe !== 'function') {
throw new Error("The provided object for PlainObjectAdapter must have a 'subscribe' method.");
}
this.unsubscribe = this.store.subscribe(callback);
}
}
class TeraFileSyncPlugin {
get projectId() {
var _a, _b, _c;
if (!((_c = (_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.project) === null || _c === void 0 ? void 0 : _c.id)) {
throw new Error("TERA project context is missing.");
}
return String(this.vueInstance.$tera.project.id);
}
get saveStatus() {
return this._saveStatus;
}
constructor(config, adapter) {
this.initialized = false;
this.teraReady = false;
this.vueInstance = null;
this.userId = null;
this.saveInProgress = false;
this.autoSaveInterval = null;
this.hasShownInitialAlert = false;
this._saveStatus = SaveStatus.SAVED;
const mergedConfig = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
validateConfig(mergedConfig);
this.config = mergedConfig;
if (!(adapter instanceof StoreAdapter)) {
throw new Error("A valid store adapter must be provided.");
}
this.adapter = adapter;
this.adapter.setup();
this.keydownHandler = this.handleKeyDown.bind(this);
this.beforeUnloadHandler = this.handleBeforeUnload.bind(this);
}
// --- Public API Methods ---
setVueInstance(instance) {
this.vueInstance = instance;
}
setTeraReady() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.vueInstance)
throw new Error("Vue instance must be set before calling setTeraReady.");
validateVueInstance(this.vueInstance);
this.teraReady = true;
yield this.initializeState({ loadImmediately: this.config.loadImmediately });
this.setupAutoSave();
this.setupStateChangeTracking();
});
}
saveState() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.saveStateToFile();
});
}
promptForNewJsonFile() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.setStateFromPromptedJsonFile();
});
}
loadAndApplyStateFromFile() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
if (!((_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera)) {
logError(new Error('TERA API not available'), 'State load failed.');
return false;
}
try {
yield this.vueInstance.$tera.uiProgress({ title: 'Loading tool data', backdrop: 'static' });
const fileData = yield this.loadStateFromFile();
if (fileData) {
// If a reset function is provided, call it before applying the state.
if (this.config.resetState) {
debugLog('Calling provided store reset function (async).');
yield this.config.resetState();
}
// Extract file data and replace state
const parsedState = objectToMapSet(fileData);
yield this.adapter.replaceState(parsedState);
debugLog('Store state replaced from file data.');
this.updateSaveStatus(SaveStatus.SAVED);
return true;
}
else {
debugLog('No file data found to load. Using default state.');
this.updateSaveStatus(SaveStatus.UNSAVED);
return true;
}
}
catch (error) {
logError(error, 'State load and apply failed.');
showNotification('Error loading state from file.');
return false;
}
finally {
if ((_c = (_b = this.vueInstance) === null || _b === void 0 ? void 0 : _b.$tera) === null || _c === void 0 ? void 0 : _c.uiProgress) {
yield this.vueInstance.$tera.uiProgress(false);
}
}
});
}
getFileMetadata() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.vueInstance)
return null;
try {
const fileName = yield this.getStorageFileName();
if (!fileName) {
debugLog('Cannot get metadata, no storage file name available.');
return null;
}
const fileObject = yield this.vueInstance.$tera.getProjectFile(fileName, { cache: false });
if (!fileObject) {
debugLog(`Could not retrieve file metadata for ${fileName}.`);
return null;
}
const metadata = {
modified: new Date(fileObject.modified),
};
debugLog(`Retrieved metadata for ${fileName}:`, metadata);
return metadata;
}
catch (error) {
if (error.message && error.message.includes('not found')) {
debugLog('State file not found, no metadata available.');
return null;
}
logError(error, 'Failed to get file metadata');
return null;
}
});
}
destroy() {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
}
this.unregisterHotkeys();
this.unregisterBeforeUnload();
this.adapter.destroy();
this.initialized = false;
this.teraReady = false;
}
createDataFileBackup() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!this.vueInstance) {
logError(new Error('Vue instance not set'), 'Backup failed');
return false;
}
try {
if (this.vueInstance.$tera.uiProgress) {
yield this.vueInstance.$tera.uiProgress({ title: 'Creating backup...', backdrop: 'static' });
}
// Get the data to backup
let dataToBackup = mapSetToObject(this.adapter.getState());
if (!dataToBackup) {
debugLog('No existing file found on disk for backup. Backing up current from disk instead.');
dataToBackup = yield this.loadStateFromFile();
}
// Generate a formatted datetime string (YYYYMMDD-HHMMSS)
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const current_datetime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
// Construct the backup filename
const backupFileName = `data-${this.config.keyPrefix}-backup-${current_datetime}.json`;
// Create the new file in TERA
const backupFile = yield (0, p_retry_1.default)(() => this.vueInstance.$tera.createProjectFile(backupFileName), {
retries: 2,
minTimeout: 200,
onFailedAttempt: error => debugLog(`[Create backup file attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`),
});
// Write the data to the newly created file
yield (0, p_retry_1.default)(() => backupFile.setContents(dataToBackup), {
retries: 2,
minTimeout: 200,
onFailedAttempt: error => debugLog(`[Set backup contents attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`),
});
debugLog(`Backup created successfully: ${backupFileName}`);
// Optional: Show a success notification if Element UI or similar is available
if (this.vueInstance.$notify) {
this.vueInstance.$notify({
title: 'Backup Successful',
message: `Backup saved as ${backupFileName}`,
type: 'success'
});
}
return true;
}
catch (error) {
logError(error, 'Failed to create data file backup');
showNotification('Failed to create backup file.');
return false;
}
finally {
if ((_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.uiProgress) {
yield this.vueInstance.$tera.uiProgress(false);
}
}
});
}
// --- Internal Methods ---
handleKeyDown(event) {
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
debugLog('Ctrl+S hotkey detected, saving state');
this.saveStateToFile().then(success => {
if (success) {
debugLog('Save completed via hotkey');
}
});
}
}
handleBeforeUnload(event) {
if (this._saveStatus === SaveStatus.UNSAVED) {
const message = 'You have unsaved changes. Are you sure you want to leave?';
event.returnValue = message;
return message;
}
return undefined;
}
registerHotkeys() {
if (!this.config.enableSaveHotkey) {
debugLog('Save hotkey disabled in configuration');
return;
}
debugLog('Registering Ctrl+S hotkey');
if (typeof window !== 'undefined') {
window.removeEventListener('keydown', this.keydownHandler);
window.addEventListener('keydown', this.keydownHandler);
}
}
unregisterHotkeys() {
if (typeof window !== 'undefined') {
window.removeEventListener('keydown', this.keydownHandler);
debugLog('Unregistered hotkeys');
}
}
registerBeforeUnload() {
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', this.beforeUnloadHandler);
debugLog('Registered beforeunload listener');
}
}
unregisterBeforeUnload() {
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
debugLog('Unregistered beforeunload listener');
}
}
showInitialAlert() {
if (this.config.showInitialAlert && !this.hasShownInitialAlert) {
this.hasShownInitialAlert = true;
const message = "This TERA tool no longer automatically saves progress, use Ctrl+S, or click save in the top right corner, to save progress. (This is a short-term temporary pop-up, it will be removed at the start of April)";
if (this.vueInstance && this.vueInstance.$notify) {
this.vueInstance.$notify({
title: 'Important',
message,
type: 'warning',
duration: 10000,
showClose: true,
});
}
else {
setTimeout(() => showNotification(message), 1000);
}
debugLog('Showed initial manual save alert');
}
}
updateSaveStatus(status) {
debugLog(`Updating save status: ${status}`);
this._saveStatus = status;
this.adapter.updateSaveStatus(status);
}
getStorageKey() {
return __awaiter(this, void 0, void 0, function* () {
if (this.config.isSeparateStateForEachUser) {
if (!this.userId) {
if (!this.vueInstance)
throw new Error("Vue instance not available to get user ID.");
try {
const user = yield this.vueInstance.$tera.getUser();
this.userId = user.id;
debugLog('User ID initialized:', this.userId);
}
catch (error) {
logError(error, 'Failed to get user ID');
throw error;
}
}
return `${this.config.keyPrefix}-${this.userId}`;
}
return `${this.config.keyPrefix}`;
});
}
getStorageFileName() {
return __awaiter(this, arguments, void 0, function* ({ returnFullPath = false } = {}) {
var _a, _b, _c;
if (!((_c = (_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.project) === null || _c === void 0 ? void 0 : _c.id)) {
throw new Error("Error getting fileStorageName: TERA project context is missing.");
}
if (!this.vueInstance.$tera.project.temp) {
console.warn("Warning: $tera.project.temp is missing. Creating it.");
this.vueInstance.$tera.project.temp = {};
}
const key = yield this.getStorageKey();
let fileStorageName = this.vueInstance.$tera.project.temp[key];
if (!fileStorageName) {
debugLog("No existing file for project/tool, creating one");
fileStorageName = `data-${this.config.keyPrefix}-${(0, nanoid_1.nanoid)()}.json`;
const newFile = yield (0, p_retry_1.default)(() => this.vueInstance.$tera.createProjectFile(fileStorageName), {
retries: 2,
minTimeout: 200,
onFailedAttempt: error => debugLog(`[Create file attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`),
});
yield (0, p_retry_1.default)(() => newFile.setContents(this.adapter.getState()), {
retries: 2,
minTimeout: 200,
onFailedAttempt: error => debugLog(`[Set contents attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`),
});
yield (0, p_retry_1.default)(() => this.vueInstance.$tera.setProjectState(`temp.${key}`, fileStorageName), {
retries: 2,
minTimeout: 200,
onFailedAttempt: error => debugLog(`[Set storage key attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`),
});
}
if (typeof fileStorageName !== 'string') {
throw new Error(`fileStorageName is not a string: ${fileStorageName}`);
}
return returnFullPath ? `${this.vueInstance.$tera.project.id}/${fileStorageName}` : fileStorageName;
});
}
loadStateFromFile() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!this.vueInstance)
return null;
let fileName = '';
try {
let fileContent;
yield (0, p_retry_1.default)(() => __awaiter(this, void 0, void 0, function* () {
fileName = yield this.getStorageFileName();
debugLog(`Loading state from file: ${fileName}`);
if (!fileName)
throw new Error('No file name returned when expected!');
if (!this.vueInstance)
throw new Error('this.vueInstance expected!');
fileContent = yield api.getFileContent(this.projectId, fileName, this.vueInstance.$tera);
}), {
retries: 3,
minTimeout: 1000,
factor: 2,
onFailedAttempt: error => debugLog(`[Load Attempt ${error.attemptNumber}] Failed for ${fileName}. Retries left: ${error.retriesLeft}.`),
});
if (!fileContent) {
debugLog('File not found or empty');
return null;
}
debugLog('State loaded from file successfully:', fileContent);
return fileContent;
}
catch (error) {
if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("Unexpected end of JSON input"))) {
debugLog('State file not found or corrupt, will be created on first save');
return null;
}
logError(error, 'Failed to load state from file');
showNotification('Failed to load state from file, using default state');
return null;
}
});
}
saveStateToFile() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!this.vueInstance)
return false;
if (this.config.onBeforeSave) {
const validationResult = this.config.onBeforeSave();
if (validationResult !== true) {
debugLog('Save prevented by onBeforeSave hook. Reason:', validationResult);
if (typeof validationResult === 'string' && validationResult.length > 0) {
showNotification(validationResult);
}
return false;
}
}
if (this.saveInProgress) {
debugLog('Save already in progress, skipping');
return false;
}
const state = this.adapter.getState();
let fileName = '';
try {
this.saveInProgress = true;
this.updateSaveStatus(SaveStatus.SAVING);
yield this.vueInstance.$tera.uiProgress({ title: 'Saving tool data', backdrop: 'static' });
yield (0, p_retry_1.default)(() => __awaiter(this, void 0, void 0, function* () {
var _a;
fileName = yield this.getStorageFileName();
if (!fileName)
throw new Error('No fileName returned');
const stateToSave = mapSetToObject(state);
if (!this.vueInstance)
throw new Error('this.vueInstance expected!');
yield api.saveFileContent(this.projectId, fileName, stateToSave, (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera);
}), {
retries: 3,
minTimeout: 1000,
factor: 2,
onFailedAttempt: error => debugLog(`[Save Attempt ${error.attemptNumber}] Failed for ${fileName}. Retries left: ${error.retriesLeft}.`),
});
this.updateSaveStatus(SaveStatus.SAVED);
debugLog(`State saved to file: ${fileName}`);
return true;
}
catch (error) {
logError(error, 'Failed to save state to file');
showNotification('Failed to save state to file, hit F12 for debug information.');
this.updateSaveStatus(SaveStatus.UNSAVED);
return false;
}
finally {
this.saveInProgress = false;
if ((_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.uiProgress) {
yield this.vueInstance.$tera.uiProgress(false);
}
}
});
}
initializeState() {
return __awaiter(this, arguments, void 0, function* ({ loadImmediately = true } = {}) {
var _a;
if (!this.teraReady || !((_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera)) {
debugLog('TERA not ready, skipping initialization');
return;
}
// Only show UI progress if we are actually going to load immediately
if (loadImmediately && this.vueInstance.$tera.uiProgress) {
yield this.vueInstance.$tera.uiProgress({ title: 'Loading tool data', backdrop: 'static' });
}
try {
if (loadImmediately) {
debugLog('Initializing with immediate load from file.');
yield this.loadAndApplyStateFromFile();
}
else {
debugLog('Skipping immediate load. State will be considered unsaved until loaded.');
this.updateSaveStatus(SaveStatus.UNSAVED);
}
this.showInitialAlert();
this.registerHotkeys();
this.registerBeforeUnload();
}
catch (error) {
logError(error, 'State initialization failed');
showNotification('Error initializing state from file');
}
finally {
this.initialized = true;
// Only hide UI progress if we showed it in the first place
if (loadImmediately && this.vueInstance.$tera.uiProgress) {
yield this.vueInstance.$tera.uiProgress(false);
}
}
});
}
setupAutoSave() {
if (this.config.autoSaveIntervalMinutes <= 0) {
debugLog('Auto-save disabled');
return;
}
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
}
const intervalMs = this.config.autoSaveIntervalMinutes * 60 * 1000;
debugLog(`Setting up auto-save every ${this.config.autoSaveIntervalMinutes} minutes`);
this.autoSaveInterval = setInterval(() => {
if (this._saveStatus !== SaveStatus.SAVED) {
debugLog('Auto-save triggered');
this.saveStateToFile();
}
else {
debugLog('Auto-save skipped - no changes detected');
}
}, intervalMs);
}
setupStateChangeTracking() {
this.adapter.subscribe(() => {
if (!this.saveInProgress) {
this.updateSaveStatus(SaveStatus.UNSAVED);
}
});
}
setStateFromPromptedJsonFile() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.vueInstance)
return;
try {
const projectFile = yield this.vueInstance.$tera.selectProjectFile({
title: 'Load JSON file',
showHiddenFiles: true,
filters: {
ext: 'json', // Only show files with the 'json' extension
},
});
if (!projectFile) {
debugLog('User cancelled file selection.');
return;
}
const fileData = yield (0, p_retry_1.default)(() => projectFile.getContents({ format: 'json' }), {
retries: 3,
minTimeout: 1000,
onFailedAttempt: error => debugLog(`[Load Prompted File Attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`),
});
if (!fileData) {
debugLog('Selected file is empty.');
showNotification('The selected file is empty.');
return;
}
const parsedState = objectToMapSet(fileData);
yield this.adapter.replaceState(parsedState);
const key = yield this.getStorageKey();
// Clear the current file association in TERA's project state (both local cache and persistent state).
// This ensures the plugin no longer points to the previous file, nor does it link to the
// specific file the user just selected.
if (this.vueInstance.$tera.project.temp) {
delete this.vueInstance.$tera.project.temp[key];
}
yield this.vueInstance.$tera.setProjectState(`temp.${key}`, null);
// Trigger a save. Since the key is cleared above, getStorageFileName will:
// 1. Generate a NEW unique filename (data-prefix-nanoid.json)
// 2. Create that file
// 3. Write the current store state (the imported data) to it
// 4. Set the temp key to point to this NEW file
debugLog('Store initialized from prompted file data. Saving to new copy...');
yield this.saveStateToFile();
this.updateSaveStatus(SaveStatus.SAVED);
}
catch (error) {
logError(error, 'Failed to set state from prompted JSON file');
showNotification('Failed to load data from the selected file.');
this.updateSaveStatus(SaveStatus.UNSAVED);
}
});
}
}
/**
* Creates and initializes a new TERA file sync manager.
* This is the main entry point for using the plugin.
*
* @param config - Plugin configuration options.
* @param store - The Vuex store, Pinia store, or a custom aggregator object that conforms to the `PlainObjectStore` interface.
* @returns The plugin instance, providing a public API to manage state synchronization.
*/
function createTeraSync(config, store) {
let adapter;
// Type-safe "sniffing" to determine the store type.
if (store &&
typeof store.getState === 'function' &&
typeof store.replaceState === 'function' &&
typeof store.updateSaveStatus === 'function' &&
typeof store.subscribe === 'function') {
debugLog('Detected custom aggregator object. Using PlainObjectAdapter.');
adapter = new PlainObjectAdapter(store);
}
else if (store && typeof store.$id === 'string' && typeof store.$patch === 'function') {
debugLog('Detected Pinia store. Using PiniaAdapter.');
adapter = new PiniaAdapter(store);
}
else if (store && typeof store.commit === 'function' && typeof store.subscribe === 'function') {
debugLog('Detected Vuex store. Using VuexAdapter.');
adapter = new VuexAdapter(store);
}
else {
throw new Error('Could not determine store type. Please provide a valid Vuex store, Pinia store, or a custom aggregator object.');
}
const plugin = new TeraFileSyncPlugin(config, adapter);
return plugin;
}
//# sourceMappingURL=index.js.map