@directive-run/knowledge 0.2.0
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/LICENSE +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Example: notifications
|
|
2
|
+
// Source: examples/notifications/src/notifications.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Notifications & Toasts — Directive Modules
|
|
7
|
+
*
|
|
8
|
+
* Two modules:
|
|
9
|
+
* - notifications: queue management, auto-dismiss via constraints, overflow protection
|
|
10
|
+
* - app: action log that triggers cross-module notifications via effects
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type ModuleSchema, createModule, t } from "@directive-run/core";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface Notification {
|
|
20
|
+
id: string;
|
|
21
|
+
message: string;
|
|
22
|
+
level: "info" | "success" | "warning" | "error";
|
|
23
|
+
createdAt: number;
|
|
24
|
+
ttl: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Notifications Module
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export const notificationsSchema = {
|
|
32
|
+
facts: {
|
|
33
|
+
queue: t.object<Notification[]>(),
|
|
34
|
+
maxVisible: t.number(),
|
|
35
|
+
now: t.number(),
|
|
36
|
+
idCounter: t.number(),
|
|
37
|
+
},
|
|
38
|
+
derivations: {
|
|
39
|
+
visibleNotifications: t.object<Notification[]>(),
|
|
40
|
+
hasNotifications: t.boolean(),
|
|
41
|
+
oldestExpired: t.object<Notification | null>(),
|
|
42
|
+
},
|
|
43
|
+
events: {
|
|
44
|
+
addNotification: {
|
|
45
|
+
message: t.string(),
|
|
46
|
+
level: t.string(),
|
|
47
|
+
ttl: t.number().optional(),
|
|
48
|
+
},
|
|
49
|
+
dismissNotification: { id: t.string() },
|
|
50
|
+
tick: {},
|
|
51
|
+
setMaxVisible: { value: t.number() },
|
|
52
|
+
},
|
|
53
|
+
requirements: {
|
|
54
|
+
DISMISS_NOTIFICATION: { id: t.string() },
|
|
55
|
+
},
|
|
56
|
+
} satisfies ModuleSchema;
|
|
57
|
+
|
|
58
|
+
export const notificationsModule = createModule("notifications", {
|
|
59
|
+
schema: notificationsSchema,
|
|
60
|
+
|
|
61
|
+
init: (facts) => {
|
|
62
|
+
facts.queue = [];
|
|
63
|
+
facts.maxVisible = 5;
|
|
64
|
+
facts.now = Date.now();
|
|
65
|
+
facts.idCounter = 0;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Derivations
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
derive: {
|
|
73
|
+
visibleNotifications: (facts) => {
|
|
74
|
+
return (facts.queue as Notification[]).slice(
|
|
75
|
+
0,
|
|
76
|
+
facts.maxVisible as number,
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
hasNotifications: (facts) => {
|
|
81
|
+
return (facts.queue as Notification[]).length > 0;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
oldestExpired: (facts) => {
|
|
85
|
+
const queue = facts.queue as Notification[];
|
|
86
|
+
const oldest = queue[0];
|
|
87
|
+
if (!oldest) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ((facts.now as number) > oldest.createdAt + oldest.ttl) {
|
|
92
|
+
return oldest;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Constraints
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
constraints: {
|
|
104
|
+
autoDismiss: {
|
|
105
|
+
priority: 50,
|
|
106
|
+
when: (_facts, derive) => derive.oldestExpired !== null,
|
|
107
|
+
require: (_facts, derive) => ({
|
|
108
|
+
type: "DISMISS_NOTIFICATION" as const,
|
|
109
|
+
id: (derive.oldestExpired as Notification).id,
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
overflow: {
|
|
114
|
+
priority: 60,
|
|
115
|
+
when: (facts) => {
|
|
116
|
+
const queue = facts.queue as Notification[];
|
|
117
|
+
|
|
118
|
+
return queue.length > (facts.maxVisible as number) + 5;
|
|
119
|
+
},
|
|
120
|
+
require: (facts) => ({
|
|
121
|
+
type: "DISMISS_NOTIFICATION" as const,
|
|
122
|
+
id: (facts.queue as Notification[])[0].id,
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Resolvers
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
resolvers: {
|
|
132
|
+
dismiss: {
|
|
133
|
+
requirement: "DISMISS_NOTIFICATION",
|
|
134
|
+
resolve: async (req, context) => {
|
|
135
|
+
context.facts.queue = (context.facts.queue as Notification[]).filter(
|
|
136
|
+
(n) => n.id !== req.id,
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Events
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
events: {
|
|
147
|
+
addNotification: (
|
|
148
|
+
facts,
|
|
149
|
+
payload: { message: string; level: string; ttl?: number },
|
|
150
|
+
) => {
|
|
151
|
+
const ttlMap: Record<string, number> = {
|
|
152
|
+
info: 4000,
|
|
153
|
+
success: 3000,
|
|
154
|
+
warning: 6000,
|
|
155
|
+
error: 10000,
|
|
156
|
+
};
|
|
157
|
+
const counter = (facts.idCounter as number) + 1;
|
|
158
|
+
facts.idCounter = counter;
|
|
159
|
+
|
|
160
|
+
const notification: Notification = {
|
|
161
|
+
id: `notif-${counter}`,
|
|
162
|
+
message: payload.message,
|
|
163
|
+
level: payload.level as Notification["level"],
|
|
164
|
+
createdAt: Date.now(),
|
|
165
|
+
ttl: payload.ttl ?? ttlMap[payload.level] ?? 4000,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
facts.queue = [...(facts.queue as Notification[]), notification];
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
dismissNotification: (facts, { id }: { id: string }) => {
|
|
172
|
+
facts.queue = (facts.queue as Notification[]).filter((n) => n.id !== id);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
tick: (facts) => {
|
|
176
|
+
facts.now = Date.now();
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
setMaxVisible: (facts, { value }: { value: number }) => {
|
|
180
|
+
facts.maxVisible = value;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// App Module
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
export const appSchema = {
|
|
190
|
+
facts: {
|
|
191
|
+
actionLog: t.object<string[]>(),
|
|
192
|
+
},
|
|
193
|
+
events: {
|
|
194
|
+
simulateAction: { message: t.string(), level: t.string() },
|
|
195
|
+
},
|
|
196
|
+
} satisfies ModuleSchema;
|
|
197
|
+
|
|
198
|
+
export const appModule = createModule("app", {
|
|
199
|
+
schema: appSchema,
|
|
200
|
+
|
|
201
|
+
init: (facts) => {
|
|
202
|
+
facts.actionLog = [];
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
events: {
|
|
206
|
+
simulateAction: (facts, { message }: { message: string }) => {
|
|
207
|
+
facts.actionLog = [...(facts.actionLog as string[]), message];
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Example: optimistic-updates
|
|
2
|
+
// Source: examples/optimistic-updates/src/optimistic-updates.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Optimistic Updates — Directive Module
|
|
7
|
+
*
|
|
8
|
+
* Demonstrates optimistic mutations via events (instant UI), server sync via
|
|
9
|
+
* constraint-resolver pattern, per-operation rollback from a sync queue,
|
|
10
|
+
* resolver key deduplication, toast notifications, and context.snapshot().
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type ModuleSchema, createModule, t } from "@directive-run/core";
|
|
14
|
+
import { mockServerSync } from "./mock-server.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface TodoItem {
|
|
21
|
+
id: string;
|
|
22
|
+
text: string;
|
|
23
|
+
done: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type OpType = "toggle" | "delete" | "add";
|
|
27
|
+
|
|
28
|
+
export interface SyncQueueEntry {
|
|
29
|
+
opId: string;
|
|
30
|
+
itemId: string;
|
|
31
|
+
op: OpType;
|
|
32
|
+
undoItems: TodoItem[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EventLogEntry {
|
|
36
|
+
timestamp: number;
|
|
37
|
+
event: string;
|
|
38
|
+
detail: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// ID Generation
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
let nextId = 6; // items are pre-seeded 1-5
|
|
46
|
+
let nextOpId = 1;
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Schema
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
export const optimisticUpdatesSchema = {
|
|
53
|
+
facts: {
|
|
54
|
+
items: t.object<TodoItem[]>(),
|
|
55
|
+
syncQueue: t.object<SyncQueueEntry[]>(),
|
|
56
|
+
syncingOpId: t.string(),
|
|
57
|
+
newItemText: t.string(),
|
|
58
|
+
serverDelay: t.number(),
|
|
59
|
+
failRate: t.number(),
|
|
60
|
+
toastMessage: t.string(),
|
|
61
|
+
toastType: t.string(),
|
|
62
|
+
eventLog: t.object<EventLogEntry[]>(),
|
|
63
|
+
},
|
|
64
|
+
derivations: {
|
|
65
|
+
totalCount: t.number(),
|
|
66
|
+
doneCount: t.number(),
|
|
67
|
+
pendingCount: t.number(),
|
|
68
|
+
canAdd: t.boolean(),
|
|
69
|
+
isSyncing: t.boolean(),
|
|
70
|
+
},
|
|
71
|
+
events: {
|
|
72
|
+
toggleItem: { id: t.string() },
|
|
73
|
+
deleteItem: { id: t.string() },
|
|
74
|
+
addItem: {},
|
|
75
|
+
setNewItemText: { value: t.string() },
|
|
76
|
+
setServerDelay: { value: t.number() },
|
|
77
|
+
setFailRate: { value: t.number() },
|
|
78
|
+
dismissToast: {},
|
|
79
|
+
},
|
|
80
|
+
requirements: {
|
|
81
|
+
SYNC_TODO: {
|
|
82
|
+
opId: t.string(),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
} satisfies ModuleSchema;
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Helpers
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
function addLogEntry(facts: any, event: string, detail: string): void {
|
|
92
|
+
const log = [...(facts.eventLog as EventLogEntry[])];
|
|
93
|
+
log.push({ timestamp: Date.now(), event, detail });
|
|
94
|
+
if (log.length > 100) {
|
|
95
|
+
log.splice(0, log.length - 100);
|
|
96
|
+
}
|
|
97
|
+
facts.eventLog = log;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Module
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
export const optimisticUpdatesModule = createModule("optimistic-updates", {
|
|
105
|
+
schema: optimisticUpdatesSchema,
|
|
106
|
+
|
|
107
|
+
init: (facts) => {
|
|
108
|
+
facts.items = [
|
|
109
|
+
{ id: "1", text: "Buy groceries", done: false },
|
|
110
|
+
{ id: "2", text: "Learn Directive", done: true },
|
|
111
|
+
{ id: "3", text: "Walk the dog", done: false },
|
|
112
|
+
{ id: "4", text: "Read a book", done: false },
|
|
113
|
+
{ id: "5", text: "Fix the bug", done: true },
|
|
114
|
+
];
|
|
115
|
+
facts.syncQueue = [];
|
|
116
|
+
facts.syncingOpId = "";
|
|
117
|
+
facts.newItemText = "";
|
|
118
|
+
facts.serverDelay = 800;
|
|
119
|
+
facts.failRate = 30;
|
|
120
|
+
facts.toastMessage = "";
|
|
121
|
+
facts.toastType = "";
|
|
122
|
+
facts.eventLog = [];
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Derivations
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
derive: {
|
|
130
|
+
totalCount: (facts) => (facts.items as TodoItem[]).length,
|
|
131
|
+
|
|
132
|
+
doneCount: (facts) =>
|
|
133
|
+
(facts.items as TodoItem[]).filter((i) => i.done).length,
|
|
134
|
+
|
|
135
|
+
pendingCount: (facts) => (facts.syncQueue as SyncQueueEntry[]).length,
|
|
136
|
+
|
|
137
|
+
canAdd: (facts) => (facts.newItemText as string).trim() !== "",
|
|
138
|
+
|
|
139
|
+
isSyncing: (facts) => (facts.syncingOpId as string) !== "",
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Events
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
events: {
|
|
147
|
+
toggleItem: (facts, { id }) => {
|
|
148
|
+
const items = facts.items as TodoItem[];
|
|
149
|
+
const undoItems = items.map((i) => ({ ...i }));
|
|
150
|
+
|
|
151
|
+
facts.items = items.map((i) =>
|
|
152
|
+
i.id === id ? { ...i, done: !i.done } : i,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const opId = String(nextOpId++);
|
|
156
|
+
const queue = [...(facts.syncQueue as SyncQueueEntry[])];
|
|
157
|
+
queue.push({ opId, itemId: id, op: "toggle", undoItems });
|
|
158
|
+
facts.syncQueue = queue;
|
|
159
|
+
|
|
160
|
+
addLogEntry(facts, "optimistic", `Toggle item ${id}`);
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
deleteItem: (facts, { id }) => {
|
|
164
|
+
const items = facts.items as TodoItem[];
|
|
165
|
+
const undoItems = items.map((i) => ({ ...i }));
|
|
166
|
+
|
|
167
|
+
facts.items = items.filter((i) => i.id !== id);
|
|
168
|
+
|
|
169
|
+
const opId = String(nextOpId++);
|
|
170
|
+
const queue = [...(facts.syncQueue as SyncQueueEntry[])];
|
|
171
|
+
queue.push({ opId, itemId: id, op: "delete", undoItems });
|
|
172
|
+
facts.syncQueue = queue;
|
|
173
|
+
|
|
174
|
+
addLogEntry(facts, "optimistic", `Delete item ${id}`);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
addItem: (facts) => {
|
|
178
|
+
const text = (facts.newItemText as string).trim();
|
|
179
|
+
if (!text) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const items = facts.items as TodoItem[];
|
|
184
|
+
const undoItems = items.map((i) => ({ ...i }));
|
|
185
|
+
|
|
186
|
+
const itemId = String(nextId++);
|
|
187
|
+
facts.items = [...items, { id: itemId, text, done: false }];
|
|
188
|
+
facts.newItemText = "";
|
|
189
|
+
|
|
190
|
+
const opId = String(nextOpId++);
|
|
191
|
+
const queue = [...(facts.syncQueue as SyncQueueEntry[])];
|
|
192
|
+
queue.push({ opId, itemId, op: "add", undoItems });
|
|
193
|
+
facts.syncQueue = queue;
|
|
194
|
+
|
|
195
|
+
addLogEntry(facts, "optimistic", `Add item "${text}"`);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
setNewItemText: (facts, { value }) => {
|
|
199
|
+
facts.newItemText = value;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
setServerDelay: (facts, { value }) => {
|
|
203
|
+
facts.serverDelay = value;
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
setFailRate: (facts, { value }) => {
|
|
207
|
+
facts.failRate = value;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
dismissToast: (facts) => {
|
|
211
|
+
facts.toastMessage = "";
|
|
212
|
+
facts.toastType = "";
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Constraints
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
constraints: {
|
|
221
|
+
needsSync: {
|
|
222
|
+
priority: 100,
|
|
223
|
+
when: (facts) => {
|
|
224
|
+
const queue = facts.syncQueue as SyncQueueEntry[];
|
|
225
|
+
const syncingOpId = facts.syncingOpId as string;
|
|
226
|
+
|
|
227
|
+
return queue.length > 0 && syncingOpId === "";
|
|
228
|
+
},
|
|
229
|
+
require: (facts) => {
|
|
230
|
+
const queue = facts.syncQueue as SyncQueueEntry[];
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
type: "SYNC_TODO",
|
|
234
|
+
opId: queue[0].opId,
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Resolvers
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
resolvers: {
|
|
245
|
+
syncTodo: {
|
|
246
|
+
requirement: "SYNC_TODO",
|
|
247
|
+
key: (req) => `sync-${req.opId}`,
|
|
248
|
+
timeout: 10000,
|
|
249
|
+
resolve: async (req, context) => {
|
|
250
|
+
const queue = context.facts.syncQueue as SyncQueueEntry[];
|
|
251
|
+
const entry = queue.find((e) => e.opId === req.opId);
|
|
252
|
+
if (!entry) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
context.facts.syncingOpId = req.opId;
|
|
257
|
+
addLogEntry(
|
|
258
|
+
context.facts,
|
|
259
|
+
"syncing",
|
|
260
|
+
`Syncing ${entry.op} for item ${entry.itemId}...`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const serverDelay = context.facts.serverDelay as number;
|
|
264
|
+
const failRate = context.facts.failRate as number;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await mockServerSync(entry.op, entry.itemId, serverDelay, failRate);
|
|
268
|
+
|
|
269
|
+
addLogEntry(
|
|
270
|
+
context.facts,
|
|
271
|
+
"success",
|
|
272
|
+
`${entry.op} item ${entry.itemId} synced`,
|
|
273
|
+
);
|
|
274
|
+
context.facts.toastMessage = `${entry.op} synced successfully`;
|
|
275
|
+
context.facts.toastType = "success";
|
|
276
|
+
} catch {
|
|
277
|
+
context.facts.items = entry.undoItems;
|
|
278
|
+
addLogEntry(
|
|
279
|
+
context.facts,
|
|
280
|
+
"rollback",
|
|
281
|
+
`Failed to ${entry.op} item ${entry.itemId} — rolled back`,
|
|
282
|
+
);
|
|
283
|
+
context.facts.toastMessage = `Failed to ${entry.op} — rolled back`;
|
|
284
|
+
context.facts.toastType = "error";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Remove entry from queue
|
|
288
|
+
const currentQueue = context.facts.syncQueue as SyncQueueEntry[];
|
|
289
|
+
context.facts.syncQueue = currentQueue.filter(
|
|
290
|
+
(e) => e.opId !== req.opId,
|
|
291
|
+
);
|
|
292
|
+
context.facts.syncingOpId = "";
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// Effects
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
effects: {
|
|
302
|
+
logSyncChange: {
|
|
303
|
+
deps: ["syncingOpId"],
|
|
304
|
+
run: (facts, prev) => {
|
|
305
|
+
if (prev) {
|
|
306
|
+
const prevId = prev.syncingOpId as string;
|
|
307
|
+
const currId = facts.syncingOpId as string;
|
|
308
|
+
if (prevId === "" && currId !== "") {
|
|
309
|
+
addLogEntry(facts, "status", `Sync started: op ${currId}`);
|
|
310
|
+
} else if (prevId !== "" && currId === "") {
|
|
311
|
+
addLogEntry(facts, "status", `Sync completed: op ${prevId}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|