@venturewild/workspace 0.6.28 → 0.6.30

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,103 @@
1
+ // PreviewRails — the shared-preview registry client on the rails (bmo-sync).
2
+ //
3
+ // WHY: a `pv-<token>` link must route, through bmo-sync, back to the daemon of
4
+ // the account that started the dev server. bmo-sync needs token→account so its
5
+ // proxy forward path can find the right daemon, plus an optional shared-workspace
6
+ // tag so collaborators can DISCOVER each other's live previews (membership-gated,
7
+ // exactly like session-rails). This thin client publishes/heartbeats/retires the
8
+ // token and lists previews — for the workspace (members) or for this account (the
9
+ // owner's own devices). The local dev PORT never leaves this machine; the Node
10
+ // server maps token→port locally (preview-proxy.mjs LocalPreviewRegistry).
11
+ //
12
+ // degrade-never-throw: every call fails soft (publish/heartbeat/retire → false;
13
+ // lists → []), so the dev-port watcher keeps the LOCAL registry working — and the
14
+ // owner's same-machine localhost fallback keeps working — when the rails are down,
15
+ // the endpoint isn't deployed yet (Part D), or the install has no account.
16
+ // Modeled on session-rails.mjs / canvas-rails.mjs.
17
+
18
+ const DEFAULT_TIMEOUT_MS = 3000;
19
+
20
+ export class PreviewRails {
21
+ constructor({
22
+ bmoSyncUrl,
23
+ accountToken,
24
+ timeoutMs = DEFAULT_TIMEOUT_MS,
25
+ fetchImpl = (...a) => globalThis.fetch(...a),
26
+ } = {}) {
27
+ this.bmoSyncUrl = bmoSyncUrl ? bmoSyncUrl.replace(/\/$/, '') : null;
28
+ this.accountToken = accountToken || null;
29
+ this.timeoutMs = timeoutMs;
30
+ this.fetchImpl = fetchImpl;
31
+ this.capable = Boolean(this.accountToken) && Boolean(this.bmoSyncUrl);
32
+ }
33
+
34
+ async _post(path, body) {
35
+ if (!this.capable) return null;
36
+ const ctrl = new AbortController();
37
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
38
+ if (timer.unref) timer.unref();
39
+ try {
40
+ const r = await this.fetchImpl(`${this.bmoSyncUrl}${path}`, {
41
+ method: 'POST',
42
+ headers: { 'content-type': 'application/json' },
43
+ body: JSON.stringify({ account_token: this.accountToken, ...body }),
44
+ signal: ctrl.signal,
45
+ });
46
+ if (!r || !r.ok) return null;
47
+ return await r.json().catch(() => null);
48
+ } catch {
49
+ return null;
50
+ } finally {
51
+ clearTimeout(timer);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Publish (create-or-touch) a live preview. `slug` is the shared-workspace slug
57
+ * (or null for a personal/unshared preview). The dev port stays local.
58
+ * @returns {Promise<boolean>} true iff the rails accepted it.
59
+ */
60
+ async publish({ token, slug = null, label = null } = {}) {
61
+ const resp = await this._post('/api/preview/publish', { token, slug, label });
62
+ return Boolean(resp && resp.ok === true);
63
+ }
64
+
65
+ /** Refresh a preview's heartbeat (keeps it from auto-expiring). */
66
+ async heartbeat(token) {
67
+ const resp = await this._post('/api/preview/heartbeat', { token });
68
+ return Boolean(resp && resp.ok === true);
69
+ }
70
+
71
+ /** Retire a preview now (dev server stopped). Idempotent. */
72
+ async retire(token) {
73
+ const resp = await this._post('/api/preview/retire', { token });
74
+ return Boolean(resp && resp.ok === true);
75
+ }
76
+
77
+ /**
78
+ * List a shared workspace's live previews (membership-gated server-side).
79
+ * @returns {Promise<{ok:boolean, previews:Array}>} each:
80
+ * {token,url,owner_account_id,owner_email,label,created_at,updated_at}
81
+ */
82
+ async listWorkspace(slug) {
83
+ const resp = await this._post('/api/preview/list', { slug });
84
+ if (!resp || resp.ok !== true) return { ok: false, previews: [] };
85
+ return { ok: true, previews: Array.isArray(resp.previews) ? resp.previews : [] };
86
+ }
87
+
88
+ /** List THIS account's own live previews (the owner's other devices). */
89
+ async listMine() {
90
+ const resp = await this._post('/api/preview/list-mine', {});
91
+ if (!resp || resp.ok !== true) return { ok: false, previews: [] };
92
+ return { ok: true, previews: Array.isArray(resp.previews) ? resp.previews : [] };
93
+ }
94
+ }
95
+
96
+ /** Build the rails client from server config (inert when not logged in). */
97
+ export function createPreviewRails(config, fetchImpl) {
98
+ return new PreviewRails({
99
+ bmoSyncUrl: config?.bmoSyncServerUrl,
100
+ accountToken: config?.accountToken,
101
+ fetchImpl,
102
+ });
103
+ }