@fluidframework/local-driver 3.1.0 → 3.3.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.
@@ -24,7 +24,6 @@ import {
24
24
  type DataStoreRegistry,
25
25
  type FluidContainerAttached,
26
26
  type FluidContainerWithService,
27
- type OldestSupportedServiceClientVersion,
28
27
  type Registry,
29
28
  type ServiceClient,
30
29
  type ServiceOptions,
@@ -41,10 +40,13 @@ import {
41
40
  LocalDeltaConnectionServer,
42
41
  type ILocalDeltaConnectionServer,
43
42
  } from "@fluidframework/server-local-server";
43
+ import type { ITestDbFactory } from "@fluidframework/server-test-utils";
44
44
  import { UsageError } from "@fluidframework/driver-utils/internal";
45
+ import { v4 as uuid } from "uuid";
45
46
 
46
47
  import { LocalDocumentServiceFactory } from "./localDocumentServiceFactory.js";
47
48
  import { createLocalResolverCreateNewRequest, LocalResolver } from "./localResolver.js";
49
+ import { LocalSessionStorageDbFactory } from "./localSessionStorageDb.js";
48
50
  import { pkgVersion } from "./packageVersion.js";
49
51
 
50
52
  /**
@@ -63,13 +65,39 @@ export function startEphemeralService(isDefault = true): EphemeralService {
63
65
  throw new UsageError("A default EphemeralService is already running");
64
66
  }
65
67
 
66
- const service = new EphemeralServiceImplementation();
68
+ const service = new LocalServiceImplementation();
67
69
  if (isDefault) {
68
70
  defaultEphemeralService = service;
69
71
  }
70
72
  return service;
71
73
  }
72
74
 
75
+ /**
76
+ * Gets the session-storage-backed local Fluid service for the current JavaScript realm, creating it on first use.
77
+ *
78
+ * @remarks
79
+ * Attached documents remain available after a page reload within the same browser tab. Repeated calls return
80
+ * the same service instance within the current JavaScript realm.
81
+ *
82
+ * This service is only available in browser environments that provide `sessionStorage`.
83
+ * @returns A local service backed by browser session storage.
84
+ * @alpha
85
+ */
86
+ export function getSessionService(): SessionService {
87
+ if (typeof sessionStorage === "undefined") {
88
+ throw new UsageError("SessionService requires browser session storage");
89
+ }
90
+ return (sessionService ??= new LocalServiceImplementation(
91
+ new LocalSessionStorageDbFactory(),
92
+ ));
93
+ }
94
+
95
+ /** Closes and clears the current session service for test isolation. */
96
+ export async function resetSessionServiceForTesting(): Promise<void> {
97
+ await sessionService?.close();
98
+ sessionService = undefined;
99
+ }
100
+
73
101
  /**
74
102
  * Cleans up the service passed in {@link startEphemeralService}, or the {@link getDefaultEphemeralService|default} if none is passed.
75
103
  * @remarks
@@ -102,24 +130,100 @@ export function getDefaultEphemeralService(): EphemeralService {
102
130
  }
103
131
 
104
132
  /**
105
- * Internal Options for creating an {@link EphemeralServiceClient}, extending {@link @fluidframework/driver-definitions#ServiceOptions}
106
- * with the {@link EphemeralService} the client should connect to.
133
+ * Internal options for creating a local service client, extending
134
+ * {@link @fluidframework/driver-definitions#ServiceOptions} with the service the client should connect to.
107
135
  * @input
108
136
  * @internal
109
137
  */
110
- export interface EphemeralServiceOptions extends ServiceOptions {
138
+ export interface LocalServiceOptions<TService extends LocalService = LocalService>
139
+ extends ServiceOptions {
111
140
  /**
112
- * {@inheritdoc @fluidframework/driver-definitions#ServiceOptions.oldestSupportedClient}
141
+ * The service instance to connect to.
113
142
  */
114
- readonly oldestSupportedClient: OldestSupportedServiceClientVersion;
143
+ readonly service: TService;
144
+ }
145
+
146
+ /**
147
+ * Internal options for creating a {@link LocalServiceClient} connected to an {@link EphemeralService}.
148
+ * @input
149
+ * @internal
150
+ */
151
+ export interface EphemeralServiceOptions extends LocalServiceOptions<EphemeralService> {}
152
+
153
+ /**
154
+ * A local Fluid service with an explicitly managed lifecycle.
155
+ * @remarks
156
+ * There are two implementations of this interface with different document lifetimes:
157
+ * {@link EphemeralService} and {@link SessionService}.
158
+ *
159
+ * @typeParam TClient - The type of client this service creates.
160
+ * @alpha @sealed
161
+ */
162
+ export interface LocalService<out TClient extends ServiceClient = LocalServiceClient>
163
+ extends ErasedBaseType<readonly ["LocalService", TClient]> {
115
164
  /**
116
- * The service instance to connect to.
165
+ * Lists the IDs of documents currently stored by this service.
166
+ */
167
+ listDocumentIds(): Promise<readonly string[]>;
168
+
169
+ /**
170
+ * Deletes a stored document.
171
+ *
172
+ * @remarks
173
+ * Deletion is only allowed when this service has no open containers because resetting the local
174
+ * server invalidates all of its active connections. Content-addressed summary data shared with other
175
+ * documents may be retained until {@link LocalService.deleteAllDocuments} is called.
176
+ * Only one call to {@link LocalService.deleteDocument} or {@link LocalService.deleteAllDocuments}
177
+ * may be in progress at a time.
178
+ *
179
+ * @param id - The ID of the document to delete.
180
+ * @throws A `UsageError` if the service is closed, another document deletion is in progress, or the
181
+ * service has open containers.
117
182
  */
118
- readonly service: EphemeralService;
183
+ deleteDocument(id: string): Promise<void>;
184
+
185
+ /**
186
+ * Deletes all documents stored by this service.
187
+ *
188
+ * @remarks
189
+ * Deletion is only allowed when this service has no open containers because resetting the local
190
+ * server invalidates all of its active connections.
191
+ * Only one call to {@link LocalService.deleteDocument} or {@link LocalService.deleteAllDocuments}
192
+ * may be in progress at a time.
193
+ * @throws A `UsageError` if the service is closed, another document deletion is in progress, or the
194
+ * service has open containers.
195
+ */
196
+ deleteAllDocuments(): Promise<void>;
197
+
198
+ /**
199
+ * Drives all containers connected to this service toward convergence, processing pending operations and
200
+ * waiting for all dirty containers to save.
201
+ *
202
+ * @param timeoutMilliseconds - The maximum time to wait for containers to quiesce, in milliseconds. Defaults to 30_000.
203
+ * @throws A `UsageError` if the containers do not quiesce before the timeout expires.
204
+ *
205
+ * @privateRemarks
206
+ * This is a best-effort implementation simplified from `LoaderContainerTracker.ensureSynchronized`.
207
+ * Currently it does not perform receiver-side sequence-number quiescence or wait for join/leave (audience) ops.
208
+ * See `LoaderContainerTracker.ensureSynchronized` for the fuller version this is based on.
209
+ */
210
+ synchronize(timeoutMilliseconds?: number): Promise<void>;
211
+
212
+ /**
213
+ * Creates a client connected to this service.
214
+ *
215
+ * @param options - Collaboration options for the client.
216
+ */
217
+ newClient(options: ServiceOptions): TClient;
218
+
219
+ /**
220
+ * A client connected to this service using the default options.
221
+ */
222
+ readonly defaultClient: TClient;
119
223
  }
120
224
 
121
225
  /**
122
- * An in-memory Fluid service that can produce connected {@link EphemeralServiceClient}s.
226
+ * An in-memory Fluid service that can produce connected {@link LocalServiceClient}s.
123
227
  * @remarks
124
228
  * All documents created through clients connected to a given `EphemeralService` are held in-memory by that service.
125
229
  * Closing the service (via {@link EphemeralService.close} or {@link cleanupEphemeralService}) closes the connections
@@ -137,20 +241,20 @@ export interface EphemeralServiceOptions extends ServiceOptions {
137
241
  * document with different `oldestSupportedClient` values.
138
242
  * This also exposes a place to put APIs for preloading and exporting document contents in the future.
139
243
  *
140
- * This is an erased type: its only implementation is the module-private {@link EphemeralServiceImplementation}, which holds
244
+ * This is an erased type: its only implementation is the module-private `LocalServiceImplementation`, which holds
141
245
  * the mutable server and container state so it does not appear on this public type.
142
246
  *
143
247
  * TODO: formalize this lifecycle with an interface which documents these stages.
144
248
  * Lifecycle:
145
249
  * The intended lifecycle of an {@link EphemeralService} follows roughly the same pattern as containers:
146
250
  *
147
- * 1. Open: accepts connections from {@link EphemeralServiceClient}s, which can create and load containers.
251
+ * 1. Open: accepts connections from {@link LocalServiceClient}s, which can create and load containers.
148
252
  * Might have timers and event registrations which can trigger asynchronous work, and retain the object in memory.
149
253
  *
150
254
  * 2. Closing: asynchronous transition from open to closed. New use should behave as it closed, but may be cleaning up or saving resources asynchronously.
151
255
  * Timers and event registrations may still be active, but should be cleaned up by the time the transition to closed completes.
152
256
  *
153
- * 3. Closed: no longer accepts connections from {@link EphemeralServiceClient}s, and all containers connected to it are closed.
257
+ * 3. Closed: no longer accepts connections from {@link LocalServiceClient}s, and all containers connected to it are closed.
154
258
  * Should have no subscriptions to events or timers which could retain it in memory or trigger asynchronous work.
155
259
  * The object can still be used in a limited capacity (typically just to inspect its status (e.g. `isClosed`), and to view (but not edit) the final state of any containers which were connected to it before it closed.)
156
260
  *
@@ -159,106 +263,106 @@ export interface EphemeralServiceOptions extends ServiceOptions {
159
263
  *
160
264
  * @alpha @sealed
161
265
  */
162
- export interface EphemeralService extends ErasedBaseType<readonly ["EphemeralService"]> {
163
- /**
164
- * Close this service, which closes all containers connected to it and releases its resources.
165
- * @remarks
166
- * All documents held by this service are discarded, and any timers it (or its containers) were keeping alive
167
- * are cleaned up.
168
- * The returned promise resolves once all asynchronous cleanup (including shutting down the in-memory server)
169
- * has completed.
170
- * Closing is idempotent: calling it again after the service is closed resolves without doing anything.
171
- */
172
- close(): Promise<void>;
173
-
266
+ export interface EphemeralService extends LocalService<LocalServiceClient<EphemeralService>> {
174
267
  /**
175
- * Drives all containers connected to this service toward convergence, processing pending operations and
176
- * waiting for all dirty containers to save.
177
- *
178
- * @param timeoutMilliseconds - The maximum time to wait for containers to quiesce, in milliseconds. Defaults to 30_000.
179
- *
180
- * @privateRemarks
181
- * This is a best-effort implementation simplified from `LoaderContainerTracker.ensureSynchronized`.
182
- * Currently it does not perform receiver-side sequence-number quiescence or wait for join/leave (audience) ops.
183
- * See `LoaderContainerTracker.ensureSynchronized` for the fuller version this is based on.
184
- * For the currently exposed API surface, this should be sufficient,
185
- * but users down casting to internal types might run into some limitations.
186
- */
187
- synchronize(timeoutMilliseconds?: number): Promise<void>;
188
-
189
- /**
190
- * Creates and returns a {@link EphemeralServiceClient} for an in-memory, ephemeral Fluid service.
191
- *
192
- * @param options - Options for the client. `oldestSupportedClient` may be omitted because all
193
- * clients are in the same process, so it defaults to the current version. `service` may be omitted to allocate a new
194
- * {@link EphemeralService} dedicated to this client, or provided to connect the client to an existing service instance.
268
+ * Closes all containers connected to this service and releases its active resources.
195
269
  *
196
270
  * @remarks
197
- * The service is ephemeral and in-memory: all documents are held by the {@link EphemeralService} the client is
198
- * connected to, and live for as long as that service is open — independent of whether any container for them is open.
199
- * A document created and attached (obtaining an `id`) can be loaded by `id` for as long as its service remains open,
200
- * even after every container for it has been closed.
201
- * Closing the service (via {@link EphemeralService.close} or {@link cleanupEphemeralService}) discards all of its
202
- * documents and releases its resources; afterwards those `id`s can no longer be loaded.
203
- *
204
- * When no `service` is provided, a new one is allocated for this client (accessible via {@link EphemeralServiceClient.service}).
205
- * Provide the same {@link EphemeralService} to multiple clients (via `options.service`) to have them collaborate on the
206
- * same documents, and control that service's lifetime explicitly.
207
- *
208
- * Since a service holds timers while open, tests should close the services they use (e.g. via
209
- * {@link cleanupEphemeralService} in an `afterEach`) to avoid lingering timers that can hang test runners.
210
- *
211
- * @privateRemarks
212
- * TODO: We should provide a way to extract (for potential serialization as test data) and load documents into a service.
213
- * This is needed to use this API surface for testing reference documents.
214
- * Ideally we would provide a service agnostic way to do the export, but likely only support loading them into the local service.
215
- * This can be done via an API on FluidContainer (or a free function taking one) to do the export, then adding a
216
- * service specific API (on {@link EphemeralService}) to load from the export format and return the ID of the loaded document.
217
- */
218
- newClient(options: ServiceOptions): EphemeralServiceClient;
219
-
220
- /**
221
- * A client connected to this service using the default options.
271
+ * Closing is idempotent. Closing an ephemeral service permanently discards all documents it holds,
272
+ * so their IDs can no longer be loaded.
222
273
  */
223
- readonly defaultClient: EphemeralServiceClient;
274
+ close(): Promise<void>;
224
275
  }
225
276
 
277
+ /**
278
+ * A browser-local Fluid service that persists documents in session storage.
279
+ *
280
+ * @remarks
281
+ * Its attached documents remain available after a page reload within the same browser tab. The service is
282
+ * shared within the current JavaScript realm and intentionally has no close operation: its active resources
283
+ * live until the realm is unloaded.
284
+ *
285
+ * Session storage is shared more broadly than JavaScript module state. Separate same-origin realms, such as
286
+ * same-origin frames, or applications that load separate copies of this package can access the same stored
287
+ * documents while running independent local servers. Concurrently editing the same document from such realms
288
+ * is unsupported and may produce inconsistent stored state.
289
+ *
290
+ * Create one with {@link getSessionService}.
291
+ * @alpha @sealed
292
+ */
293
+ export interface SessionService extends LocalService<LocalServiceClient<SessionService>> {}
294
+
226
295
  /**
227
296
  * The {@link defaultEphemeralService} if one has been {@link startEphemeralService|started}.
228
297
  */
229
- let defaultEphemeralService: EphemeralServiceImplementation | undefined;
298
+ let defaultEphemeralService: LocalServiceImplementation | undefined;
230
299
 
231
300
  /**
232
- * The concrete implementation of {@link EphemeralService}.
301
+ * The lazily created session service for this JavaScript realm.
302
+ */
303
+ let sessionService: LocalServiceImplementation | undefined;
304
+
305
+ /**
306
+ * The concrete implementation of local services.
233
307
  * @remarks
234
308
  * Kept module-private so its mutable state and internal helpers are not part of the public API.
235
- * Narrow an {@link EphemeralService} to it with `EphemeralServiceImplementation.narrow`.
309
+ * Narrow a {@link LocalService} to it with `LocalServiceImplementation.narrow`.
236
310
  */
237
- class EphemeralServiceImplementation
238
- extends ErasedTypeImplementation<EphemeralService>
239
- implements EphemeralService
311
+ class LocalServiceImplementation
312
+ extends ErasedTypeImplementation<
313
+ LocalService<LocalServiceClientImplementation<LocalServiceImplementation>>
314
+ >
315
+ implements LocalService<LocalServiceClientImplementation<LocalServiceImplementation>>
240
316
  {
241
- // A single server is shared by all containers connected to this service so they can communicate with each other.
242
- private readonly server: ILocalDeltaConnectionServer =
243
- LocalDeltaConnectionServer.create(
244
- // new LocalSessionStorageDbFactory(),
245
- );
246
- private readonly documentServiceFactory = new LocalDocumentServiceFactory(this.server);
317
+ /**
318
+ * The active in-memory server shared by this service's containers so they can communicate.
319
+ * Replaced after document maintenance resets the server.
320
+ */
321
+ private server: ILocalDeltaConnectionServer;
322
+ /** The document service factory bound to the active {@link LocalServiceImplementation.server}. */
323
+ private documentServiceFactory: LocalDocumentServiceFactory;
324
+ /** The persistent database factory retained when the active server is replaced. */
325
+ private readonly databaseFactory: ITestDbFactory;
326
+ /** The open containers connected to this service. */
247
327
  private readonly containers = new Set<EphemeralServiceContainer<unknown>>();
328
+ /** Whether this service has been permanently closed. */
248
329
  private closed = false;
330
+ /** Whether document deletion is temporarily preventing other service operations. */
331
+ private maintenanceInProgress = false;
249
332
 
250
- public constructor() {
333
+ public constructor(databaseFactory?: ITestDbFactory) {
251
334
  super();
335
+ this.server = LocalDeltaConnectionServer.create(databaseFactory);
336
+ this.databaseFactory = this.server.testDbFactory;
337
+ this.documentServiceFactory = new LocalDocumentServiceFactory(this.server);
252
338
  this.defaultClient = this.newClient();
253
339
  }
254
- public newClient(options?: Partial<ServiceOptions>): EphemeralServiceClient {
255
- const finalOptions: EphemeralServiceOptions = {
340
+ public newClient(
341
+ options?: Partial<ServiceOptions>,
342
+ ): LocalServiceClientImplementation<LocalServiceImplementation> {
343
+ const finalOptions: LocalServiceOptions<LocalServiceImplementation> = {
344
+ ...options,
256
345
  oldestSupportedClient: options?.oldestSupportedClient ?? featureVersion(pkgVersion),
257
346
  service: this,
258
347
  };
259
- return new EphemeralServiceClientImplementation(finalOptions);
348
+ return new LocalServiceClientImplementation(finalOptions);
349
+ }
350
+ public readonly defaultClient: LocalServiceClientImplementation<LocalServiceImplementation>;
351
+
352
+ public async listDocumentIds(): Promise<readonly string[]> {
353
+ this.ensureAvailable();
354
+ const documentCollection = await this.server.databaseManager.getDocumentCollection();
355
+ const documents = await documentCollection.findAll();
356
+ return documents.map((document) => document.documentId);
357
+ }
358
+
359
+ public async deleteDocument(id: string): Promise<void> {
360
+ await this.deleteDocuments(id);
361
+ }
362
+
363
+ public async deleteAllDocuments(): Promise<void> {
364
+ await this.deleteDocuments();
260
365
  }
261
- public readonly defaultClient: EphemeralServiceClient;
262
366
 
263
367
  public async close(): Promise<void> {
264
368
  if (this.closed) {
@@ -279,6 +383,67 @@ class EphemeralServiceImplementation
279
383
  await this.server.close();
280
384
  }
281
385
 
386
+ private ensureAvailable(): void {
387
+ if (this.closed) {
388
+ throw new UsageError("Local service is closed");
389
+ }
390
+ if (this.maintenanceInProgress) {
391
+ throw new UsageError("Local service document maintenance is already in progress");
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Delete all documents unless `id` is specified, in which case only that document is deleted.
397
+ */
398
+ private async deleteDocuments(id?: string): Promise<void> {
399
+ this.ensureAvailable();
400
+ if (this.containers.size > 0) {
401
+ throw new UsageError("Close all containers before deleting local service documents");
402
+ }
403
+
404
+ this.maintenanceInProgress = true;
405
+ let serverClosed = false;
406
+ try {
407
+ await this.server.close();
408
+ serverClosed = true;
409
+ const databaseManager = this.server.databaseManager;
410
+ const filter = id === undefined ? {} : { documentId: id };
411
+ const historianDatabase = this.databaseFactory.testDatabase;
412
+ const documentCollection = await databaseManager.getDocumentCollection();
413
+ const checkpointCollection = await databaseManager.getCheckpointCollection();
414
+ const deltaCollection = await databaseManager.getDeltaCollection(undefined, id);
415
+ const scribeDeltaCollection = await databaseManager.getScribeDeltaCollection(
416
+ undefined,
417
+ id,
418
+ );
419
+ const deletions = [
420
+ documentCollection.deleteMany(filter),
421
+ checkpointCollection.deleteMany(filter),
422
+ deltaCollection.deleteMany(filter),
423
+ scribeDeltaCollection.deleteMany(filter),
424
+ historianDatabase
425
+ .collection("refs")
426
+ .deleteMany(id === undefined ? {} : { _id: `heads/${id}` }),
427
+ ];
428
+ if (id === undefined) {
429
+ const nodeCollection = await databaseManager.getNodeCollection();
430
+ deletions.push(
431
+ nodeCollection.deleteMany({}),
432
+ historianDatabase.collection("blobs").deleteMany({}),
433
+ historianDatabase.collection("commits").deleteMany({}),
434
+ historianDatabase.collection("trees").deleteMany({}),
435
+ );
436
+ }
437
+ await Promise.all(deletions);
438
+ } finally {
439
+ if (serverClosed) {
440
+ this.server = LocalDeltaConnectionServer.create(this.databaseFactory);
441
+ this.documentServiceFactory = new LocalDocumentServiceFactory(this.server);
442
+ }
443
+ this.maintenanceInProgress = false;
444
+ }
445
+ }
446
+
282
447
  public async synchronize(timeoutMilliseconds = 30_000): Promise<void> {
283
448
  // Timeout to allow for better errors in the case of hangs.
284
449
  let timedOut = false;
@@ -366,6 +531,7 @@ class EphemeralServiceImplementation
366
531
  * @remarks Internal helper for {@link EphemeralServiceContainer}; not part of the public {@link EphemeralService} API.
367
532
  */
368
533
  public getDocumentServiceFactory(): LocalDocumentServiceFactory {
534
+ this.ensureAvailable();
369
535
  assert(
370
536
  !this.closed,
371
537
  0xd11 /* Cannot create or load containers on a closed EphemeralService */,
@@ -391,23 +557,27 @@ class EphemeralServiceImplementation
391
557
  }
392
558
 
393
559
  /**
394
- * A {@link @fluidframework/driver-definitions#ServiceClient} connected to a specific {@link EphemeralService}.
560
+ * A {@link @fluidframework/driver-definitions#ServiceClient} connected to a specific {@link LocalService}.
561
+ *
562
+ * @typeParam TService - The type of local service this client is connected to.
395
563
  * @alpha @sealed
396
564
  */
397
- export interface EphemeralServiceClient extends ServiceClient {
565
+ export interface LocalServiceClient<
566
+ out TService extends LocalService<ServiceClient> = LocalService<ServiceClient>,
567
+ > extends ServiceClient {
398
568
  /**
399
569
  * The service instance this client is connected to.
400
570
  */
401
- readonly service: EphemeralService;
571
+ readonly service: TService;
402
572
  }
403
573
 
404
- class EphemeralServiceClientImplementation
405
- extends ServiceClientImplementation<EphemeralServiceOptions>
406
- implements EphemeralServiceClient
574
+ class LocalServiceClientImplementation<TService extends LocalService>
575
+ extends ServiceClientImplementation<LocalServiceOptions<TService>>
576
+ implements LocalServiceClient
407
577
  {
408
- public readonly service: EphemeralService;
578
+ public readonly service: TService;
409
579
 
410
- public constructor(options: EphemeralServiceOptions) {
580
+ public constructor(options: LocalServiceOptions<TService>) {
411
581
  super(options, EphemeralServiceContainer);
412
582
  this.service = options.service;
413
583
  }
@@ -452,8 +622,6 @@ const createLoadExistingRequest = (documentId: string): IRequest => {
452
622
  return { url: `http://localhost:3000/${documentId}` };
453
623
  };
454
624
 
455
- let documentIdCounter = 0;
456
-
457
625
  /**
458
626
  * A Fluid container backed by an ephemeral (in-memory) local service, implementing
459
627
  * {@link @fluidframework/driver-definitions#FluidContainerWithService}.
@@ -465,19 +633,20 @@ let documentIdCounter = 0;
465
633
  * @internal
466
634
  */
467
635
  export class EphemeralServiceContainer<TData>
468
- extends ServiceContainerBase<TData, EphemeralServiceOptions>
636
+ extends ServiceContainerBase<TData, LocalServiceOptions>
469
637
  implements FluidContainerWithService<TData>
470
638
  {
471
- public readonly service: EphemeralService;
639
+ public readonly service: LocalService;
472
640
 
473
641
  public static async createDetached<T>(
474
642
  registry: DataStoreRegistry<T>,
475
- options: EphemeralServiceOptions,
643
+ options: LocalServiceOptions,
476
644
  root: DataStoreKind<T>,
477
645
  ): Promise<EphemeralServiceContainer<T>> {
478
- EphemeralServiceImplementation.narrow(options.service);
646
+ LocalServiceImplementation.narrow(options.service);
479
647
  const container: IContainer = await createDetachedContainer({
480
648
  codeDetails: { package: "1.0" },
649
+ logger: options.logger,
481
650
  urlResolver,
482
651
  documentServiceFactory: options.service.getDocumentServiceFactory(),
483
652
  codeLoader: makeCodeLoader(
@@ -499,12 +668,13 @@ export class EphemeralServiceContainer<TData>
499
668
 
500
669
  public static async load<T>(
501
670
  registry: DataStoreRegistry<T>,
502
- options: EphemeralServiceOptions,
671
+ options: LocalServiceOptions,
503
672
  id: string,
504
673
  ): Promise<EphemeralServiceContainer<T> & FluidContainerAttached<T>> {
505
- EphemeralServiceImplementation.narrow(options.service);
674
+ LocalServiceImplementation.narrow(options.service);
506
675
  const containerInner = await loadExistingContainer({
507
676
  request: createLoadExistingRequest(id),
677
+ logger: options.logger,
508
678
  urlResolver,
509
679
  documentServiceFactory: options.service.getDocumentServiceFactory(),
510
680
  codeLoader: makeCodeLoader(
@@ -530,26 +700,25 @@ export class EphemeralServiceContainer<TData>
530
700
 
531
701
  private constructor(
532
702
  registry: Registry<Promise<DataStoreKind<TData>>>,
533
- options: EphemeralServiceOptions,
703
+ options: LocalServiceOptions,
534
704
  container: IContainer,
535
705
  data: TData,
536
706
  id: string | undefined,
537
707
  ) {
538
708
  super(registry, options, container, data, id);
539
709
  this.service = options.service;
540
- EphemeralServiceImplementation.narrow(this.service);
710
+ LocalServiceImplementation.narrow(this.service);
541
711
  this.service.addContainer(this);
542
712
  }
543
713
 
544
714
  public override close(): void {
545
715
  super.close();
546
716
  // Remove this now-closed container from its service's set of open containers.
547
- EphemeralServiceImplementation.narrow(this.service);
717
+ LocalServiceImplementation.narrow(this.service);
548
718
  this.service.removeContainer(this);
549
719
  }
550
720
 
551
721
  protected createAttachRequest(): IRequest {
552
- const documentId = (documentIdCounter++).toString();
553
- return createLocalResolverCreateNewRequest(documentId);
722
+ return createLocalResolverCreateNewRequest(uuid());
554
723
  }
555
724
  }
package/src/index.ts CHANGED
@@ -16,11 +16,15 @@ export {
16
16
  export { LocalSessionStorageDbFactory } from "./localSessionStorageDb.js";
17
17
  export type {
18
18
  EphemeralService,
19
- EphemeralServiceClient,
20
19
  EphemeralServiceOptions,
20
+ LocalService,
21
+ LocalServiceClient,
22
+ LocalServiceOptions,
23
+ SessionService,
21
24
  } from "./ephemeralService.js";
22
25
  export {
23
26
  startEphemeralService,
27
+ getSessionService,
24
28
  cleanupEphemeralService,
25
29
  getDefaultEphemeralService,
26
30
  } from "./ephemeralService.js";
@@ -8,15 +8,23 @@ import type { ICollection, IDb } from "@fluidframework/server-services-core";
8
8
  import type { ITestDbFactory } from "@fluidframework/server-test-utils";
9
9
  import { v4 as uuid } from "uuid";
10
10
 
11
+ /** Namespace for all session-storage records owned by local-driver. */
12
+ const sessionStorageKeyPrefix = "@fluidframework/local-driver:";
13
+
11
14
  /**
12
15
  * A collection for local session storage, where data is stored in the browser
13
16
  * Functions include database operations such as queries, insertion and update.
14
17
  */
15
18
  class LocalSessionStorageCollection<T> implements ICollection<T> {
19
+ /** Prefix that scopes session-storage records to this collection. */
20
+ private readonly storageKeyPrefix: string;
21
+
16
22
  /**
17
23
  * @param collectionName - data type of the collection, e.g. blobs, deltas, trees, etc.
18
24
  */
19
- constructor(private readonly collectionName: string) {}
25
+ constructor(collectionName: string) {
26
+ this.storageKeyPrefix = `${sessionStorageKeyPrefix}${collectionName}-`;
27
+ }
20
28
 
21
29
  public aggregate(pipeline: any, options?: any): any {
22
30
  throw new Error("Method Not Implemented");
@@ -194,14 +202,22 @@ class LocalSessionStorageCollection<T> implements ICollection<T> {
194
202
  * {@inheritDoc @fluidframework/server-services-core#ICollection.deleteOne}
195
203
  */
196
204
  public async deleteOne(query: any): Promise<any> {
197
- throw new Error("Method not implemented.");
205
+ const value = this.findOneInternal(query);
206
+ if (value !== null) {
207
+ sessionStorage.removeItem(`${this.storageKeyPrefix}${value._id}`);
208
+ }
209
+ return value;
198
210
  }
199
211
 
200
212
  /**
201
213
  * {@inheritDoc @fluidframework/server-services-core#ICollection.deleteMany}
202
214
  */
203
215
  public async deleteMany(query: any): Promise<any> {
204
- throw new Error("Method not implemented.");
216
+ const values = await this.find(query, undefined);
217
+ for (const value of values) {
218
+ sessionStorage.removeItem(`${this.storageKeyPrefix}${value._id}`);
219
+ }
220
+ return values;
205
221
  }
206
222
 
207
223
  /**
@@ -219,7 +235,7 @@ class LocalSessionStorageCollection<T> implements ICollection<T> {
219
235
  for (let i = 0; i < sessionStorage.length; i++) {
220
236
  const key = sessionStorage.key(i);
221
237
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
222
- if (key!.startsWith(this.collectionName)) {
238
+ if (key!.startsWith(this.storageKeyPrefix)) {
223
239
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
224
240
  values.push(JSON.parse(sessionStorage.getItem(key!)!));
225
241
  }
@@ -240,7 +256,7 @@ class LocalSessionStorageCollection<T> implements ICollection<T> {
240
256
  if (!value._id) {
241
257
  value._id = uuid();
242
258
  }
243
- sessionStorage.setItem(`${this.collectionName}-${value._id}`, JSON.stringify(value));
259
+ sessionStorage.setItem(`${this.storageKeyPrefix}${value._id}`, JSON.stringify(value));
244
260
  }
245
261
  }
246
262
  }
@@ -254,7 +270,7 @@ class LocalSessionStorageCollection<T> implements ICollection<T> {
254
270
  */
255
271
  private findOneInternal(query: any): any {
256
272
  if (query._id) {
257
- const json = sessionStorage.getItem(`${this.collectionName}-${query._id}`);
273
+ const json = sessionStorage.getItem(`${this.storageKeyPrefix}${query._id}`);
258
274
  if (json) {
259
275
  return JSON.parse(json);
260
276
  }
@@ -263,7 +279,7 @@ class LocalSessionStorageCollection<T> implements ICollection<T> {
263
279
  for (let i = 0; i < sessionStorage.length; i++) {
264
280
  const ssKey = sessionStorage.key(i);
265
281
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266
- if (!ssKey!.startsWith(this.collectionName)) {
282
+ if (!ssKey!.startsWith(this.storageKeyPrefix)) {
267
283
  continue;
268
284
  }
269
285
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/local-driver";
9
- export const pkgVersion = "3.1.0";
9
+ export const pkgVersion = "3.3.0";