pyric 0.1.0-alpha.21 → 0.1.0-alpha.22

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.
@@ -121,10 +121,8 @@ export class StorageService {
121
121
  /** Options for {@link getStorageSandbox}. */
122
122
  export interface StorageOptions {
123
123
  /**
124
- * Bucket identifier recorded on uploaded metadata. v1 has a
125
- * single implicit bucket and does not enforce cross-bucket
126
- * isolation — passing different values per call is accepted and
127
- * round-trips in metadata, but the data store is shared.
124
+ * Bucket identifier recorded on uploaded metadata and used to isolate
125
+ * storage persistence across multiple buckets.
128
126
  */
129
127
  bucket?: string;
130
128
  /**
@@ -180,18 +178,44 @@ interface OpenStorageService {
180
178
  /** One open storage service, with its opening configuration, per `Sandbox`. */
181
179
  const OPEN_SERVICES = new WeakMap<Sandbox, OpenStorageService>();
182
180
 
183
- /** One sandbox `FirebaseStorage` handle per `SandboxContext`. */
184
- const SANDBOX_HANDLES = new WeakMap<SandboxContext, FirebaseStorage>();
181
+ /** One scoped `StorageService` per (Sandbox, bucket). */
182
+ const SCOPED_SERVICES = new WeakMap<Sandbox, Map<string, Promise<StorageService>>>();
183
+
184
+ function getOrCreateScopedService(
185
+ sandbox: Sandbox,
186
+ bucket: string,
187
+ rootServicePromise: Promise<StorageService>,
188
+ ): Promise<StorageService> {
189
+ let bucketMap = SCOPED_SERVICES.get(sandbox);
190
+ if (!bucketMap) {
191
+ bucketMap = new Map();
192
+ SCOPED_SERVICES.set(sandbox, bucketMap);
193
+ }
194
+ let existing = bucketMap.get(bucket);
195
+ if (!existing) {
196
+ existing = rootServicePromise.then((rootService) => {
197
+ const backend = rootService.backend.scoped
198
+ ? rootService.backend.scoped(bucket)
199
+ : rootService.backend;
200
+ return new StorageService(backend, rootService.rules, rootService.crossServiceIam);
201
+ });
202
+ bucketMap.set(bucket, existing);
203
+ }
204
+ return existing;
205
+ }
206
+
207
+ /** Sandbox `FirebaseStorage` handles cached per `(SandboxContext, bucket)`. */
208
+ const SANDBOX_HANDLES = new WeakMap<SandboxContext, Map<string, FirebaseStorage>>();
185
209
 
186
210
  /**
187
- * One anonymous handle per bare `Sandbox`. `Sandbox.withAuth(null)`
211
+ * Anonymous handles cached per `(Sandbox, bucket)`. `Sandbox.withAuth(null)`
188
212
  * mints a FRESH `SandboxContext` on every call, so the per-context
189
213
  * cache above would miss for the bare-`Sandbox` convenience path —
190
214
  * `getStorageSandbox(sandbox)` twice returned two different handles
191
215
  * (ST-B3). Caching on the `Sandbox` keeps that path identity-stable,
192
216
  * matching the documented "handle is identity-stable per Sandbox".
193
217
  */
194
- const BARE_SANDBOX_HANDLES = new WeakMap<Sandbox, FirebaseStorage>();
218
+ const BARE_SANDBOX_HANDLES = new WeakMap<Sandbox, Map<string, FirebaseStorage>>();
195
219
 
196
220
  /**
197
221
  * Reject a late option that differs from the one the service is already
@@ -285,6 +309,7 @@ function ensureService(
285
309
  const crossServiceIam = options.crossServiceIam ?? 'granted';
286
310
  const servicePromise = openStorageBackend(
287
311
  options.dbName ?? storageDbName(options.projectId),
312
+ options.bucket ?? DEFAULT_BUCKET,
288
313
  ).then(
289
314
  (backend) => new StorageService(backend, rules, crossServiceIam),
290
315
  );
@@ -336,24 +361,22 @@ export function getStorageSandbox(
336
361
  // Open (or fetch) the ONE per-sandbox service FIRST — before any handle
337
362
  // cache fast-path — so a late, differing `rules` option throws instead of
338
363
  // being silently discarded (see ensureService).
339
- const servicePromise = ensureService(sandbox, options, 'getStorageSandbox');
364
+ const rootServicePromise = ensureService(sandbox, options, 'getStorageSandbox');
365
+ const bucket = options.bucket ?? DEFAULT_BUCKET;
340
366
 
341
367
  // Fast path: a repeat bare-`Sandbox` call returns the cached
342
- // anonymous handle (the per-context cache below can't catch it
343
- // because `withAuth(null)` mints a fresh context each time). Like
344
- // the per-context path, bucket/dbName options are honored only on
345
- // the FIRST call per Sandbox.
368
+ // anonymous handle for the requested bucket.
346
369
  if (fromBareSandbox) {
347
- const cachedBare = BARE_SANDBOX_HANDLES.get(sandbox);
370
+ const cachedBare = BARE_SANDBOX_HANDLES.get(sandbox)?.get(bucket);
348
371
  if (cachedBare) return cachedBare;
349
372
  }
350
373
 
351
374
  const ctx = isContext(target) ? target : target.withAuth(null);
352
- const bucket = options.bucket ?? DEFAULT_BUCKET;
353
-
354
- const cached = SANDBOX_HANDLES.get(ctx);
375
+ const cached = SANDBOX_HANDLES.get(ctx)?.get(bucket);
355
376
  if (cached) return cached;
356
377
 
378
+ const servicePromise = getOrCreateScopedService(sandbox, bucket, rootServicePromise);
379
+
357
380
  const sandboxTarget: SandboxTarget = {
358
381
  kind: 'sandbox',
359
382
  sandbox,
@@ -363,15 +386,29 @@ export function getStorageSandbox(
363
386
  servicePromise,
364
387
  };
365
388
  const handle: FirebaseStorage = Object.freeze({ [TARGET_SYMBOL]: sandboxTarget });
366
- SANDBOX_HANDLES.set(ctx, handle);
367
- if (fromBareSandbox) BARE_SANDBOX_HANDLES.set(sandbox, handle);
389
+
390
+ let ctxMap = SANDBOX_HANDLES.get(ctx);
391
+ if (!ctxMap) {
392
+ ctxMap = new Map();
393
+ SANDBOX_HANDLES.set(ctx, ctxMap);
394
+ }
395
+ ctxMap.set(bucket, handle);
396
+
397
+ if (fromBareSandbox) {
398
+ let bareMap = BARE_SANDBOX_HANDLES.get(sandbox);
399
+ if (!bareMap) {
400
+ bareMap = new Map();
401
+ BARE_SANDBOX_HANDLES.set(sandbox, bareMap);
402
+ }
403
+ bareMap.set(bucket, handle);
404
+ }
368
405
  return handle;
369
406
  }
370
407
 
371
408
  /** Admin handles are cached by bound context so Studio, agent, and
372
409
  * unattributed callers can share storage state without sharing provenance. */
373
- const ADMIN_CONTEXT_HANDLES = new WeakMap<SandboxContext, FirebaseStorage>();
374
- const BARE_ADMIN_HANDLES = new WeakMap<Sandbox, FirebaseStorage>();
410
+ const ADMIN_CONTEXT_HANDLES = new WeakMap<SandboxContext, Map<string, FirebaseStorage>>();
411
+ const BARE_ADMIN_HANDLES = new WeakMap<Sandbox, Map<string, FirebaseStorage>>();
375
412
 
376
413
  /**
377
414
  * INTERNAL (exported via `pyric/storage/internal` only) — construct
@@ -395,10 +432,11 @@ export function getAdminStorageSandbox(
395
432
  const fromBareSandbox = !isContext(target);
396
433
  const sandbox = isContext(target) ? target.sandbox : target;
397
434
  // Service first (late differing `rules` must throw, even on a cache hit).
398
- const servicePromise = ensureService(sandbox, options, 'getAdminStorageSandbox');
435
+ const rootServicePromise = ensureService(sandbox, options, 'getAdminStorageSandbox');
436
+ const bucket = options.bucket ?? DEFAULT_BUCKET;
399
437
 
400
438
  if (fromBareSandbox) {
401
- const cachedBare = BARE_ADMIN_HANDLES.get(sandbox);
439
+ const cachedBare = BARE_ADMIN_HANDLES.get(sandbox)?.get(bucket);
402
440
  if (cachedBare) return cachedBare;
403
441
  }
404
442
 
@@ -412,20 +450,36 @@ export function getAdminStorageSandbox(
412
450
  ? {}
413
451
  : { planId: baseContext.operationContext.planId }),
414
452
  });
415
- const cached = ADMIN_CONTEXT_HANDLES.get(baseContext);
453
+ const cached = ADMIN_CONTEXT_HANDLES.get(baseContext)?.get(bucket);
416
454
  if (cached) return cached;
417
455
 
456
+ const servicePromise = getOrCreateScopedService(sandbox, bucket, rootServicePromise);
457
+
418
458
  const sandboxTarget: SandboxTarget = {
419
459
  kind: 'sandbox',
420
460
  sandbox,
421
461
  context,
422
- bucket: options.bucket ?? DEFAULT_BUCKET,
462
+ bucket,
423
463
  servicePromise,
424
464
  admin: true,
425
465
  };
426
466
  const handle: FirebaseStorage = Object.freeze({ [TARGET_SYMBOL]: sandboxTarget });
427
- ADMIN_CONTEXT_HANDLES.set(baseContext, handle);
428
- if (fromBareSandbox) BARE_ADMIN_HANDLES.set(sandbox, handle);
467
+
468
+ let ctxMap = ADMIN_CONTEXT_HANDLES.get(baseContext);
469
+ if (!ctxMap) {
470
+ ctxMap = new Map();
471
+ ADMIN_CONTEXT_HANDLES.set(baseContext, ctxMap);
472
+ }
473
+ ctxMap.set(bucket, handle);
474
+
475
+ if (fromBareSandbox) {
476
+ let bareMap = BARE_ADMIN_HANDLES.get(sandbox);
477
+ if (!bareMap) {
478
+ bareMap = new Map();
479
+ BARE_ADMIN_HANDLES.set(sandbox, bareMap);
480
+ }
481
+ bareMap.set(bucket, handle);
482
+ }
429
483
  return handle;
430
484
  }
431
485