Skip to content

feat(storageState): add OPFS support#41420

Draft
gwennlbh wants to merge 7 commits into
microsoft:mainfrom
gwennlbh:storagestate-opfs
Draft

feat(storageState): add OPFS support#41420
gwennlbh wants to merge 7 commits into
microsoft:mainfrom
gwennlbh:storagestate-opfs

Conversation

@gwennlbh

Copy link
Copy Markdown
Contributor

Closes #41400

@gwennlbh gwennlbh force-pushed the storagestate-opfs branch from 6f6684c to 18b71cc Compare June 23, 2026 08:53
Comment thread packages/injected/src/storageScript.ts Outdated

@gwennlbh gwennlbh left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll wait for an answer on the comment i added since if baseline 2025 is not acceptable and neither is putting the script in a web worker, i don't wanna spend time on writing tests 😅

Comment thread packages/injected/src/storageScript.ts Outdated
}

// Getting a File object's contents requires async
export async function serializeFile(value: File): Promise<Extract<SerializedValue, { f: any; }>> {

This comment was marked as outdated.

Comment thread packages/injected/src/storageScript.ts Outdated
Comment thread packages/injected/src/storageScript.ts Outdated
};

type OPFSTree = Array<
[name: string, contents: Extract<SerializedValue, {f: any}> | OPFSTree]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use conservative types:

type FSEntry {
  type: 'file' | 'folder';
  name: string;
  lastModified: number;
}

type File = FSEntry & {
  type: 'file';
  base64: string;
}

type Folder = FSEntry & {
  type: 'folder';
  entries: (File | Folder)[];
}

{ ta: { b: string, k: TypedArrayKind } } |
{ ab: { b: string } };
{ ab: { b: string } } |
{ f: { b: string, n: string, t: string, m: number } };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to serialize and restore opfs without this.

return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]);
if ('ab' in value)
return base64ToTypedArray(value.ab.b, Uint8Array).buffer;
if ('f' in value) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it like this so that we would get File object support in IndexedDB as a side effect of the PR, but yeah it's not strictly necessary


async storageState(params: channels.BrowserContextStorageStateParams, progress: Progress): Promise<channels.BrowserContextStorageStateResult> {
return await this._context.storageState(progress, params.indexedDB, params.credentials);
return await this._context.storageState(progress, params.indexedDB, params.credentials, params.opfs);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass params as object

@gwennlbh gwennlbh force-pushed the storagestate-opfs branch 3 times, most recently from e9d3564 to 2040049 Compare June 25, 2026 19:34
@gwennlbh

gwennlbh commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

i just realized, i don't think there's any way to set the lastModified or content type of the file when writing it into OPFS. Maybe we should just leave them out of the serialized data :/ Unfortunate cuz tests can't rely on these things (esp. the last modified date could be useful, i feel like)

I mean, they have a solution: mocking FileSystemFileHandle#getFile i guess


async storageState(params: channels.APIRequestContextStorageStateParams, progress: Progress): Promise<channels.APIRequestContextStorageStateResult> {
return await this._object.storageState(progress, params.indexedDB);
return await this._object.storageState(progress, params.indexedDB, params.opfs);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: params object here

}[],
};

export type FSEntry = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is generated, where did we get these entries from and why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, didn't know that

i didn't run the build or test my changes yet, thats why the PR is still a draft (i was waiting on a decision to move forward even with some stuff not being baseline widely available yet).

also, what do you think about the lastModified thing ? i think i'll just not store it: if we can't control it, might as well not make tests unreliable by making the serialization dependent on the current time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found it, I edited packages/protocol/spec and docs/src.

Comment thread docs/src/api/class-apirequest.md Outdated
- `type` <[FSEntryType]<"file"|"folder">> Type of the entry. "folder" for the root of the OPFS
- `base64` ?<[string]> Contents of the file, for "file" entries
- `contentType` ?<[string]> Content type of the file, for "file" entries
- `entries` ?<[Array]<[Object]<[string], [any]>>> Array of child entries (recursive), for "folder" entries

@gwennlbh gwennlbh Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recursive nature of FSEntry is causing issues here (a type mismatch at APIRequest#newContext)

I think I'm gonna go with a flat structure instead, that store absolute paths to files. It's a bit annoying because we need to be able to represent empty directories so we still have to make base64 & contentType optional (or make them take sentinel values like application/vnd.playwright.opfs-folder for contentType but thats worse imo), but oh well.

@gwennlbh gwennlbh force-pushed the storagestate-opfs branch from 08cbb44 to bbaaff9 Compare July 3, 2026 07:28
@gwennlbh

gwennlbh commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

sorry for the big force-push, I forgot the conventional commits prefixes in my later commits (i mean it's gonna get squashed anyways i guess, but since contributing.md talks about naming your commits that way might aswell do it)

@gwennlbh gwennlbh left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pavelfeldman for the test, i'm planning on adding a "store TODOs to OPFS" button in the to-do-notifications mock app, that would write one folder per deadline month containing one .md file per item:

- list:
  - listitem: "lorem [lorem] - 00:00, January 1 2027."
  - listitem: "ipsum [ipsum] - 00:00, January 5 2027."
  - listitem: "dolor [dolor] - 00:00, December 5 2027."

would result in

(opfs root)
/ January
  / lorem.md
  / ipsum.md
/ December
  / dolor.md

is this okay?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Capture OPFS in storageState

2 participants