@autumnsgrove/groveengine 0.9.90 → 0.9.91

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.
Files changed (31) hide show
  1. package/dist/curios/timeline/Timeline.svelte +21 -2
  2. package/dist/durable-objects/TenantDO.d.ts +7 -106
  3. package/dist/durable-objects/TenantDO.js +7 -423
  4. package/dist/durable-objects/index.d.ts +0 -1
  5. package/dist/durable-objects/index.js +0 -2
  6. package/dist/ui/vineyard/AuthButton.svelte +127 -0
  7. package/dist/ui/vineyard/AuthButton.svelte.d.ts +6 -0
  8. package/dist/ui/vineyard/CodeExample.svelte +186 -0
  9. package/dist/ui/vineyard/CodeExample.svelte.d.ts +8 -0
  10. package/dist/ui/vineyard/DemoContainer.svelte +139 -0
  11. package/dist/ui/vineyard/DemoContainer.svelte.d.ts +8 -0
  12. package/dist/ui/vineyard/FeatureCard.svelte +127 -0
  13. package/dist/ui/vineyard/FeatureCard.svelte.d.ts +8 -0
  14. package/dist/ui/vineyard/RoadmapSection.svelte +238 -0
  15. package/dist/ui/vineyard/RoadmapSection.svelte.d.ts +4 -0
  16. package/dist/ui/vineyard/StatusBadge.svelte +88 -0
  17. package/dist/ui/vineyard/StatusBadge.svelte.d.ts +4 -0
  18. package/dist/ui/vineyard/TierGate.svelte +172 -0
  19. package/dist/ui/vineyard/TierGate.svelte.d.ts +10 -0
  20. package/dist/ui/vineyard/UserMenu.svelte +289 -0
  21. package/dist/ui/vineyard/UserMenu.svelte.d.ts +6 -0
  22. package/dist/ui/vineyard/VineyardLayout.svelte +254 -0
  23. package/dist/ui/vineyard/VineyardLayout.svelte.d.ts +8 -0
  24. package/dist/ui/vineyard/auth.d.ts +81 -0
  25. package/dist/ui/vineyard/auth.js +134 -0
  26. package/dist/ui/vineyard/index.d.ts +14 -3
  27. package/dist/ui/vineyard/index.js +20 -3
  28. package/dist/ui/vineyard/types.d.ts +147 -0
  29. package/dist/ui/vineyard/types.js +5 -0
  30. package/package.json +1 -2
  31. package/static/favicon.svg +27 -16
@@ -441,11 +441,17 @@
441
441
  font-weight: 600;
442
442
  color: var(--color-foreground, #333);
443
443
  }
444
+ :global(.dark) .date-full {
445
+ color: var(--bark, #f5f2ea);
446
+ }
444
447
  .date-short {
445
448
  display: none;
446
449
  font-weight: 600;
447
450
  color: var(--color-foreground, #333);
448
451
  }
452
+ :global(.dark) .date-short {
453
+ color: var(--bark, #f5f2ea);
454
+ }
449
455
  :global(.today-badge) {
450
456
  background: var(--grove-500, #4ade80);
451
457
  color: white;
@@ -518,13 +524,16 @@
518
524
  font-size: 0.95rem;
519
525
  }
520
526
  :global(.dark) .rest-message {
521
- color: #777;
527
+ color: var(--bark-600, #b69575);
522
528
  }
523
529
  .brief-summary {
524
530
  margin: 0 0 0.75rem;
525
531
  color: var(--color-foreground, #333);
526
532
  line-height: 1.5;
527
533
  }
534
+ :global(.dark) .brief-summary {
535
+ color: var(--bark, #f5f2ea);
536
+ }
528
537
  .meta-info {
529
538
  display: flex;
530
539
  flex-wrap: wrap;
@@ -533,6 +542,9 @@
533
542
  color: var(--color-foreground, #333);
534
543
  margin-bottom: 0.75rem;
535
544
  }
545
+ :global(.dark) .meta-info {
546
+ color: var(--bark-700, #ccb59c);
547
+ }
536
548
  .repos, .changes {
537
549
  display: flex;
538
550
  align-items: center;
@@ -606,6 +618,7 @@
606
618
  }
607
619
  :global(.dark) .detailed-timeline {
608
620
  background: var(--cream-300, #2a2a2a);
621
+ color: var(--bark, #f5f2ea);
609
622
  }
610
623
  .markdown-content :global(h2) {
611
624
  font-size: 1.1rem;
@@ -624,6 +637,9 @@
624
637
  margin: 1rem 0 0.5rem;
625
638
  font-weight: 600;
626
639
  }
640
+ :global(.dark) .markdown-content :global(h3) {
641
+ color: var(--bark, #f5f2ea);
642
+ }
627
643
  .markdown-content :global(h3 a) {
628
644
  color: #2c5f2d;
629
645
  text-decoration: none;
@@ -646,6 +662,9 @@
646
662
  margin-bottom: 0.25rem;
647
663
  color: var(--color-foreground, #333);
648
664
  }
665
+ :global(.dark) .markdown-content :global(li) {
666
+ color: var(--bark, #f5f2ea);
667
+ }
649
668
  .markdown-content :global(code) {
650
669
  background: var(--color-border-strong, #e0e0e0);
651
670
  padding: 0.15rem 0.35rem;
@@ -680,7 +699,7 @@
680
699
  }
681
700
  :global(.dark) .timeline-footer {
682
701
  border-top-color: var(--color-border-strong, #444);
683
- color: #666;
702
+ color: var(--bark-700, #ccb59c);
684
703
  }
685
704
 
686
705
  /* Mobile Responsiveness */
@@ -1,16 +1,16 @@
1
1
  /**
2
- * TenantDO - Per-Tenant Durable Object
2
+ * TenantDO Types
3
3
  *
4
- * Provides:
5
- * - Config caching (eliminates D1 reads on every request)
6
- * - Cross-device draft sync
7
- * - Analytics event buffering
4
+ * Type definitions for the TenantDO Durable Object.
5
+ * The actual class implementation lives in packages/durable-objects/src/TenantDO.ts
6
+ * and is deployed as a separate Cloudflare Worker (grove-durable-objects).
8
7
  *
9
- * ID Pattern: tenant:{subdomain}
8
+ * The engine references this DO via service binding (script_name in wrangler.toml),
9
+ * so only types are needed here.
10
10
  *
11
11
  * Part of the Loom pattern - Grove's coordination layer.
12
12
  */
13
- import { type PaidTierKey } from "../config/tiers.js";
13
+ import type { PaidTierKey } from "../config/tiers.js";
14
14
  export interface TenantConfig {
15
15
  id: string;
16
16
  subdomain: string;
@@ -42,102 +42,3 @@ export interface AnalyticsEvent {
42
42
  data?: Record<string, unknown>;
43
43
  timestamp: number;
44
44
  }
45
- export declare class TenantDO implements DurableObject {
46
- private state;
47
- private env;
48
- private config;
49
- private configLoadedAt;
50
- private analyticsBuffer;
51
- private initialized;
52
- private refreshPromise;
53
- private subdomain;
54
- constructor(state: DurableObjectState, env: Env);
55
- /**
56
- * Initialize SQLite tables in DO storage
57
- */
58
- private initializeStorage;
59
- /**
60
- * Main request handler - routes to appropriate method
61
- */
62
- fetch(request: Request): Promise<Response>;
63
- /**
64
- * Get tenant config (cached in memory, refreshed from D1 if stale)
65
- *
66
- * Uses a promise lock pattern to prevent race conditions where multiple
67
- * concurrent requests all trigger D1 queries simultaneously.
68
- */
69
- private handleGetConfig;
70
- /**
71
- * Refresh config from DO storage or D1
72
- *
73
- * This method caches the tenant ID in the config to avoid repeated D1 lookups
74
- * in hooks.server.ts. The ID is stored alongside other config data.
75
- */
76
- private refreshConfig;
77
- /**
78
- * Update tenant config
79
- *
80
- * Uses the same promise lock pattern as handleGetConfig to prevent
81
- * race conditions where concurrent updates could clobber each other.
82
- */
83
- private handleUpdateConfig;
84
- /**
85
- * Get tier limits from centralized tiers.ts config
86
- *
87
- * This ensures TenantDO limits match the single source of truth,
88
- * preventing drift between DO cached limits and actual tier configuration.
89
- */
90
- private getTierLimits;
91
- /**
92
- * List all drafts for this tenant
93
- */
94
- private handleListDrafts;
95
- /**
96
- * Get a specific draft
97
- */
98
- private handleGetDraft;
99
- /**
100
- * Save or update a draft
101
- */
102
- private handleSaveDraft;
103
- /**
104
- * Delete a draft
105
- */
106
- private handleDeleteDraft;
107
- /**
108
- * Record an analytics event (buffered)
109
- */
110
- private handleRecordEvent;
111
- /**
112
- * Alarm handler - flush analytics buffer
113
- */
114
- alarm(): Promise<void>;
115
- /**
116
- * Flush analytics buffer to D1
117
- */
118
- private flushAnalytics;
119
- /**
120
- * Get the subdomain for this tenant DO
121
- *
122
- * Priority:
123
- * 1. Subdomain passed via X-Tenant-Subdomain header (set on every request)
124
- * 2. Cached subdomain from previous request
125
- * 3. Subdomain from cached config
126
- *
127
- * Returns null if subdomain is not yet known (shouldn't happen in practice
128
- * since hooks.server.ts always sends the header).
129
- */
130
- private getSubdomain;
131
- /**
132
- * Get the tenant ID (UUID) for this tenant
133
- *
134
- * Returns the cached ID from config, or null if not yet loaded.
135
- * The ID is fetched from D1 on first config load and cached.
136
- */
137
- private getTenantId;
138
- }
139
- interface Env {
140
- DB: D1Database;
141
- CACHE_KV: KVNamespace;
142
- }
143
- export {};
@@ -1,429 +1,13 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
1
  /**
3
- * TenantDO - Per-Tenant Durable Object
2
+ * TenantDO Types
4
3
  *
5
- * Provides:
6
- * - Config caching (eliminates D1 reads on every request)
7
- * - Cross-device draft sync
8
- * - Analytics event buffering
4
+ * Type definitions for the TenantDO Durable Object.
5
+ * The actual class implementation lives in packages/durable-objects/src/TenantDO.ts
6
+ * and is deployed as a separate Cloudflare Worker (grove-durable-objects).
9
7
  *
10
- * ID Pattern: tenant:{subdomain}
8
+ * The engine references this DO via service binding (script_name in wrangler.toml),
9
+ * so only types are needed here.
11
10
  *
12
11
  * Part of the Loom pattern - Grove's coordination layer.
13
12
  */
14
- import { TIERS } from "../config/tiers.js";
15
- // ============================================================================
16
- // Constants
17
- // ============================================================================
18
- /**
19
- * Maximum analytics events to buffer before forcing a flush.
20
- * Prevents memory leak if alarm mechanism fails.
21
- */
22
- const MAX_ANALYTICS_BUFFER = 1000;
23
- // ============================================================================
24
- // TenantDO Class
25
- // ============================================================================
26
- export class TenantDO {
27
- state;
28
- env;
29
- // In-memory caches (faster than storage for hot data)
30
- config = null;
31
- configLoadedAt = 0;
32
- analyticsBuffer = [];
33
- initialized = false;
34
- // Race condition prevention: only one refresh at a time
35
- refreshPromise = null;
36
- // Subdomain extracted from DO name (set on first request)
37
- subdomain = null;
38
- constructor(state, env) {
39
- this.state = state;
40
- this.env = env;
41
- // Block concurrent requests while initializing storage
42
- this.state.blockConcurrencyWhile(async () => {
43
- await this.initializeStorage();
44
- });
45
- }
46
- /**
47
- * Initialize SQLite tables in DO storage
48
- */
49
- async initializeStorage() {
50
- if (this.initialized)
51
- return;
52
- await this.state.storage.sql.exec(`
53
- CREATE TABLE IF NOT EXISTS config (
54
- key TEXT PRIMARY KEY,
55
- value TEXT NOT NULL,
56
- updated_at INTEGER NOT NULL
57
- );
58
-
59
- CREATE TABLE IF NOT EXISTS drafts (
60
- slug TEXT PRIMARY KEY,
61
- content TEXT NOT NULL,
62
- metadata TEXT NOT NULL,
63
- last_saved INTEGER NOT NULL,
64
- device_id TEXT NOT NULL
65
- );
66
-
67
- CREATE TABLE IF NOT EXISTS analytics_buffer (
68
- id INTEGER PRIMARY KEY AUTOINCREMENT,
69
- event_type TEXT NOT NULL,
70
- event_data TEXT,
71
- timestamp INTEGER NOT NULL
72
- );
73
- `);
74
- this.initialized = true;
75
- }
76
- /**
77
- * Main request handler - routes to appropriate method
78
- */
79
- async fetch(request) {
80
- const url = new URL(request.url);
81
- const path = url.pathname;
82
- // Extract subdomain from request header (set by hooks.server.ts)
83
- // This is passed on every request to ensure we always know our identity
84
- const subdomainHeader = request.headers.get("X-Tenant-Subdomain");
85
- if (subdomainHeader && !this.subdomain) {
86
- this.subdomain = subdomainHeader;
87
- }
88
- try {
89
- // Config endpoints
90
- if (path === "/config" && request.method === "GET") {
91
- return this.handleGetConfig();
92
- }
93
- if (path === "/config" && request.method === "PUT") {
94
- return this.handleUpdateConfig(request);
95
- }
96
- // Draft endpoints
97
- if (path === "/drafts" && request.method === "GET") {
98
- return this.handleListDrafts();
99
- }
100
- if (path.startsWith("/drafts/") && request.method === "GET") {
101
- const slug = path.split("/").pop();
102
- return this.handleGetDraft(slug);
103
- }
104
- if (path.startsWith("/drafts/") && request.method === "PUT") {
105
- const slug = path.split("/").pop();
106
- return this.handleSaveDraft(slug, request);
107
- }
108
- if (path.startsWith("/drafts/") && request.method === "DELETE") {
109
- const slug = path.split("/").pop();
110
- return this.handleDeleteDraft(slug);
111
- }
112
- // Analytics endpoint
113
- if (path === "/analytics" && request.method === "POST") {
114
- return this.handleRecordEvent(request);
115
- }
116
- return new Response("Not found", { status: 404 });
117
- }
118
- catch (err) {
119
- console.error("[TenantDO] Error:", err);
120
- return new Response(JSON.stringify({
121
- error: err instanceof Error ? err.message : "Internal error",
122
- }), { status: 500, headers: { "Content-Type": "application/json" } });
123
- }
124
- }
125
- // ============================================================================
126
- // Config Methods
127
- // ============================================================================
128
- /**
129
- * Get tenant config (cached in memory, refreshed from D1 if stale)
130
- *
131
- * Uses a promise lock pattern to prevent race conditions where multiple
132
- * concurrent requests all trigger D1 queries simultaneously.
133
- */
134
- async handleGetConfig() {
135
- const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
136
- // Refresh if stale or not loaded (with race condition prevention)
137
- if (!this.config || Date.now() - this.configLoadedAt > STALE_THRESHOLD_MS) {
138
- // Only start one refresh at a time - other requests wait for it
139
- if (!this.refreshPromise) {
140
- this.refreshPromise = this.refreshConfig().finally(() => {
141
- this.refreshPromise = null;
142
- });
143
- }
144
- await this.refreshPromise;
145
- }
146
- if (!this.config) {
147
- return new Response("Tenant not found", { status: 404 });
148
- }
149
- return Response.json(this.config);
150
- }
151
- /**
152
- * Refresh config from DO storage or D1
153
- *
154
- * This method caches the tenant ID in the config to avoid repeated D1 lookups
155
- * in hooks.server.ts. The ID is stored alongside other config data.
156
- */
157
- async refreshConfig() {
158
- // Try DO storage first (fastest)
159
- const stored = this.state.storage.sql
160
- .exec("SELECT value FROM config WHERE key = 'tenant_config'")
161
- .one();
162
- if (stored?.value) {
163
- try {
164
- this.config = JSON.parse(stored.value);
165
- // Also set subdomain from cached config if we don't have it
166
- if (this.config?.subdomain && !this.subdomain) {
167
- this.subdomain = this.config.subdomain;
168
- }
169
- this.configLoadedAt = Date.now();
170
- return;
171
- }
172
- catch (err) {
173
- // Corrupted cache - clear it and fall through to D1
174
- console.warn("[TenantDO] Failed to parse cached config, clearing:", err instanceof Error ? err.message : err);
175
- await this.state.storage.sql.exec("DELETE FROM config WHERE key = 'tenant_config'");
176
- }
177
- }
178
- // Fall back to D1 - need subdomain to query
179
- const subdomain = this.getSubdomain();
180
- if (!subdomain) {
181
- console.error("[TenantDO] Cannot refresh config: no subdomain available");
182
- return;
183
- }
184
- // Query D1 for full tenant data including ID
185
- const row = await this.env.DB.prepare(`
186
- SELECT id, subdomain, name as displayName, theme, plan as tier, owner_id as ownerId
187
- FROM tenants
188
- WHERE subdomain = ? AND active = 1
189
- `)
190
- .bind(subdomain)
191
- .first();
192
- if (row) {
193
- // Build config with tier limits from centralized tiers.ts
194
- const tier = row.tier || "seedling";
195
- // Safely parse theme JSON (corrupted data shouldn't crash the DO)
196
- let theme = null;
197
- if (row.theme) {
198
- try {
199
- theme = JSON.parse(row.theme);
200
- }
201
- catch (err) {
202
- console.warn(`[TenantDO] Failed to parse theme JSON for ${this.subdomain}:`, err instanceof Error ? err.message : err);
203
- }
204
- }
205
- this.config = {
206
- id: row.id, // Include tenant ID to eliminate hooks.server.ts D1 query
207
- subdomain: row.subdomain,
208
- displayName: row.displayName,
209
- theme,
210
- tier,
211
- ownerId: row.ownerId,
212
- limits: this.getTierLimits(tier),
213
- };
214
- // Cache in DO storage for next time
215
- await this.state.storage.sql.exec("INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)", "tenant_config", JSON.stringify(this.config), Date.now());
216
- this.configLoadedAt = Date.now();
217
- }
218
- }
219
- /**
220
- * Update tenant config
221
- *
222
- * Uses the same promise lock pattern as handleGetConfig to prevent
223
- * race conditions where concurrent updates could clobber each other.
224
- */
225
- async handleUpdateConfig(request) {
226
- const updates = (await request.json());
227
- // Ensure config is loaded with race condition prevention
228
- if (!this.config) {
229
- if (!this.refreshPromise) {
230
- this.refreshPromise = this.refreshConfig().finally(() => {
231
- this.refreshPromise = null;
232
- });
233
- }
234
- await this.refreshPromise;
235
- }
236
- if (!this.config) {
237
- return new Response("Tenant not found", { status: 404 });
238
- }
239
- this.config = { ...this.config, ...updates };
240
- // Update DO storage
241
- await this.state.storage.sql.exec("INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)", "tenant_config", JSON.stringify(this.config), Date.now());
242
- // Update D1 (source of truth) using the subdomain
243
- const subdomain = this.getSubdomain();
244
- if (subdomain) {
245
- await this.env.DB.prepare(`
246
- UPDATE tenants
247
- SET name = ?, theme = ?, updated_at = datetime('now')
248
- WHERE subdomain = ?
249
- `)
250
- .bind(this.config.displayName, this.config.theme ? JSON.stringify(this.config.theme) : null, subdomain)
251
- .run();
252
- }
253
- this.configLoadedAt = Date.now();
254
- return Response.json({ success: true });
255
- }
256
- /**
257
- * Get tier limits from centralized tiers.ts config
258
- *
259
- * This ensures TenantDO limits match the single source of truth,
260
- * preventing drift between DO cached limits and actual tier configuration.
261
- */
262
- getTierLimits(tier) {
263
- // Map TenantConfig tier to TierKey (they're compatible)
264
- const tierConfig = TIERS[tier] ?? TIERS.seedling;
265
- return {
266
- // Convert Infinity to -1 for JSON serialization (Infinity isn't valid JSON)
267
- postsPerMonth: tierConfig.limits.posts === Infinity ? -1 : tierConfig.limits.posts,
268
- storageBytes: tierConfig.limits.storage,
269
- // Custom domains based on tier features
270
- customDomains: tierConfig.features.customDomain
271
- ? tier === "evergreen"
272
- ? 10
273
- : tier === "oak"
274
- ? 3
275
- : 1
276
- : 0,
277
- };
278
- }
279
- // ============================================================================
280
- // Draft Methods
281
- // ============================================================================
282
- /**
283
- * List all drafts for this tenant
284
- */
285
- async handleListDrafts() {
286
- const rows = this.state.storage.sql
287
- .exec("SELECT slug, metadata, last_saved, device_id FROM drafts ORDER BY last_saved DESC")
288
- .toArray();
289
- const drafts = rows.map((row) => {
290
- let metadata = { title: "Untitled" };
291
- try {
292
- metadata = JSON.parse(row.metadata);
293
- }
294
- catch (err) {
295
- console.warn(`[TenantDO] Failed to parse draft metadata for ${row.slug}:`, err instanceof Error ? err.message : err);
296
- }
297
- return {
298
- slug: row.slug,
299
- metadata,
300
- lastSaved: row.last_saved,
301
- deviceId: row.device_id,
302
- };
303
- });
304
- return Response.json(drafts);
305
- }
306
- /**
307
- * Get a specific draft
308
- */
309
- async handleGetDraft(slug) {
310
- const row = this.state.storage.sql
311
- .exec("SELECT * FROM drafts WHERE slug = ?", slug)
312
- .one();
313
- if (!row) {
314
- return new Response("Draft not found", { status: 404 });
315
- }
316
- let metadata = { title: "Untitled" };
317
- try {
318
- metadata = JSON.parse(row.metadata);
319
- }
320
- catch (err) {
321
- console.warn(`[TenantDO] Failed to parse draft metadata for ${slug}:`, err instanceof Error ? err.message : err);
322
- }
323
- return Response.json({
324
- slug: row.slug,
325
- content: row.content,
326
- metadata,
327
- lastSaved: row.last_saved,
328
- deviceId: row.device_id,
329
- });
330
- }
331
- /**
332
- * Save or update a draft
333
- */
334
- async handleSaveDraft(slug, request) {
335
- const draft = (await request.json());
336
- const now = Date.now();
337
- await this.state.storage.sql.exec(`
338
- INSERT OR REPLACE INTO drafts (slug, content, metadata, last_saved, device_id)
339
- VALUES (?, ?, ?, ?, ?)
340
- `, slug, draft.content, JSON.stringify(draft.metadata), now, draft.deviceId);
341
- return Response.json({ success: true, lastSaved: now });
342
- }
343
- /**
344
- * Delete a draft
345
- */
346
- async handleDeleteDraft(slug) {
347
- await this.state.storage.sql.exec("DELETE FROM drafts WHERE slug = ?", slug);
348
- return Response.json({ success: true });
349
- }
350
- // ============================================================================
351
- // Analytics Methods
352
- // ============================================================================
353
- /**
354
- * Record an analytics event (buffered)
355
- */
356
- async handleRecordEvent(request) {
357
- const event = (await request.json());
358
- // Add to memory buffer
359
- this.analyticsBuffer.push({
360
- ...event,
361
- timestamp: event.timestamp || Date.now(),
362
- });
363
- // Flush conditions:
364
- // 1. Normal threshold (100): flush and let alarm reschedule
365
- // 2. MAX_ANALYTICS_BUFFER: safety net if alarm mechanism fails
366
- if (this.analyticsBuffer.length >= MAX_ANALYTICS_BUFFER) {
367
- // Force flush - buffer is dangerously large
368
- console.warn(`[TenantDO] Analytics buffer hit max (${MAX_ANALYTICS_BUFFER}), forcing flush`);
369
- await this.flushAnalytics();
370
- }
371
- else if (this.analyticsBuffer.length >= 100) {
372
- // Normal flush threshold
373
- await this.flushAnalytics();
374
- }
375
- else {
376
- // Schedule flush via alarm if not already set
377
- const currentAlarm = await this.state.storage.getAlarm();
378
- if (!currentAlarm) {
379
- await this.state.storage.setAlarm(Date.now() + 60_000); // 1 minute
380
- }
381
- }
382
- return Response.json({ success: true });
383
- }
384
- /**
385
- * Alarm handler - flush analytics buffer
386
- */
387
- async alarm() {
388
- await this.flushAnalytics();
389
- }
390
- /**
391
- * Flush analytics buffer to D1
392
- */
393
- async flushAnalytics() {
394
- if (this.analyticsBuffer.length === 0)
395
- return;
396
- const events = this.analyticsBuffer.splice(0, this.analyticsBuffer.length);
397
- const subdomain = this.getSubdomain() || "unknown";
398
- // For now, just log - analytics table implementation deferred to Rings
399
- console.log(`[TenantDO] Flushing ${events.length} analytics events for ${subdomain}`);
400
- // TODO: When Rings is implemented, batch insert to analytics table
401
- // This will use the AnalyticsDO pattern from the Rings spec
402
- }
403
- // ============================================================================
404
- // Utility Methods
405
- // ============================================================================
406
- /**
407
- * Get the subdomain for this tenant DO
408
- *
409
- * Priority:
410
- * 1. Subdomain passed via X-Tenant-Subdomain header (set on every request)
411
- * 2. Cached subdomain from previous request
412
- * 3. Subdomain from cached config
413
- *
414
- * Returns null if subdomain is not yet known (shouldn't happen in practice
415
- * since hooks.server.ts always sends the header).
416
- */
417
- getSubdomain() {
418
- return this.subdomain || this.config?.subdomain || null;
419
- }
420
- /**
421
- * Get the tenant ID (UUID) for this tenant
422
- *
423
- * Returns the cached ID from config, or null if not yet loaded.
424
- * The ID is fetched from D1 on first config load and cached.
425
- */
426
- getTenantId() {
427
- return this.config?.id || null;
428
- }
429
- }
13
+ export {};
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * Part of the Loom pattern - Grove's coordination layer.
9
9
  */
10
- export { TenantDO } from "./TenantDO.js";
11
10
  export type { TenantConfig, TierLimits, Draft, DraftMetadata, AnalyticsEvent, } from "./TenantDO.js";
12
11
  export { PostMetaDO } from "./PostMetaDO.js";
13
12
  export type { PostMeta, ReactionCounts, ReactionEvent, PresenceInfo, } from "./PostMetaDO.js";
@@ -7,8 +7,6 @@
7
7
  *
8
8
  * Part of the Loom pattern - Grove's coordination layer.
9
9
  */
10
- // TenantDO - Per-tenant config, drafts, analytics
11
- export { TenantDO } from "./TenantDO.js";
12
10
  // PostMetaDO - Per-post reactions, views, presence (hot data)
13
11
  export { PostMetaDO } from "./PostMetaDO.js";
14
12
  // PostContentDO - Per-post content caching (warm data, hibernates)