183 lines
6.9 KiB
JavaScript
183 lines
6.9 KiB
JavaScript
(function (root, factory) {
|
|
const api = factory();
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
else root.createOfflineWorkStore = api;
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
|
|
const ENABLED_KEY = 'stackchain.offline-work.enabled.v1';
|
|
const SNAPSHOT_KEY = 'stackchain.offline-work.snapshot.v1';
|
|
const DETAILS_KEY = 'stackchain.offline-work.details.v1';
|
|
const VERSION = 1;
|
|
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
const ITEM_FIELDS = [
|
|
'id', 'number', 'title', 'state', 'repository', 'labels', 'assignees',
|
|
'updated_at', 'due_date', 'milestone', 'url', 'work_reasons',
|
|
];
|
|
const NOTIFICATION_FIELDS = [
|
|
'id', 'number', 'title', 'unread', 'repository', 'subject_type', 'updated_at', 'url',
|
|
];
|
|
const DETAIL_FIELDS = [
|
|
'title', 'body', 'state', 'labels', 'assignees', 'author', 'url', 'due_date', 'milestone',
|
|
'id', 'repository', 'subject_type', 'subject_body', 'source_updated_at', 'head_sha', 'ci_state',
|
|
];
|
|
const COMMENT_FIELDS = ['id', 'author', 'body', 'created_at', 'updated_at', 'url'];
|
|
const REVIEW_FILE_FIELDS = [
|
|
'filename', 'status', 'additions', 'deletions', 'diff_available', 'diff_binary', 'diff_truncated',
|
|
];
|
|
const REVIEW_FIELDS = ['id', 'state', 'body', 'submitted_at'];
|
|
|
|
function pick(source, fields) {
|
|
const output = {};
|
|
fields.forEach(field => {
|
|
if (source && source[field] !== undefined) output[field] = source[field];
|
|
});
|
|
return output;
|
|
}
|
|
|
|
function createOfflineWorkStore({
|
|
storage, now = () => new Date(), maxAgeMs = DEFAULT_MAX_AGE_MS, maxDetails = 10,
|
|
}) {
|
|
function enabled() {
|
|
try { return storage.getItem(ENABLED_KEY) === 'true'; }
|
|
catch (_) { return false; }
|
|
}
|
|
|
|
function clear() {
|
|
try {
|
|
storage.removeItem(SNAPSHOT_KEY);
|
|
storage.removeItem(DETAILS_KEY);
|
|
}
|
|
catch (_) { return false; }
|
|
return true;
|
|
}
|
|
|
|
function detailKey(item) {
|
|
if (item?.kind === 'update') {
|
|
const notificationId = Number(item?.notification_id || 0);
|
|
return Number.isInteger(notificationId) && notificationId > 0 ? 'update:' + notificationId : '';
|
|
}
|
|
const kind = item?.kind === 'review' ? 'review' :
|
|
item?.kind === 'pull' ? 'pull' : item?.kind === 'issue' ? 'issue' : '';
|
|
const repository = String(item?.repository || '');
|
|
const number = Number(item?.number || 0);
|
|
return kind && repository && number > 0 ? [kind, repository, number].join(':') : '';
|
|
}
|
|
|
|
function readDetails() {
|
|
try {
|
|
const value = JSON.parse(storage.getItem(DETAILS_KEY) || '[]');
|
|
return Array.isArray(value) ? value : [];
|
|
} catch (_) {
|
|
try { storage.removeItem(DETAILS_KEY); } catch (_error) { /* Best effort purge. */ }
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveDetail(login, item, detail) {
|
|
const key = detailKey(item);
|
|
login = String(login || '').trim();
|
|
if (!enabled() || !login || !key || !detail) return false;
|
|
const conversation = detail.conversation || {};
|
|
const data = {
|
|
...pick(detail, DETAIL_FIELDS),
|
|
conversation: {
|
|
comments: (conversation.comments || []).slice(-20).map(comment => pick(comment, COMMENT_FIELDS)),
|
|
page: Number(conversation.page || 1),
|
|
older_page: conversation.older_page ?? null,
|
|
total: Number(conversation.total || 0),
|
|
},
|
|
};
|
|
if (item?.is_review || item?.kind === 'review') {
|
|
data.files = (detail.files || []).slice(0, 50).map(file => ({
|
|
...pick(file, REVIEW_FILE_FIELDS),
|
|
diff_lines: (file?.diff_lines || []).slice(0, 400).map(line => String(line)),
|
|
}));
|
|
data.reviews = (detail.reviews || []).slice(-20).map(review => ({
|
|
...pick(review, REVIEW_FIELDS),
|
|
user: pick(review?.user, ['login']),
|
|
}));
|
|
}
|
|
const record = {
|
|
key,
|
|
user_login: login,
|
|
saved_at: now().toISOString(),
|
|
data,
|
|
};
|
|
const records = readDetails().filter(candidate =>
|
|
!(candidate?.key === key && candidate?.user_login === login)
|
|
);
|
|
records.push(record);
|
|
try { storage.setItem(DETAILS_KEY, JSON.stringify(records.slice(-Math.max(1, maxDetails)))); }
|
|
catch (_) { return false; }
|
|
return true;
|
|
}
|
|
|
|
function loadDetail(login, item) {
|
|
const key = detailKey(item);
|
|
login = String(login || '').trim();
|
|
if (!login || !key) return null;
|
|
const records = readDetails();
|
|
const currentTime = now().getTime();
|
|
const valid = records.filter(record => {
|
|
const savedAt = Date.parse(record?.saved_at || '');
|
|
return record?.key && record?.user_login && record?.data && Number.isFinite(savedAt) &&
|
|
currentTime - savedAt <= maxAgeMs;
|
|
});
|
|
if (valid.length !== records.length) {
|
|
try { storage.setItem(DETAILS_KEY, JSON.stringify(valid)); } catch (_) { /* Best effort purge. */ }
|
|
}
|
|
const record = valid.find(candidate => candidate.key === key && candidate.user_login === login);
|
|
return record ? { ...record.data, saved_at: record.saved_at } : null;
|
|
}
|
|
|
|
function setEnabled(value) {
|
|
try {
|
|
storage.setItem(ENABLED_KEY, value ? 'true' : 'false');
|
|
if (!value) clear();
|
|
return true;
|
|
} catch (_) { return false; }
|
|
}
|
|
|
|
function save(snapshot) {
|
|
if (!enabled() || !snapshot?.user?.login) return false;
|
|
const record = {
|
|
version: VERSION,
|
|
user_login: String(snapshot.user.login),
|
|
saved_at: now().toISOString(),
|
|
data: {
|
|
user: pick(snapshot.user, ['login', 'full_name']),
|
|
issues: (snapshot.issues || []).map(item => pick(item, ITEM_FIELDS)),
|
|
pull_requests: (snapshot.pull_requests || []).map(item => pick(item, ITEM_FIELDS)),
|
|
notifications: (snapshot.notifications || []).map(item => pick(item, NOTIFICATION_FIELDS)),
|
|
work_pagination: snapshot.work_pagination || {},
|
|
notification_pagination: snapshot.notification_pagination || {},
|
|
},
|
|
};
|
|
try { storage.setItem(SNAPSHOT_KEY, JSON.stringify(record)); }
|
|
catch (_) { return false; }
|
|
return true;
|
|
}
|
|
|
|
function load(expectedLogin) {
|
|
let record;
|
|
try { record = JSON.parse(storage.getItem(SNAPSHOT_KEY) || 'null'); }
|
|
catch (_) { clear(); return null; }
|
|
const savedAt = Date.parse(record?.saved_at || '');
|
|
const invalid = record?.version !== VERSION || !record?.user_login || !record?.data ||
|
|
!Number.isFinite(savedAt);
|
|
const expired = Number.isFinite(savedAt) && now().getTime() - savedAt > maxAgeMs;
|
|
if (invalid || expired) {
|
|
clear();
|
|
return null;
|
|
}
|
|
if (expectedLogin && record.user_login !== expectedLogin) return null;
|
|
return { ...record.data, saved_at: record.saved_at };
|
|
}
|
|
|
|
return { enabled, setEnabled, save, load, saveDetail, loadDetail, clear };
|
|
}
|
|
|
|
return createOfflineWorkStore;
|
|
});
|