share rule optimization. NOT YET OPENED OR TESTED

This commit is contained in:
2026-02-19 12:01:54 -06:00
parent a1ebb8f898
commit ce7398b660
+133 -27
View File
@@ -435,6 +435,16 @@ const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
// ShareRule match
const matchingRule = store.shareRules.find(rule => {
if (rule.targetUserId !== currentUserId) return false;
// Global Rule Logic: If owner == target, it's a "Global Subscription" to that tag.
// We ignore the rule.ownerId check (which would match the task.user)
// and instead match purely on the tag.
if (rule.ownerId === rule.targetUserId && rule.type === 'tag') {
const tags = task.tags || [];
return tags.some(t => t.toLowerCase() === (rule.tagName || "").toLowerCase());
}
// Standard Rule Logic: Match specific owner
if (task.user !== rule.ownerId) return false;
if (rule.type === 'all') return true;
if (rule.type === 'tag') {
@@ -799,18 +809,48 @@ export const initStore = async () => {
// Share Rules Incomplete
if (incomingRules.length > 0) {
const ownerRules = new Map<string, { type: 'all' | 'tag', tagName?: string }[]>();
// Separate "Global Rules" (Self-Rules) from "Specific User Rules"
const globalTagRules: string[] = [];
const specificUserRules = new Map<string, { type: 'all' | 'tag', tagName?: string }[]>();
incomingRules.forEach((rule: any) => {
const existing = ownerRules.get(rule.ownerId) || [];
existing.push({ type: rule.type, tagName: rule.tagName });
ownerRules.set(rule.ownerId, existing);
if (rule.ownerId === rule.targetUserId && rule.type === 'tag' && rule.tagName) {
globalTagRules.push(rule.tagName);
} else {
const existing = specificUserRules.get(rule.ownerId) || [];
existing.push({ type: rule.type, tagName: rule.tagName });
specificUserRules.set(rule.ownerId, existing);
}
});
Array.from(ownerRules.entries()).forEach(([ownerId, rules]) => {
// 1. Fetch Global Rule Tasks (from ANY user)
if (globalTagRules.length > 0) {
// We want tasks that have ANY of these tags, AND are not completed
// filter: (tags ~ "A" || tags ~ "B") && completed = false
const tagFilter = globalTagRules.map(t => `tags ~ "${t}"`).join(' || ');
incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({
filter: `(${tagFilter}) && completed = false`,
sort: '-created',
fields: LIST_FIELDS,
requestKey: null
}).catch(() => []));
}
// 2. Fetch Specific User Rule Tasks (from specific owner)
Array.from(specificUserRules.entries()).forEach(([ownerId, rules]) => {
const hasAllRule = rules.some(r => r.type === 'all');
const tagRules = rules.filter(r => r.type === 'tag').map(r => r.tagName).filter(Boolean);
let filter = `user = "${ownerId}" && completed = false`;
if (!hasAllRule && tagRules.length > 0) filter += ` && (${tagRules.map(t => `tags ~ "${t}"`).join(' || ')})`;
if (hasAllRule) {
// Get everything from this user
} else if (tagRules.length > 0) {
// Get specific tags from this user
filter += ` && (${tagRules.map(t => `tags ~ "${t}"`).join(' || ')})`;
} else {
return; // No valid rules for this user
}
incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({
filter,
@@ -1363,6 +1403,31 @@ export const upsertTagDefinition = async (name: string, value: number, color?: s
theme: theme !== undefined ? theme : existing.theme
});
} else {
// Check for case-insensitive duplicate
const nameLower = name.toLowerCase();
const duplicate = store.tagDefinitions.find(d => d.name.toLowerCase() === nameLower);
if (duplicate) {
if (duplicate.name !== name) {
// Case difference only -> Update to new casing?
// Or just warn? "Tag already exists with different casing".
// For now, we'll just return the existing one to be safe, or update it if needed.
// A strict interpretation of "There should never be two tags with the same name"
// usually implies normalization.
// But if the user typed "Work" and "work" exists, maybe they mean "work".
toast.error(`Tag "${duplicate.name}" already exists.`);
return;
}
// Exact match logic handled by 'existing' check above usually, but 'existing' uses strict equality?
// The existing check at top of function is strict: `existing = store.tagDefinitions.find(d => d.name === name);`
// So this block handles case-insensitive duplicates that weren't caught by strict check.
// We should update the existing one if we want to change casing, or just reject.
// Rejection is safer for unique constraint.
toast.error(`Tag "${duplicate.name}" already exists.`);
return;
}
const record = await pb.collection(TAGS_COLLECTION).create({
user: pb.authStore.model?.id,
name,
@@ -1537,27 +1602,23 @@ export const syncSystemTagsAndRules = async () => {
}
// --- User Share Rule Sync ---
if (user.id !== currentUserId) {
const matchingRules = normalizedRules.filter(r =>
r.targetUserId === user.id &&
r.type === 'tag' &&
r.tagName === userName
);
// OPTIMIZATION: We no longer create individual share rules for every user-pair.
// Instead, we rely on the "Global Rule" (Self-Rule) created above for the user's own name.
// When User A tags a task "Bob", the Global Rule for Bob (Owner=Bob, Target=Bob, Tag=Bob)
// will pick it up via the updated `shouldTaskBeVisible` logic.
if (matchingRules.length === 0) {
await addShareRule('tag', user.id, userName, 'ADD_USER');
} else if (matchingRules.length > 1) {
const [, ...remove] = matchingRules;
for (const rule of remove) {
try {
await pb.collection(SHARE_RULES_COLLECTION).delete(rule.id);
setStore("shareRules", list => list.filter(r => r.id !== rule.id));
} catch (e) {
console.error("Failed to delete duplicate rule:", e);
}
}
}
// If we want to support "Collab" (Edit access), we might still need specific rules or specific shares,
// but for "View" access (Handoff), the Global Rule is sufficient.
// We do NOT delete existing rules here to avoid data loss during transition,
// unless strictly requested. The user said "I want to optimize share rules... make them ownerless".
// So we stop creating new ones.
/*
if (user.id !== currentUserId) {
// Legacy loop removed to prevent exponential growth
}
*/
}
} catch (err) {
@@ -1573,6 +1634,14 @@ export const renameTagDefinition = async (oldName: string, newName: string) => {
if (!def) return;
const finalName = newName.trim();
const finalNameLower = finalName.toLowerCase();
// Check availability (case-insensitive)
const duplicate = store.tagDefinitions.find(d => d.name.toLowerCase() === finalNameLower && d.id !== def.id);
if (duplicate) {
toast.error(`Tag "${duplicate.name}" already exists.`);
return;
}
try {
await pb.collection(TAGS_COLLECTION).update(def.id, { name: finalName });
@@ -1621,12 +1690,32 @@ export const loadAllHistory = async () => {
}).catch(() => []));
}
// Add Share Rules
const incomingRules = store.shareRules.filter(r => r.targetUserId === userId);
if (incomingRules.length > 0) {
const rulesToFetch: string[] = [];
incomingRules.forEach((rule: any) => {
let filter = `user = "${rule.ownerId}"`;
if (rule.type === 'tag' && rule.tagName) filter += ` && tags ~ "${rule.tagName}"`;
// Global Rule (Self-Targeted)
if (rule.ownerId === rule.targetUserId && rule.type === 'tag' && rule.tagName) {
rulesToFetch.push(`tags ~ "${rule.tagName}"`);
} else {
// Standard Rule
let f = `user = "${rule.ownerId}"`;
if (rule.type === 'tag' && rule.tagName) f += ` && tags ~ "${rule.tagName}"`;
rulesToFetch.push(f);
}
});
// Optimization: Group Global Rules?
// For now, simpler to just push them all.
// But if we have many global rules (e.g. buckets), we might want to group them.
// The Global Rules usually behave like buckets.
// Let's just push them as individual queries for safety.
rulesToFetch.forEach(filter => {
promises.push(pb.collection(TASGRID_COLLECTION).getFullList({ filter, sort: '-created', fields: LIST_FIELDS, requestKey: null }).catch(() => []));
});
}
@@ -1896,6 +1985,23 @@ export const loadShareRules = async () => {
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string, share_mode: 'ADD_USER' | 'SEND_TASK' = 'ADD_USER') => {
if (!pb.authStore.isValid) return;
// Check for duplicates
// We check against the local store to avoid network round-trip delay issues
const currentUserId = pb.authStore.model?.id;
const exists = store.shareRules.some(r =>
r.ownerId === currentUserId &&
r.targetUserId === targetUserId &&
r.type === type &&
(type === 'all' || r.tagName === tagName)
// We don't strictly check share_mode because having two rules for same tag/person with different modes is confusing.
// Usually we update the existing one if mode changes.
);
if (exists) {
// toast.info("Share rule already exists.");
return; // Silent return or update?
}
try {
await pb.collection(SHARE_RULES_COLLECTION).create({
ownerId: pb.authStore.model?.id,