Skip to content

Sharable playground state - #10

Merged
antstei merged 8 commits into
playgroundfrom
playground-sharable
Sep 1, 2026
Merged

antstei merged 8 commits into
playgroundfrom
playground-sharable

Conversation

@jacobpake

Copy link
Copy Markdown
Contributor

Adds the bones for sharable state via URL params

@jacobpake

Copy link
Copy Markdown
Contributor Author

E.g. https://playground-sharable.purepy.pages.dev/playground?v=0&c=1&p=q1ZKy8xJLVayiq5WKkgsyVCyUspNzMzTK6hU0lFKSSxJVLJSKijKzCvRiFHySM3JyVdIK8rPVSjJSFUIDfJRjFHSVKqNrQUA

This seems to be OK with excessively large files (though the scrolling seems to be broken in the editor so I won't demo that now).

We will generate "pretty" URLs for the builtin examples.

@antstei

antstei commented Aug 24, 2026

Copy link
Copy Markdown
Member

Thanks a lot, Jacob! 🎉

Would it make sense to spell out the query parameters and move the payload into the URL fragment after the hash (#)?

/playground?version=0&compressed=false#<payload>

This would make the parameters more self-describing and keep the payload separate from the format options. For the compression flag, I would prefer true/false over 1/0 because the meaning is clearer, while the difference in URL length is negligible compared with the payload. If compression is enabled by default, we could omit compressed=true and use compressed=false only when compression is disabled.

Would it also make sense to add an optional embed flag? If omitted, it could default to the current behavior, while embed=true, for example, would enable an embedded view.

@jacobpake

Copy link
Copy Markdown
Contributor Author

This would make the parameters more self-describing and keep the payload separate from the format options. For the compression flag, I would prefer true/false over 1/0 because the meaning is clearer, while the difference in URL length is negligible compared with the payload.

I would typically agree, but these parameters relate directly to the payload, rather than to the page, and I am think that making their meaning clearer would cause more harm as they are not meant to be user specified or read by users. They are completely dependent on the payload, so changing one without updating the other would break things.

The only reason that they are not included in the hash themselves is so that they can talk about the encoding method used.

Perhaps these could be prepended to the hash rather than stored as separate query parameters? Then that might avoid any confusion (particularly if we wanted to use query parameters that actually do effect the page e.g. we might want version/v to specify the PurePy version used!)

Would it also make sense to add an optional embed flag? If omitted, it could default to the current behavior, while embed=true, for example, would enable an embedded view.

Yes - though we don't support this yet.

@jacobpake

jacobpake commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

What about something like /playground?state={data} where data contains the version, compression flag and the hash?

@antstei

antstei commented Aug 24, 2026

Copy link
Copy Markdown
Member

They are completely dependent on the payload, so changing one without updating the other would break things.
Perhaps these could be prepended to the hash rather than stored as separate query parameters?
What about something like /playground?state={data} where data contains the version, compression flag and the hash?

Those are good points. I like the idea of putting the current payload-related parameters—version and compression—in the URL fragment together with the payload.

Then that might avoid any confusion particularly if we wanted to use query parameters that actually do effect the page e.g. we might want version to specify the PurePy version used.

I agree that this separation makes sense. Furthermore, I see three categories of information:

  1. Payload-format information, such as the compression setting, which is required to decode the payload.
  2. PurePy program information, such as the PurePy version and, potentially, the program entry point when multiple files are supplied, alongside the PurePy program itself, which describes the encoded PurePy program and should therefore be stored within the same serialized payload as the program itself.
  3. Playground options, such as an optional embed flag or dark-mode setting, which control how the playground is displayed and are passed as regular URL parameters.

For example, in

/playground?embed=true#<encoded-envelope>

embed=true is a playground option, while the <encoded-envelope> URL fragment contains the payload-format information and the serialized payload:

{
  "format_version": 1,
  "compression": "deflate-raw",
  "payload": "H4sIAJc5jGoC_6tW..."
}

The payload field contains the serialized PurePy program information and program after compression. Once decoded and decompressed, it might contain:

{
  "purepy_version": "1.0",
  "entry_point": "main.py",
  "files": [
    {
      "path": "main.py",
      "data": "print('Hello')"
    }
  ]
}

…ms, encode version in first byte of the hash, drop separate compression configuration
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying purepy with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5c589ce
Status: ✅  Deploy successful!
Preview URL: https://18d50373.purepy.pages.dev
Branch Preview URL: https://playground-sharable.purepy.pages.dev

View logs

@jacobpake

jacobpake commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@antstei I have updated to use the URL fragment/hash like you suggested. I believe this has the advantage that the information is never sent across the wire i.e. it lives only in the URL bar, which is nice.

I have removed the compression parameter like you suggested. The reason this was separate was so that the same version could conditionally compress e.g. when the data is over a certain size. If we do decide to do that it can just be represented by a different version number.

To reduce confusion, rather than keeping the version number as a separate parameter, it is prepended to the Uint8Array before it is base64 encoded. The share URL now looks like:

/playground#AKtWSsvM...Sqja0FAA

If you are happy, I would like to merge this, and address any other concerns/improvements in a future PR.

@jacobpake

jacobpake commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

I have added a share= prefix, to provide a bit of context if a user wonders why there is a long hash in their URL bar.

/playground#share=AKtWSsvM...Sqja0FAA

@antstei
antstei self-requested a review August 25, 2026 10:05
Comment thread src/lib/playground/lib/share/codec.ts Outdated
@@ -0,0 +1,96 @@
// The state is encoded in the URL hash fragment as:
// /playground#e215OiBvYmplY3QgaXMgdm...

@antstei antstei Aug 25, 2026

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.

/playground#share=AKtWSsvM...Sqja0FAA, since we added the share URL parameter.

Comment thread src/lib/playground/lib/share/codec.ts Outdated
Comment on lines +89 to +96
export const decode_state = async (
version: Version,
payload: Uint8Array<ArrayBuffer>,
) => {
const raw_bytes = await inflate_bytes(payload);
const object = bytes_to_object(raw_bytes);
return SharableState.parse(object);
};

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.

Maybe we can connect the supported versions, VERSIONS, with their decoder implementations through a decoder map:

const decoders = {
  0: async (
    payload: Uint8Array<ArrayBuffer>,
  ): Promise<SharableState> => {
    const raw_bytes = await inflate_bytes(payload);
    const object = bytes_to_object(raw_bytes);
    return SharableState.parse(object);
  },
} satisfies Record<
  Version,
  (payload: Uint8Array<ArrayBuffer>) => Promise<SharableState>
>;
export const decode_state = (
  version: Version,
  payload: Uint8Array<ArrayBuffer>,
) => {
  return decoders[version](payload);
};

The satisfies Record<Version, ...> constraint uses compile-time checking to ensure that the decoder map contains an entry for every version represented by the Version type:

  • VERSIONS: runtime list of versions accepted by the decoder.
  • Version: compile-time union derived from VERSIONS.
  • VERSION: version used for newly encoded links.
  • decoders: decoder implementation for every supported version represented by the Version type.
  • is_valid_version: runtime validation for the numeric byte read from the URL.
  • decode_state: dispatches to the decoder for the validated version.

Use a versioned decoder map with compile-time checks to ensure every
supported version has a corresponding decoder implementation.
@antstei
antstei merged commit 8128ed0 into playground Sep 1, 2026
2 checks passed
@jacobpake
jacobpake deleted the playground-sharable branch September 16, 2026 08:05
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.

2 participants