mnfst 0.5.164 → 0.5.165

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.
@@ -0,0 +1,292 @@
1
+ /* Manifest Appwrite Presences ($presence)
2
+ /* By Andrew Matlock under MIT license
3
+ /* https://github.com/andrewmatlock/Manifest
4
+ /*
5
+ /* Reactive user status/roster over the native Appwrite Presences API
6
+ /* Requires Alpine JS (alpinejs.dev) to operate
7
+ */
8
+
9
+ (function () {
10
+ 'use strict';
11
+
12
+ /* State */
13
+
14
+ const ctx = {
15
+ client: null,
16
+ presences: null,
17
+ realtime: null,
18
+ sub: null,
19
+ heartbeat: null,
20
+ started: false,
21
+ status: 'online',
22
+ metadata: null,
23
+ auto: true, // focus/blur auto-flip
24
+ heartbeatMs: 25000, // TTL refresh cadence
25
+ onFocus: null,
26
+ onBlur: null
27
+ };
28
+
29
+ /* Config */
30
+
31
+ // Resolve ${VAR} against window.env
32
+ function resolveEnv(v) {
33
+ if (typeof v !== 'string') return v;
34
+ return v.replace(/\$\{([^}]+)\}/g, (m, name) => {
35
+ if (typeof window !== 'undefined' && window.env && window.env[name] != null) return window.env[name];
36
+ return null;
37
+ });
38
+ }
39
+
40
+ async function getConfig() {
41
+ try {
42
+ const manifest = await window.ManifestDataConfig?.ensureManifest?.();
43
+ const aw = manifest?.appwrite;
44
+ if (!aw) return null;
45
+ const endpoint = resolveEnv(aw.endpoint);
46
+ const projectId = resolveEnv(aw.projectId);
47
+ if (!endpoint || !projectId) return null;
48
+ let devKey = aw.devKey ? resolveEnv(aw.devKey) : null;
49
+ if (devKey && /\$\{/.test(devKey)) devKey = null;
50
+ return { endpoint, projectId, devKey };
51
+ } catch (e) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function store() {
57
+ return (typeof Alpine !== 'undefined') ? Alpine.store('presence') : null;
58
+ }
59
+
60
+ function authUser() {
61
+ const s = (typeof Alpine !== 'undefined') ? Alpine.store('auth') : null;
62
+ return s && s.user ? s.user : null;
63
+ }
64
+
65
+ // Active team id (read-scoping)
66
+ function currentTeamId() {
67
+ const s = (typeof Alpine !== 'undefined') ? Alpine.store('auth') : null;
68
+ if (!s) return null;
69
+ if (s.currentTeam) return typeof s.currentTeam === 'string' ? s.currentTeam : (s.currentTeam.$id || s.currentTeam.id || null);
70
+ if (Array.isArray(s.teams) && s.teams.length) return s.teams[0].$id || s.teams[0].id || null;
71
+ return null;
72
+ }
73
+
74
+ // Consistent per-user color
75
+ function userColor(id) {
76
+ if (!id) return '#666';
77
+ let hash = 0;
78
+ for (let i = 0; i < id.length; i++) hash = id.charCodeAt(i) + ((hash << 5) - hash);
79
+ return `hsl(${Math.abs(hash) % 360}, 70%, 50%)`;
80
+ }
81
+
82
+ // Appwrite client + services — authed via the session cookie, not the dev key (which is the guests principal for user ops)
83
+ async function ensureServices() {
84
+ if (ctx.presences && ctx.realtime) return true;
85
+ const A = window.Appwrite;
86
+ if (!A || !A.Presences || !A.Realtime) return false;
87
+ const cfg = await getConfig();
88
+ if (!cfg) return false;
89
+ const client = new A.Client().setEndpoint(cfg.endpoint).setProject(cfg.projectId);
90
+ ctx.client = client;
91
+ ctx.presences = new A.Presences(client);
92
+ ctx.realtime = new A.Realtime(client);
93
+ return true;
94
+ }
95
+
96
+ /* Records */
97
+
98
+ function normalize(rec) {
99
+ if (!rec) return null;
100
+ const userId = rec.userId || rec.$userId || rec.presenceId || rec.$id;
101
+ if (!userId) return null;
102
+ let metadata = rec.metadata;
103
+ if (typeof metadata === 'string') { try { metadata = JSON.parse(metadata); } catch (e) { } }
104
+ const updatedAt = rec.$updatedAt || rec.$createdAt || null;
105
+ return {
106
+ userId,
107
+ status: rec.status || null,
108
+ metadata: metadata || null,
109
+ lastSeen: updatedAt ? Date.parse(updatedAt) : Date.now(),
110
+ expiresAt: rec.expiresAt || rec.$expiresAt || null,
111
+ color: userColor(userId)
112
+ };
113
+ }
114
+
115
+ // Reactive map updates (reassign for Alpine tracking)
116
+ function putRecord(rec) {
117
+ const s = store(); if (!s || !rec) return;
118
+ s.records = { ...s.records, [rec.userId]: rec };
119
+ }
120
+ function removeRecord(userId) {
121
+ const s = store(); if (!s || !userId) return;
122
+ if (!s.records[userId]) return;
123
+ const next = { ...s.records }; delete next[userId]; s.records = next;
124
+ }
125
+
126
+ async function hydrate() {
127
+ const s = store();
128
+ try {
129
+ const res = await ctx.presences.list();
130
+ const rows = res?.presences || res?.rows || res?.documents || [];
131
+ const map = {};
132
+ rows.forEach(r => { const n = normalize(r); if (n) map[n.userId] = n; });
133
+ if (s) s.records = map;
134
+ } catch (e) {
135
+ if (s) s.error = e?.message || String(e);
136
+ }
137
+ }
138
+
139
+ function subscribe() {
140
+ const A = window.Appwrite;
141
+ try {
142
+ const handle = ctx.realtime.subscribe(A.Channel.presences(), (response) => {
143
+ if (!response) return;
144
+ const events = Array.isArray(response.events) ? response.events : (response.events ? [response.events] : []);
145
+ const rec = normalize(response.payload || response);
146
+ if (!rec) return;
147
+ // Ignore echoes of our own presence (tracked locally; delayed echoes would clobber newer status)
148
+ const meId = authUser()?.$id;
149
+ if (meId && rec.userId === meId) return;
150
+ if (events.some(e => typeof e === 'string' && e.includes('delete'))) removeRecord(rec.userId);
151
+ else putRecord(rec);
152
+ });
153
+ // Normalise the unsubscribe handle
154
+ if (handle && typeof handle.then === 'function') {
155
+ handle.then(r => { ctx.sub = typeof r === 'function' ? r : (r && r.close ? () => r.close() : null); });
156
+ ctx.sub = () => { handle.then(r => { if (typeof r === 'function') r(); else if (r && r.close) r.close(); }); };
157
+ } else if (typeof handle === 'function') {
158
+ ctx.sub = handle;
159
+ } else if (handle && typeof handle.close === 'function') {
160
+ ctx.sub = () => handle.close();
161
+ }
162
+ } catch (e) {
163
+ const s = store(); if (s) s.error = e?.message || String(e);
164
+ }
165
+ }
166
+
167
+ /* Operations */
168
+
169
+ async function set(status, metadata) {
170
+ const A = window.Appwrite;
171
+ const user = authUser();
172
+ const s = store();
173
+ if (!user) { if (s) s.error = 'sign in required'; return false; }
174
+ if (!(await ensureServices())) { if (s) s.error = 'Appwrite Presences unavailable'; return false; }
175
+ ctx.status = status != null ? status : ctx.status;
176
+ ctx.metadata = metadata !== undefined ? metadata : ctx.metadata;
177
+
178
+ // Read-scope + owner write (owner update/delete required, else updates 401)
179
+ const teamId = currentTeamId();
180
+ const permissions = [
181
+ teamId ? A.Permission.read(A.Role.team(teamId)) : A.Permission.read(A.Role.users()),
182
+ A.Permission.update(A.Role.user(user.$id)),
183
+ A.Permission.delete(A.Role.user(user.$id))
184
+ ];
185
+
186
+ // name/color in metadata for rosters (name null when unknown — display label is the author's)
187
+ const meta = Object.assign(
188
+ { name: user.name || user.email || null, color: userColor(user.$id) },
189
+ ctx.metadata || {}
190
+ );
191
+
192
+ // Optimistic local update
193
+ putRecord(normalize({ userId: user.$id, status: ctx.status, metadata: meta, $updatedAt: new Date().toISOString() }));
194
+ if (s) s.me = ctx.status;
195
+ try {
196
+ await ctx.presences.upsert({ presenceId: user.$id, status: ctx.status, metadata: meta, permissions });
197
+ if (s) s.error = null;
198
+ return true;
199
+ } catch (e) {
200
+ if (s) s.error = e?.message || String(e);
201
+ return false;
202
+ }
203
+ }
204
+
205
+ async function clear() {
206
+ const user = authUser();
207
+ if (!user || !ctx.presences) return;
208
+ try { await ctx.presences.delete({ presenceId: user.$id }); } catch (e) { }
209
+ removeRecord(user.$id);
210
+ const s = store(); if (s) s.me = null;
211
+ }
212
+
213
+ function of(userId) {
214
+ const s = store();
215
+ return (s && s.records ? s.records[userId] : null) || null;
216
+ }
217
+
218
+ function all() {
219
+ const s = store();
220
+ return s && s.records ? Object.values(s.records) : [];
221
+ }
222
+
223
+ async function start(options) {
224
+ const opts = options || {};
225
+ if (typeof opts.auto === 'boolean') ctx.auto = opts.auto;
226
+ if (typeof opts.heartbeatMs === 'number') ctx.heartbeatMs = opts.heartbeatMs;
227
+ if (typeof opts.status === 'string') ctx.status = opts.status;
228
+
229
+ const s = store();
230
+ if (!authUser()) { if (s) s.error = 'sign in required'; return false; }
231
+ if (!(await ensureServices())) { if (s) s.error = 'Appwrite Presences unavailable'; return false; }
232
+ if (ctx.started) return true;
233
+ ctx.started = true;
234
+
235
+ await hydrate();
236
+ subscribe();
237
+ await set(ctx.status, ctx.metadata);
238
+
239
+ // Heartbeat (refresh TTL)
240
+ ctx.heartbeat = setInterval(() => { if (authUser()) set(ctx.status, ctx.metadata); }, ctx.heartbeatMs);
241
+
242
+ // Auto focus/blur flip
243
+ if (ctx.auto) {
244
+ ctx.onBlur = () => set('away');
245
+ ctx.onFocus = () => set('online');
246
+ window.addEventListener('blur', ctx.onBlur);
247
+ window.addEventListener('focus', ctx.onFocus);
248
+ }
249
+
250
+ if (s) { s.ready = true; s.error = null; }
251
+ return true;
252
+ }
253
+
254
+ function stop() {
255
+ if (ctx.heartbeat) { clearInterval(ctx.heartbeat); ctx.heartbeat = null; }
256
+ if (ctx.onBlur) { window.removeEventListener('blur', ctx.onBlur); ctx.onBlur = null; }
257
+ if (ctx.onFocus) { window.removeEventListener('focus', ctx.onFocus); ctx.onFocus = null; }
258
+ if (typeof ctx.sub === 'function') { try { ctx.sub(); } catch (e) { } }
259
+ ctx.sub = null;
260
+ ctx.started = false;
261
+ const s = store(); if (s) s.ready = false;
262
+ }
263
+
264
+ /* Registration */
265
+
266
+ document.addEventListener('alpine:init', () => {
267
+ Alpine.store('presence', { records: {}, ready: false, error: null, me: null });
268
+
269
+ // Clear on sign-out
270
+ try {
271
+ Alpine.effect(() => {
272
+ if (!authUser() && ctx.started) { clear(); stop(); }
273
+ });
274
+ } catch (e) { }
275
+
276
+ const api = {
277
+ start, stop, set, clear, of, all,
278
+ get records() { return store()?.records || {}; },
279
+ get list() { return all(); },
280
+ get ready() { return !!store()?.ready; },
281
+ get error() { return store()?.error || null; },
282
+ get me() { return store()?.me || null; }
283
+ };
284
+ Alpine.magic('presence', () => api);
285
+ });
286
+
287
+ // Clear on tab close
288
+ window.addEventListener('beforeunload', () => { try { if (ctx.started) clear(); } catch (e) { } });
289
+
290
+ // Non-Alpine handle
291
+ window.ManifestPresence = { start, stop, set, clear, of, all };
292
+ })();
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-+4T3vdzf2NsDrSpatkIqg7cO2HUnY3tNNwSgfyXCCU0NZ7K9eqX2dzhDJIlochlz",
3
3
  "manifest.appwrite.data.js": "sha384-KFKVo5TQESLJB7wgk+C8Zb/sAl43QAMdioWW3tQRWYq5umfdpGCZ5Hld9y9f6/Np",
4
+ "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
4
5
  "manifest.charts.js": "sha384-wTQlB3hJ9V9lmisCgm8l6YkpJV+nA4+Vci3dI0x1nBtWnS9TFr+eXaJelP7AusyD",
5
6
  "manifest.chat.js": "sha384-HDyzPvNxqUOKVcLC3HNSFd5w83Flgm58LHy1BDLB9ejUsbYHAmr34G4X3FwKXokW",
6
7
  "manifest.code.js": "sha384-G3MNfvQRWWY+gm4SGGrSsgQbDeil9MP+ON2DTk6Rcq9Nsex8woHMDt5EJHrFH0Nj",
@@ -28,5 +29,5 @@
28
29
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
29
30
  "manifest.utilities.js": "sha384-MtzNaVOrgyepjlY5nz38pAJk67yiFPU1H5WoPNfcNbUZTXy+oin1lrtKCIcobEOJ",
30
31
  "manifest.virtual.js": "sha384-c194pXD0Ld48QJ+HPVNibvbn54/ETJRuYhUOWesEBjtOIQcQMIKo0DnCyQMvDlLg",
31
- "manifest.js": "sha384-P69mjYcekCOw+wcQ1ETO26q3WwJHUB94HBHGfT91Tu+ngnUeayZgHk0qL+eHWV5i"
32
+ "manifest.js": "sha384-VshJgT9QAxsLpwNmQZfrxw/EAxoyLIklo0DsmhxLhjVmmT70fLmn5eNRuq98o6zs"
32
33
  }
package/lib/manifest.js CHANGED
@@ -241,13 +241,13 @@
241
241
  const APPWRITE_PLUGINS = [
242
242
  'appwrite-auth',
243
243
  'appwrite-data',
244
- 'appwrite-presence'
244
+ 'appwrite-presences'
245
245
  ];
246
246
 
247
247
  // Plugin dependencies: plugins that require other plugins to be loaded first
248
248
  const PLUGIN_DEPENDENCIES = {
249
249
  'appwrite-data': ['data'],
250
- 'appwrite-presence': ['data', 'appwrite-auth']
250
+ 'appwrite-presences': ['data', 'appwrite-auth']
251
251
  };
252
252
 
253
253
  // Derive default plugin list from manifest (only load data/localization/components when manifest needs them)
@@ -422,7 +422,7 @@
422
422
  plugins.push('appwrite-data');
423
423
  }
424
424
  if (manifest.appwrite?.presence) {
425
- plugins.push('appwrite-presence');
425
+ plugins.push('appwrite-presences');
426
426
  }
427
427
  return plugins;
428
428
  }
@@ -541,7 +541,7 @@
541
541
 
542
542
  const MANIFEST_DEPENDENT_PLUGINS = [
543
543
  'data', 'localization', 'components',
544
- 'appwrite-auth', 'appwrite-data', 'appwrite-presence', 'payments'
544
+ 'appwrite-auth', 'appwrite-data', 'appwrite-presences', 'payments'
545
545
  ];
546
546
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
547
547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.164",
3
+ "version": "0.5.165",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",