@runbound/config-browser 0.0.0-stage → 0.1.0-alpha.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ The coordinate `@runbound/config-browser` ships to npm.
4
+
5
+ `0.1.0-alpha.1` is the first release, under the one scheme every Runbound SDK
6
+ family uses: a single logical SemVer pre-release, written in each registry's
7
+ own syntax, under the Runbound Runtime and SDK Licence.
8
+
9
+ ## 0.1.0-alpha.1
10
+
11
+ The first release, an unsupported alpha. [The README](./README.md) says what
12
+ the package does and what state it is in.
13
+
14
+ - Cloud mode is not yet available: Runbound Cloud does not yet deliver
15
+ managed Config.
package/LICENSE ADDED
@@ -0,0 +1,161 @@
1
+ # Runbound Runtime and SDK Licence
2
+
3
+ Version 2026-10-19. Copyright Runbound Ltd. All rights reserved.
4
+ Licence identifier: LicenseRef-Runbound-Runtime-SDK.
5
+
6
+ ## 1. Scope
7
+
8
+ Runbound Ltd (company 17482194, England and Wales) grants this licence for the
9
+ Runbound Runtime adapters and Config and Telemetry SDKs supplied with this
10
+ text, including their Runbound-authored dependencies, source and documentation
11
+ (the Software). Contact: legal@runbound.io; 71-75 Shelton Street, Covent Garden,
12
+ London, United Kingdom, WC2H 9JQ; +44 20 4577 3939.
13
+
14
+ The Software is proprietary. This licence does not cover the Runbound CLI,
15
+ Cloud service, API or dashboard. Those have their own terms. Third-party
16
+ components retain their own licences and notices; this licence does not reduce
17
+ any rights those licences give you.
18
+
19
+ ## 2. Use in your applications
20
+
21
+ You may install, copy, run and integrate the Software to develop, test and
22
+ operate your own applications, including commercial applications, internal
23
+ tools, continuous integration and services you provide to your customers.
24
+ You may inspect and modify source supplied to you for that integration,
25
+ debugging and fixing defects, subject to section 4.
26
+
27
+ Standalone local capabilities documented by Runbound as free may be used
28
+ without an account, subscription, time limit or licence fee. Capabilities
29
+ that require Runbound Cloud may be used only with a valid entitlement to the
30
+ relevant Runbound service, including a free plan or trial when offered, and
31
+ within its terms and limits. Installing an SDK does not grant Cloud access or
32
+ make a paid service free. There is no separate SDK royalty or per-developer
33
+ licence fee for the use permitted here.
34
+
35
+ Your applications, data, policies and outputs remain yours. This licence does
36
+ not require you to disclose your application source, grant Runbound rights to
37
+ it, or license it under these terms. The industry you work in does not restrict
38
+ the application rights above.
39
+
40
+ ## 3. Shipping applications
41
+
42
+ You may distribute copies of the Software, including integration changes
43
+ permitted by section 2, as a dependency of your own applications. This includes
44
+ containers, deployment bundles, browser bundles, mobile and desktop apps, and
45
+ source-based dependencies where the language requires them. You may keep
46
+ internal dependency mirrors and give copies to contractors operating your
47
+ applications on your behalf.
48
+
49
+ Include this licence and the applicable third-party notices with distributed
50
+ copies, in your normal dependency-notices location. Keep ownership notices;
51
+ you need not display a Runbound logo or add an attribution to your product UI.
52
+ Identify modifications to the Software in its accompanying notices. Do not
53
+ represent a modified copy as an official or signed Runbound release.
54
+
55
+ Recipients may use and redistribute the embedded Software as part of that
56
+ application under this licence. They may not extract it for separate
57
+ redistribution. If an application connects to Runbound Cloud, that connection
58
+ must be covered by the appropriate Runbound service entitlement; distributing
59
+ an application does not transfer an account or grant a service entitlement.
60
+
61
+ You may fork Runbound's public source repositories and publish proposed changes
62
+ solely to contribute to Runbound under the repository's contribution terms.
63
+ Keep this licence and the applicable notices. A contribution fork does not
64
+ permit standalone product releases or a substitute service.
65
+
66
+ ## 4. Separate products and services
67
+
68
+ The permissions above do not allow you to distribute the Software, or a
69
+ modified or renamed version of it, as a standalone SDK, runtime, agent or
70
+ library. Nor may you use the Software's implementation to provide a replacement
71
+ for Runbound's runtime enforcement, configuration or telemetry service to
72
+ third parties. These activities require a separate written agreement with
73
+ Runbound. Wrapping or renaming the Software does not grant these rights.
74
+
75
+ This restriction concerns the Runbound implementation you copy or modify. It
76
+ does not restrict software you develop independently without copying that
77
+ implementation. Integrating the Software to protect your applications, retrieve
78
+ their configuration or collect their telemetry is permitted, including when
79
+ your applications themselves are commercial products or services.
80
+
81
+ Except for contribution forks permitted in section 3, do not sell, sublicense
82
+ or publicly mirror the Software separately from the application distribution
83
+ permitted in section 3. Do not remove licence or
84
+ ownership notices, bypass Cloud authorisation or entitlement checks, or claim
85
+ rights to Runbound's name or trademarks beyond truthful identification.
86
+ Nothing in this licence limits rights that applicable law does not allow us
87
+ to restrict, including applicable interoperability and security-research rights.
88
+
89
+ ## 5. No warranty
90
+
91
+ **THE SOFTWARE IS PROVIDED "AS IS", WITH ALL FAULTS. TO THE FULLEST EXTENT THE
92
+ LAW ALLOWS, RUNBOUND GIVES NO WARRANTY, CONDITION OR GUARANTEE ABOUT IT,
93
+ WHETHER EXPRESS OR IMPLIED. RUNBOUND EXCLUDES THE IMPLIED WARRANTY OF
94
+ MERCHANTABILITY AND ANY IMPLIED WARRANTY OR CONDITION OF SATISFACTORY QUALITY,
95
+ FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT. RUNBOUND DOES NOT PROMISE
96
+ THAT THE SOFTWARE IS FREE OF ERRORS OR VULNERABILITIES, OR THAT IT WILL PREVENT
97
+ OR DETECT EVERY THREAT.**
98
+
99
+ ## 6. Limitation of liability
100
+
101
+ **TO THE FULLEST EXTENT THE LAW ALLOWS, RUNBOUND IS NOT LIABLE, WHETHER IN
102
+ CONTRACT, TORT (INCLUDING NEGLIGENCE), UNDER STATUTE OR OTHERWISE, FOR ANY
103
+ INDIRECT, CONSEQUENTIAL, SPECIAL, INCIDENTAL OR PUNITIVE LOSS OR DAMAGES, OR
104
+ FOR ANY LOSS OF PROFIT, REVENUE, BUSINESS, GOODWILL OR ANTICIPATED SAVINGS,
105
+ LOSS OR CORRUPTION OF DATA, LOSS OF USE OF A SYSTEM, DOWNTIME OR BUSINESS
106
+ INTERRUPTION, OR THE COST OF RESTORING SYSTEMS OR DATA, ARISING OUT OF OR IN
107
+ CONNECTION WITH THE SOFTWARE OR THIS LICENCE, EVEN IF RUNBOUND WAS TOLD SUCH
108
+ LOSS WAS POSSIBLE. RUNBOUND'S TOTAL LIABILITY ARISING OUT OF OR IN CONNECTION
109
+ WITH THE SOFTWARE OR THIS LICENCE IS LIMITED TO GBP 100.**
110
+
111
+ This licence charges no fee for the Software, and these limits reflect that.
112
+ Sections 5 and 6 do not exclude or limit liability for death or personal
113
+ injury caused by negligence, fraud or fraudulent misrepresentation, wilful
114
+ misconduct or gross negligence, liability under mandatory product-liability
115
+ law, or any other liability that cannot lawfully be excluded or limited. They
116
+ do not reduce mandatory consumer rights. When you use the Software with a paid
117
+ Runbound service, the agreement for that service governs the service and
118
+ Runbound's liability for it, and sections 5 and 6 do not reduce the rights it
119
+ gives you.
120
+
121
+ ## 7. Your applications and policies
122
+
123
+ You decide where to run the Software and which policies and configuration to
124
+ apply. An enforcing policy can block an operation, file access, process or
125
+ network connection in your application, and a configuration change can alter
126
+ how your application behaves. Either can stop a request, a job or the
127
+ application itself. A policy is enforced only on the runtimes, platforms and
128
+ modes that the documentation describes as supported. Elsewhere it may run
129
+ degraded or not apply.
130
+
131
+ Before you rely on the Software in production, test it, your policies and your
132
+ configuration outside production. Follow the documented rollout and rollback
133
+ steps, keep backups, and keep a way to operate and recover your application
134
+ that does not depend on Runbound. You are responsible for the policies and
135
+ configuration you choose, for checking compatibility with your applications
136
+ and for the effect of enforcement on them.
137
+
138
+ ## 8. Duration and changes
139
+
140
+ The licence supplied with a release governs that release. Later terms do not
141
+ retroactively change it. Whether a standalone capability is free is determined
142
+ by the documentation supplied for that release; later documentation does not
143
+ withdraw that permission. The permissions for free standalone use and embedded
144
+ application distribution have no expiry while you comply with this licence.
145
+ Cloud access remains subject to the service agreement and entitlement. Ending
146
+ Cloud access does not end the permissions for free standalone capabilities.
147
+
148
+ If you materially breach this licence, Runbound may terminate your licence
149
+ only after written notice describing the breach and 30 days to remedy it,
150
+ unless it cannot be remedied. Termination does not remove mandatory legal
151
+ rights or the rights of recipients who remain in compliance.
152
+
153
+ This licence does not withdraw rights already validly granted under another
154
+ licence for an earlier copy, including an open-source licence. It grants no
155
+ rights in software or third-party contributions that Runbound cannot license.
156
+
157
+ ## 9. Law
158
+
159
+ The law of England and Wales governs this licence. Consumers retain the
160
+ mandatory protections of the country where they live and may bring claims in
161
+ its courts. There is no mandatory arbitration or class-action waiver.
package/README.md CHANGED
@@ -1,3 +1,137 @@
1
- # Temporary Holding Version
1
+ # @runbound/config-browser
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Read typed configuration in a web page from one immutable snapshot, react to
4
+ changes through subscriptions, and pin a version for the length of a render or
5
+ user flow. It works with any framework, or none.
6
+
7
+ ## About Runbound
8
+
9
+ Runbound runs workloads under policy and records what happened as evidence.
10
+ This package reads validated configuration in the browser.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @runbound/config-browser@alpha
16
+ ```
17
+
18
+ It ships ES modules and TypeScript declarations for your application's bundler.
19
+
20
+ ## Alpha limits
21
+
22
+ `0.1.0-alpha.1` is an early, unsupported alpha. APIs may change between alpha
23
+ releases.
24
+
25
+ - **Browsers and Node.js.** Tested in Chromium, Firefox and WebKit, and in
26
+ Node.js 22 and later, which is enough to try it without a page.
27
+ - **Cloud mode is not yet available.** Runbound Cloud does not yet deliver
28
+ managed Config, so the refresh controller has no origin to poll. Create the
29
+ client from a snapshot your application supplies, as below, and commit
30
+ updates you obtain yourself.
31
+
32
+ ## Quickstart
33
+
34
+ This runs entirely in your process and needs no account or network. Save it as
35
+ `first-value.mjs`:
36
+
37
+ ```js
38
+ import { ConfigDeliveryClient } from "@runbound/config-browser";
39
+
40
+ const client = new ConfigDeliveryClient({
41
+ activation: { digest: "sha256:activation-1", sequence: 1 },
42
+ contextRevision: 1,
43
+ identity: {
44
+ clientId: "storefront-web",
45
+ contractDigest: "sha256:storefront-contract",
46
+ publicAudience: "storefront-public",
47
+ softwareReleaseId: "storefront-2026-10-19",
48
+ },
49
+ lifecycle: {
50
+ activeSource: "current",
51
+ compatibility: "compatible",
52
+ freshness: "fresh",
53
+ pendingLive: false,
54
+ readiness: "ready",
55
+ refresh: "idle",
56
+ restartRequired: false,
57
+ },
58
+ values: { checkout: { label: "Buy" } },
59
+ });
60
+ const label = (snapshot) => snapshot.values.checkout.label;
61
+
62
+ const session = client.openSession({
63
+ sessionId: "checkout-render-1",
64
+ openedAt: "2026-10-19T12:00:00.000Z",
65
+ expiresAt: "2026-10-19T12:05:00.000Z",
66
+ });
67
+ client.subscribeSelector(label, () => {
68
+ console.log(`label changed: ${label(client.getSnapshot())}`);
69
+ });
70
+ console.log(`label: ${label(client.getSnapshot())}`);
71
+
72
+ client.commit("activation", {
73
+ ...client.getSnapshot(),
74
+ activation: { digest: "sha256:activation-2", sequence: 2 },
75
+ values: { checkout: { label: "Buy now" } },
76
+ });
77
+ console.log(`pinned session: ${session.read(label, "2026-10-19T12:01:00.000Z")}`);
78
+ ```
79
+
80
+ ```sh
81
+ node first-value.mjs
82
+ ```
83
+
84
+ Expected output:
85
+
86
+ ```text
87
+ label: Buy
88
+ label changed: Buy now
89
+ pinned session: Buy
90
+ ```
91
+
92
+ The subscriber runs only when its selected value changes, and the session
93
+ keeps the activation it opened with, so one render never mixes two versions.
94
+
95
+ ## Read and update configuration
96
+
97
+ | API | Purpose |
98
+ | --- | --- |
99
+ | `new ConfigDeliveryClient(snapshot)` | The store: one validated, immutable snapshot at a time. |
100
+ | `client.getSnapshot()`, `client.subscribe(listener)` | Read the current snapshot; hear about every commit. |
101
+ | `client.subscribeSelector(selector, listener, equality?)` | Hear only when a selected value changes. |
102
+ | `client.commit(kind, snapshot)` | Swap in a new `activation`, `context` or `status`; a commit that moves backwards or changes identity throws `ConfigDeliveryStoreError`. |
103
+ | `client.openSession({ sessionId, openedAt, expiresAt })` | Pin the current snapshot; `session.read(selector, now)` fails once it expires or closes. |
104
+ | `ConfigBrowserRefreshController` (`/config-refresh`) | Polls a managed delivery origin through a transport and verifier you supply. |
105
+ | `/config-context`, `/config-context-client` | Build and apply redacted evaluation-context revisions. |
106
+ | `/config-experimentation`, `/config-experiment-committed-render` | Record experiment assignments and release a variant only after its exposure is durable. |
107
+
108
+ The root entry exports all of these; the subpaths let a bundler load less.
109
+
110
+ ## Network access
111
+
112
+ This package makes no requests of its own. The client and sessions work
113
+ entirely in memory. `ConfigBrowserRefreshController` calls only the `fetch`
114
+ of the transport object you pass it, with `credentials: "omit"`, and accepts
115
+ only Runbound's managed public origins, `https://config.runbound.io` and
116
+ `https://config.eu.runbound.io`, as its delivery origin; anything else fails
117
+ with `CONFIG_BROWSER_REFRESH_ORIGIN_INVALID` before any request. Responses
118
+ must carry your page's exact origin in `access-control-allow-origin`.
119
+ Experiment records leave the page only through the durable queue you supply.
120
+
121
+ ## What it does not do
122
+
123
+ Everything a page holds is visible to its user: values, locally evaluated
124
+ rules and context. Treat them as presentation decisions and keep
125
+ authorization and sensitive rules on a trusted server. The client stores what
126
+ you commit; it is not signature verification, so verify remotely delivered
127
+ data before committing it. It keeps no disk cache and holds no credentials.
128
+
129
+ ## Upgrade and uninstall
130
+
131
+ Pin the exact version; a page runs a new version when it next loads your
132
+ updated bundle. To uninstall, stop any refresh controller, close open
133
+ sessions, unsubscribe listeners and remove the package.
134
+
135
+ Source: [runbound-io/config](https://github.com/runbound-io/config/tree/main/sdks/browser).
136
+ Runbound software is proprietary under the Runbound Runtime and SDK Licence in this package's `LICENSE` file. The licence permits use in your own commercial and internal applications and distribution as an application dependency; it does not permit standalone redistribution. Cloud-required features need the appropriate Runbound service entitlement. Source is intended to be public for inspection and contributions through GitHub issues and pull requests; follow the repository contribution terms. Bundled third-party software is listed in
137
+ `THIRD_PARTY_NOTICES.md` and changes in `CHANGELOG.md`.
@@ -0,0 +1,30 @@
1
+ # Third-party notices
2
+
3
+ `@runbound/config-browser` bundles the third-party software below into its published JavaScript. Each entry gives the package, its licence, and the licence text, copyright and notices it is distributed under.
4
+
5
+ ## ipaddr.js 2.4.0
6
+
7
+ - Licence: MIT
8
+ - Source: https://github.com/whitequark/ipaddr.js
9
+
10
+ ~~~text
11
+ Copyright (C) 2011-2017 whitequark <whitequark@whitequark.org>
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in
21
+ all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
+ THE SOFTWARE.
30
+ ~~~
@@ -0,0 +1,74 @@
1
+ import { type ConfigBrowserContextContract, type ConfigBrowserContextRevision, type ConfigBrowserContextScope } from "./config-context.js";
2
+ import { ConfigDeliveryClient, type ConfigDeliverySnapshot, type ConfigDeliveryValues } from "./config-delivery-store.js";
3
+ export type ConfigBrowserContextResolution<Values extends ConfigDeliveryValues> = Readonly<{
4
+ resolutionDigest: string;
5
+ values: Values;
6
+ }>;
7
+ export interface ConfigBrowserContextResolver<Values extends ConfigDeliveryValues> {
8
+ resolve(input: Readonly<{
9
+ activation: ConfigDeliverySnapshot<Values>["activation"];
10
+ context: ConfigBrowserContextRevision;
11
+ previousContext?: ConfigBrowserContextRevision;
12
+ }>): Promise<ConfigBrowserContextResolution<Values>>;
13
+ }
14
+ export type ConfigBrowserContextPartitionTransition = Readonly<{
15
+ fromIdentityGeneration?: number;
16
+ fromPartitionKey?: string;
17
+ reason: "identity-transition" | "scope-transition";
18
+ resolutionDigest: string;
19
+ toContextDigest: string;
20
+ toIdentityGeneration: number;
21
+ toPartitionKey: string;
22
+ toRevision: number;
23
+ }>;
24
+ export interface ConfigBrowserContextAtomicPartitionStore<Values extends ConfigDeliveryValues> {
25
+ /**
26
+ * Atomically seals the prior event/cache partition, activates the new
27
+ * partition, and invokes commit exactly once. Events retain their original
28
+ * partition provenance and are never relabelled or merged.
29
+ */
30
+ commitTransition(transition: ConfigBrowserContextPartitionTransition, commit: () => ConfigDeliverySnapshot<Values>): Promise<Readonly<{
31
+ committed: true;
32
+ contextDigest: string;
33
+ partitionKey: string;
34
+ revision: number;
35
+ }>>;
36
+ }
37
+ export declare class ConfigBrowserContextClientError extends Error {
38
+ readonly code: string;
39
+ constructor(code: string, message: string);
40
+ }
41
+ export declare class ConfigBrowserContextClient<Values extends ConfigDeliveryValues> {
42
+ #private;
43
+ constructor(input: Readonly<{
44
+ client: ConfigDeliveryClient<Values>;
45
+ contract: ConfigBrowserContextContract;
46
+ initialContext?: ConfigBrowserContextRevision;
47
+ partitions: ConfigBrowserContextAtomicPartitionStore<Values>;
48
+ resolver: ConfigBrowserContextResolver<Values>;
49
+ }>);
50
+ get context(): ConfigBrowserContextRevision | undefined;
51
+ get diagnostic(): Readonly<{
52
+ contextContractDigest: string;
53
+ initialized: false;
54
+ valuesIncluded: false;
55
+ }> | {
56
+ contextContractDigest: string;
57
+ contextDigest: string;
58
+ identityGeneration: number;
59
+ keys: {
60
+ key: string;
61
+ sensitive: boolean;
62
+ source: "scope" | "trusted";
63
+ valueIncluded: false;
64
+ }[];
65
+ partitionKey: string;
66
+ revision: number;
67
+ subjectIdentityIncluded: false;
68
+ };
69
+ transition(input: Readonly<{
70
+ scopes: readonly ConfigBrowserContextScope[];
71
+ subjectIdentityToken: string;
72
+ trustedValues: Readonly<Record<string, boolean | number | string>>;
73
+ }>): Promise<ConfigDeliverySnapshot<Values>>;
74
+ }
@@ -0,0 +1 @@
1
+ import*as t from"./config-context.js";import*as e from"./config-delivery-store.js";var i={50(e){e.exports=t},815(t){t.exports=e}},n={};function o(t){var e=n[t];if(void 0!==e)return e.exports;var r=n[t]={exports:{}};return i[t](r,r.exports,o),r.exports}o.d=(t,e,i)=>{var n=(e,i)=>{for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,[i]:e[n]})};n(e,"get"),n(i,"value")},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var r={},s=o(50),a=o(815);class c extends Error{code;constructor(t,e){super(e),this.code=t,this.name="ConfigBrowserContextClientError"}}class l{#t;#e;#i;#n;#o;#r=0;constructor(t){this.#t=t.client,this.#e=structuredClone(t.contract),this.#i=t.partitions,this.#n=t.resolver,t.initialContext?((t.initialContext.contextContractDigest!==t.contract.contextContractDigest||t.initialContext.revision!==t.client.getSnapshot().contextRevision)&&x("CONFIG_BROWSER_CONTEXT_INITIAL_MISMATCH","Initial context does not match the delivery client and contract."),this.#o=t.initialContext):0!==t.client.getSnapshot().contextRevision&&x("CONFIG_BROWSER_CONTEXT_INITIAL_REQUIRED","A client with an existing context revision requires its exact context.")}get context(){return this.#o}get diagnostic(){return this.#o?(0,s.configBrowserContextRedactedDiagnostic)(this.#o):Object.freeze({contextContractDigest:this.#e.contextContractDigest,initialized:!1,valuesIncluded:!1})}async transition(t){var e;this.#r+=1;let i=this.#r,n=this.#o,o=this.#t.getSnapshot(),r=(0,s.createConfigBrowserContextRevision)({contract:this.#e,...n?{previous:n}:{},scopes:t.scopes,subjectIdentityToken:t.subjectIdentityToken,trustedValues:t.trustedValues}),c=await this.#n.resolve({activation:o.activation,context:r,...n?{previousContext:n}:{}});e=c.resolutionDigest,/^sha256:[0-9a-f]{64}$/u.test(e)||x("CONFIG_BROWSER_CONTEXT_RESOLUTION_INVALID","resolutionDigest is invalid."),i!==this.#r&&x("CONFIG_BROWSER_CONTEXT_TRANSITION_SUPERSEDED","A newer context transition superseded this resolution.");let l={...o,contextRevision:r.revision,values:c.values};new a.ConfigDeliveryClient(o).commit("context",l);let C=0,h=await this.#i.commitTransition({...n?{fromIdentityGeneration:n.identityGeneration,fromPartitionKey:n.partitionKey}:{},reason:n?.identityGeneration===r.identityGeneration?"scope-transition":"identity-transition",resolutionDigest:c.resolutionDigest,toContextDigest:r.contextDigest,toIdentityGeneration:r.identityGeneration,toPartitionKey:r.partitionKey,toRevision:r.revision},()=>{1!==(C+=1)&&x("CONFIG_BROWSER_CONTEXT_COMMIT_DUPLICATE","Context partition transaction invoked its commit more than once."),(i!==this.#r||this.#t.getSnapshot()!==o||this.#o!==n)&&x("CONFIG_BROWSER_CONTEXT_TRANSITION_CONFLICT","Context or activation changed during the atomic transition.");let t=this.#t.commit("context",l);return this.#o=r,t});return(1!==C||!0!==h.committed||h.contextDigest!==r.contextDigest||h.partitionKey!==r.partitionKey||h.revision!==r.revision)&&x("CONFIG_BROWSER_CONTEXT_PARTITION_COMMIT_INVALID","Context partition transaction did not acknowledge the exact revision."),this.#t.getSnapshot()}}function x(t,e){throw new c(t,e)}o.d(r,{X:()=>l,n:()=>c});var C=r.X,h=r.n;export{C as ConfigBrowserContextClient,h as ConfigBrowserContextClientError};
@@ -0,0 +1,55 @@
1
+ export type ConfigBrowserContextContract = Readonly<{
2
+ attributes: readonly Readonly<{
3
+ clientWritable: boolean;
4
+ key: string;
5
+ maximumLength?: number;
6
+ sensitive: boolean;
7
+ type: "boolean" | "number" | "string";
8
+ }>[];
9
+ contextContractDigest: string;
10
+ trustedAttributeKeys: readonly string[];
11
+ }>;
12
+ export type ConfigBrowserContextScope = Readonly<{
13
+ scopeId: string;
14
+ values: Readonly<Record<string, boolean | number | string>>;
15
+ }>;
16
+ export type ConfigBrowserContextRevision = Readonly<{
17
+ contextContractDigest: string;
18
+ contextDigest: string;
19
+ explanation: readonly Readonly<{
20
+ key: string;
21
+ sensitive: boolean;
22
+ source: "scope" | "trusted";
23
+ }>[];
24
+ identityGeneration: number;
25
+ partitionKey: string;
26
+ revision: number;
27
+ scopeIds: readonly string[];
28
+ subjectIdentityTokenDigest: string;
29
+ values: Readonly<Record<string, boolean | number | string>>;
30
+ }>;
31
+ export declare class ConfigBrowserContextError extends Error {
32
+ readonly code: string;
33
+ constructor(code: string, message: string);
34
+ }
35
+ export declare function createConfigBrowserContextRevision(input: {
36
+ contract: ConfigBrowserContextContract;
37
+ previous?: ConfigBrowserContextRevision;
38
+ scopes: readonly ConfigBrowserContextScope[];
39
+ subjectIdentityToken: string;
40
+ trustedValues: Readonly<Record<string, boolean | number | string>>;
41
+ }): ConfigBrowserContextRevision;
42
+ export declare function configBrowserContextRedactedDiagnostic(revision: ConfigBrowserContextRevision): {
43
+ contextContractDigest: string;
44
+ contextDigest: string;
45
+ identityGeneration: number;
46
+ keys: {
47
+ key: string;
48
+ sensitive: boolean;
49
+ source: "scope" | "trusted";
50
+ valueIncluded: false;
51
+ }[];
52
+ partitionKey: string;
53
+ revision: number;
54
+ subjectIdentityIncluded: false;
55
+ };
@@ -0,0 +1 @@
1
+ import*as t from"@noble/hashes/sha2.js";import*as e from"@noble/hashes/utils.js";var n={961(e){e.exports=t},334(t){t.exports=e}},o={};function r(t){var e=o[t];if(void 0!==e)return e.exports;var s=o[t]={exports:{}};return n[t](s,s.exports,r),s.exports}r.d=(t,e,n)=>{var o=(e,n)=>{for(var o in e)r.o(e,o)&&!r.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,[n]:e[o]})};o(e,"get"),o(n,"value")},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var s={},i=r(961),a=r(334);class c extends Error{code;constructor(t,e){super(e),this.code=t,this.name="ConfigBrowserContextError"}}function u(t){let e=function(t){var e;e=t.contextContractDigest,/^sha256:[0-9a-f]{64}$/u.test(e)||d("CONFIG_BROWSER_CONTEXT_INPUT_INVALID","contextContractDigest is not a digest."),(0===t.attributes.length||t.attributes.length>256)&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Context contract requires one to 256 attributes.");let n=new Set;for(let e of t.attributes)O(e.key,"attribute.key",256),n.has(e.key)&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Context attribute keys must be unique."),n.add(e.key),void 0!==e.maximumLength&&(!Number.isSafeInteger(e.maximumLength)||e.maximumLength<1||e.maximumLength>4096)&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Context string bounds are invalid.");return(new Set(t.trustedAttributeKeys).size!==t.trustedAttributeKeys.length||t.trustedAttributeKeys.some(t=>!n.has(t)))&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Trusted context keys must be unique contract attributes."),l(structuredClone(t))}(t.contract);t.previous&&t.previous.contextContractDigest!==e.contextContractDigest&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_CHANGED","Context revisions cannot cross generated contract identity.");let n=p(O(t.subjectIdentityToken,"subjectIdentityToken",2048)),o=t.previous?.subjectIdentityTokenDigest===n?t.previous.identityGeneration:(t.previous?.identityGeneration??0)+1,r=new Map(e.attributes.map(t=>[t.key,t])),s=new Set(e.trustedAttributeKeys),i={},a=new Map,c=new Set;for(let e of(t.scopes.length>64&&d("CONFIG_BROWSER_CONTEXT_SCOPE_LIMIT","Context scope limit exceeded."),t.scopes))for(let[t,n]of(O(e.scopeId,"scopeId",256),c.has(e.scopeId)&&d("CONFIG_BROWSER_CONTEXT_SCOPE_DUPLICATE","Context scope IDs must be unique."),c.add(e.scopeId),Object.entries(e.values))){let e=r.get(t);(!e?.clientWritable||s.has(t))&&d("CONFIG_BROWSER_CONTEXT_SCOPE_FORBIDDEN","A context scope attempted to set an unknown or trusted attribute."),_(e,n),i[t]=n,a.set(t,"scope")}let u=Object.keys(t.trustedValues).sort();for(let e of((u.length!==s.size||u.some(t=>!s.has(t)))&&d("CONFIG_BROWSER_CONTEXT_TRUST_INCOMPLETE","Trusted context must provide every reserved attribute exactly once."),u)){let n=r.get(e);n||d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Trusted context key is absent from its contract.");let o=t.trustedValues[e];_(n,o),i[e]=o,a.set(e,"trusted")}let C=Object.fromEntries(Object.entries(i).sort(([t],[e])=>t.localeCompare(e))),T=p({contextContractDigest:e.contextContractDigest,identityGeneration:o,scopeIds:[...c],subjectIdentityTokenDigest:n,values:C}),x=(t.previous?.revision??0)+1;return l({contextContractDigest:e.contextContractDigest,contextDigest:T,explanation:Object.keys(C).map(t=>{let e=r.get(t),n=a.get(t);return e&&n||d("CONFIG_BROWSER_CONTEXT_STATE_INVALID","Context explanation lost its attribute lineage."),{key:t,sensitive:e.sensitive,source:n}}),identityGeneration:o,partitionKey:p({contextContractDigest:e.contextContractDigest,identityGeneration:o,subjectIdentityTokenDigest:n}),revision:x,scopeIds:[...c],subjectIdentityTokenDigest:n,values:C})}function C(t){return l({contextContractDigest:t.contextContractDigest,contextDigest:t.contextDigest,identityGeneration:t.identityGeneration,keys:t.explanation.map(({key:t,sensitive:e,source:n})=>({key:t,sensitive:e,source:n,valueIncluded:!1})),partitionKey:t.partitionKey,revision:t.revision,subjectIdentityIncluded:!1})}function _(t,e){(typeof e!==t.type||"number"==typeof e&&!Number.isFinite(e)||"string"==typeof e&&e.length>(t.maximumLength??4096))&&d("CONFIG_BROWSER_CONTEXT_VALUE_INVALID",`Context attribute ${t.key} has an invalid value.`)}function O(t,e,n){return("string"!=typeof t||t.trim()!==t||0===t.length||t.length>n)&&d("CONFIG_BROWSER_CONTEXT_INPUT_INVALID",`${e} is invalid.`),t}function p(t){let e=new TextEncoder().encode(function t(e){return null===e||"boolean"==typeof e||"string"==typeof e||"number"==typeof e&&Number.isFinite(e)?JSON.stringify(e):Array.isArray(e)?`[${e.map(e=>t(e)).join(",")}]`:e&&"object"==typeof e?`{${Object.entries(e).sort(([t],[e])=>t.localeCompare(e)).map(([e,n])=>`${JSON.stringify(e)}:${t(n)}`).join(",")}}`:void d("CONFIG_BROWSER_CONTEXT_STATE_INVALID","Context digest input is not canonical JSON.")}(t));return`sha256:${(0,a.bytesToHex)((0,i.sha256)(e))}`}function d(t,e){throw new c(t,e)}function l(t){if(t&&"object"==typeof t&&!Object.isFrozen(t))for(let e of(Object.freeze(t),Object.values(t)))l(e);return t}r.d(s,{HR:()=>u,_B:()=>c,xn:()=>C});var T=s._B,x=s.xn,N=s.HR;export{T as ConfigBrowserContextError,x as configBrowserContextRedactedDiagnostic,N as createConfigBrowserContextRevision};
@@ -0,0 +1,73 @@
1
+ import type { JsonValue } from "./internal/config-contract/core.js";
2
+ export type ConfigDeliveryValues = Readonly<Record<string, JsonValue>>;
3
+ export type ConfigDeliveryClientIdentity = Readonly<{
4
+ clientId: string;
5
+ contractDigest: string;
6
+ publicAudience: string;
7
+ softwareReleaseId: string;
8
+ }>;
9
+ export type ConfigDeliveryLifecycle = Readonly<{
10
+ activeSource: "current" | "defaults" | "lastKnownGood" | "pinnedOffline";
11
+ compatibility: "compatible" | "incompatible";
12
+ freshness: "expired" | "fresh" | "stale";
13
+ pendingLive: boolean;
14
+ readiness: "error" | "initializing" | "ready";
15
+ reason?: Readonly<{
16
+ code: string;
17
+ message: string;
18
+ }>;
19
+ refresh: "failed" | "idle" | "refreshing";
20
+ restartRequired: boolean;
21
+ }>;
22
+ export type ConfigDeliverySnapshot<Values extends ConfigDeliveryValues> = Readonly<{
23
+ activation: Readonly<{
24
+ digest: string;
25
+ sequence: number;
26
+ }>;
27
+ contextRevision: number;
28
+ identity: ConfigDeliveryClientIdentity;
29
+ lifecycle: ConfigDeliveryLifecycle;
30
+ values: Values;
31
+ }>;
32
+ export type ConfigDeliveryCommitKind = "activation" | "context" | "status";
33
+ export type ConfigDeliveryEquality<Selected> = (left: Selected, right: Selected) => boolean;
34
+ export type ConfigDeliverySelector<Values extends ConfigDeliveryValues, Selected> = (snapshot: ConfigDeliverySnapshot<Values>) => Selected;
35
+ export declare class ConfigDeliveryStoreError extends Error {
36
+ readonly code: string;
37
+ constructor(code: string, message: string);
38
+ }
39
+ export declare class ConfigDeliveryPinnedSession<Values extends ConfigDeliveryValues = ConfigDeliveryValues> {
40
+ #private;
41
+ readonly clientId: string;
42
+ readonly expiresAt: string;
43
+ readonly openedAt: string;
44
+ readonly sessionId: string;
45
+ readonly snapshot: ConfigDeliverySnapshot<Values>;
46
+ constructor(input: {
47
+ expiresAt: string;
48
+ openedAt: string;
49
+ sessionId: string;
50
+ snapshot: ConfigDeliverySnapshot<Values>;
51
+ });
52
+ close(): void;
53
+ read<Selected>(selector: ConfigDeliverySelector<Values, Selected>, now?: string): Selected;
54
+ assertUsable(now: string): void;
55
+ }
56
+ /**
57
+ * Framework-neutral public Config store. Candidates are normalized off the
58
+ * read path and become visible through one immutable pointer swap.
59
+ */
60
+ export declare class ConfigDeliveryClient<Values extends ConfigDeliveryValues = ConfigDeliveryValues> {
61
+ #private;
62
+ constructor(initialSnapshot: ConfigDeliverySnapshot<Values>);
63
+ get identity(): ConfigDeliveryClientIdentity;
64
+ getSnapshot: () => ConfigDeliverySnapshot<Values>;
65
+ subscribe: (listener: () => void) => (() => void);
66
+ subscribeSelector<Selected>(selector: ConfigDeliverySelector<Values, Selected>, listener: () => void, equality?: ConfigDeliveryEquality<Selected>): () => void;
67
+ commit(kind: ConfigDeliveryCommitKind, candidate: ConfigDeliverySnapshot<Values>): ConfigDeliverySnapshot<Values>;
68
+ openSession(input: {
69
+ expiresAt: string;
70
+ openedAt: string;
71
+ sessionId: string;
72
+ }): ConfigDeliveryPinnedSession<Values>;
73
+ }
@@ -0,0 +1 @@
1
+ var e={};e.d=(t,n,i)=>{var s=(n,i)=>{for(var s in n)e.o(n,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,[i]:n[s]})};s(n,"get"),s(i,"value")},e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var t={};class n extends Error{code;constructor(e,t){super(t),this.code=e,this.name="ConfigDeliveryStoreError"}}class i{clientId;expiresAt;openedAt;sessionId;snapshot;#e=!1;constructor(e){if(d(e.openedAt,"openedAt"),d(e.expiresAt,"expiresAt"),Date.parse(e.expiresAt)<=Date.parse(e.openedAt))throw new n("CONFIG_DELIVERY_SESSION_EXPIRY_INVALID","A pinned Config render session must expire after it opens.");u(e.sessionId,"sessionId"),this.clientId=e.snapshot.identity.clientId,this.expiresAt=e.expiresAt,this.openedAt=e.openedAt,this.sessionId=e.sessionId,this.snapshot=e.snapshot}close(){this.#e=!0}read(e,t=new Date().toISOString()){return this.assertUsable(t),e(this.snapshot)}assertUsable(e){if(d(e,"now"),this.#e)throw new n("CONFIG_DELIVERY_SESSION_CLOSED","The pinned Config render session is closed.");if(Date.parse(e)>=Date.parse(this.expiresAt))throw new n("CONFIG_DELIVERY_SESSION_EXPIRED","The pinned Config render session exceeded its declared maximum age.")}}class s{#t=new Set;#n;constructor(e){this.#n=o(void 0,e)}get identity(){return this.#n.identity}getSnapshot=()=>this.#n;subscribe=e=>(this.#t.add(e),()=>this.#t.delete(e));subscribeSelector(e,t,n=Object.is){let i=e(this.#n);return this.subscribe(()=>{let s=e(this.#n);n(i,s)||(i=s,t())})}commit(e,t){!function(e,t,i){var s,o;if(s=t.identity,o=i.identity,s.clientId!==o.clientId||s.contractDigest!==o.contractDigest||s.publicAudience!==o.publicAudience||s.softwareReleaseId!==o.softwareReleaseId)throw new n("CONFIG_DELIVERY_CLIENT_IDENTITY_CHANGED","A Config client cannot change contract, audience, software, or client identity.");if("activation"===e){if(i.activation.sequence<=t.activation.sequence||i.contextRevision!==t.contextRevision)throw new n("CONFIG_DELIVERY_ACTIVATION_SEQUENCE_INVALID","An activation commit must advance the sequence without changing context revision.");return}if(i.activation.sequence!==t.activation.sequence||i.activation.digest!==t.activation.digest)throw new n("CONFIG_DELIVERY_PINNED_ACTIVATION_CHANGED","Context and status commits cannot replace the active Config activation.");if("context"===e){if(i.contextRevision<=t.contextRevision)throw new n("CONFIG_DELIVERY_CONTEXT_REVISION_INVALID","A context commit must advance its revision atomically.");return}if(i.contextRevision!==t.contextRevision)throw new n("CONFIG_DELIVERY_STATUS_CONTEXT_CHANGED","A lifecycle-only commit cannot change context revision.");if(c(t.values,i.values)!==t.values)throw new n("CONFIG_DELIVERY_STATUS_VALUES_CHANGED","A lifecycle-only commit cannot change Config values.")}(e,this.#n,t);let i=o(this.#n,t);for(let e of(this.#n=i,[...this.#t]))e();return i}openSession(e){return new i({...e,snapshot:this.#n})}}function o(e,t){(function(e,t){if(!Number.isSafeInteger(e)||e<1)throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be a positive safe integer.`)})(t.activation.sequence,"activation.sequence"),u(t.activation.digest,"activation.digest"),function(e,t){if(!Number.isSafeInteger(e)||e<0)throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be a non-negative safe integer.`)}(t.contextRevision,"contextRevision"),function(e){for(let[t,n]of Object.entries(e))u(n,`identity.${t}`)}(t.identity),function(e){if(!["current","defaults","lastKnownGood","pinnedOffline"].includes(e.activeSource)||!["compatible","incompatible"].includes(e.compatibility)||!["expired","fresh","stale"].includes(e.freshness)||!["error","initializing","ready"].includes(e.readiness)||!["failed","idle","refreshing"].includes(e.refresh)||"boolean"!=typeof e.pendingLive||"boolean"!=typeof e.restartRequired)throw new n("CONFIG_DELIVERY_LIFECYCLE_INVALID","Config lifecycle state must use the closed readiness, source, freshness, refresh, compatibility, and restart contract.");e.reason&&(u(e.reason.code,"lifecycle.reason.code"),u(e.reason.message,"lifecycle.reason.message"))}(t.lifecycle);let i=c(e?.values,t.values);return Object.freeze({activation:r(e?.activation,t.activation),contextRevision:t.contextRevision,identity:r(e?.identity,t.identity),lifecycle:function(e,t){if(!e)return f(t);let{reason:n,...i}=e,{reason:s,...o}=t;return(void 0===n||void 0===s?n===s:a(n,s))&&"reason"in e=="reason"in t&&a(i,o)?e:f(t)}(e?.lifecycle,t.lifecycle),values:i})}function r(e,t){return e&&a(e,t)?e:Object.freeze({...t})}function a(e,t){let n=Object.keys(t);if(Object.keys(e).length!==n.length)return!1;for(let i of n)if(!Object.hasOwn(e,i)||!Object.is(e[i],t[i]))return!1;return!0}function c(e,t){if(null===t||"boolean"==typeof t||"string"==typeof t)return Object.is(e,t)?e:t;if("number"==typeof t){if(!Number.isFinite(t))throw l();return Object.is(e,t)?e:t}if(Array.isArray(t)){let n=Array.isArray(e)?e:[],i=t.map((e,t)=>c(n[t],e));return Array.isArray(e)&&e.length===i.length&&i.every((t,n)=>t===e[n])?e:Object.freeze(i)}if(t&&"object"==typeof t){let n=Object.getPrototypeOf(t);if(n!==Object.prototype&&null!==n)throw l();let i=e&&"object"==typeof e&&!Array.isArray(e)?e:{},s=Object.keys(t).sort(),o=Object.fromEntries(s.map(e=>[e,c(i[e],t[e])]));return Object.keys(i).length===s.length&&s.every(e=>o[e]===i[e])?e:Object.freeze(o)}throw l()}function l(){return new n("CONFIG_DELIVERY_VALUES_INVALID","Public Config snapshots must contain only finite JSON values.")}function f(e){return Object.freeze({...e,...e.reason?{reason:Object.freeze({...e.reason})}:{}})}function u(e,t){if("string"!=typeof e||0===e.trim().length||e.length>1024)throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be a bounded non-empty value.`)}function d(e,t){if(!Number.isFinite(Date.parse(e)))throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be an ISO timestamp.`)}e.d(t,{D6:()=>i,Kv:()=>n,cH:()=>s});var h=t.cH,I=t.D6,p=t.Kv;export{h as ConfigDeliveryClient,I as ConfigDeliveryPinnedSession,p as ConfigDeliveryStoreError};
@@ -0,0 +1,57 @@
1
+ import type { BrowserExperimentAssignmentReceipt } from "./config-experimentation.js";
2
+ export type BrowserExperimentRenderKind = "client" | "fallback" | "ssr";
3
+ export type BrowserExperimentCommittedRenderCandidate = Readonly<{
4
+ assignment: BrowserExperimentAssignmentReceipt;
5
+ deduplicationKey: string;
6
+ exposureBoundaryId: string;
7
+ exclusionReason?: "fallback-render" | "server-render";
8
+ renderAttemptId: string;
9
+ renderKind: BrowserExperimentRenderKind;
10
+ sessionId: string;
11
+ }>;
12
+ export interface BrowserExperimentCommittedRenderStore {
13
+ claim(deduplicationKey: string): Promise<"claimed" | "duplicate">;
14
+ complete(deduplicationKey: string): Promise<void>;
15
+ release(deduplicationKey: string): Promise<void>;
16
+ }
17
+ export type BrowserExperimentCommittedRenderResult<Result> = Readonly<{
18
+ reason: "abandoned-render" | "fallback-render" | "server-render";
19
+ status: "excluded";
20
+ }> | Readonly<{
21
+ status: "duplicate";
22
+ }> | Readonly<{
23
+ exposure: Result;
24
+ status: "exposed";
25
+ }>;
26
+ /**
27
+ * Framework-neutral committed-render boundary. React adapters may prepare
28
+ * during render, abandon from discarded work, and commit from the post-commit
29
+ * lifecycle. The injected store owns cross-instance StrictMode/remount dedupe.
30
+ *
31
+ * Attempt state is keyed weakly by the candidate object, and only live
32
+ * (prepared or committing) attempt identifiers are retained. Settled,
33
+ * excluded and abandoned attempts keep their answers for whoever still holds
34
+ * the candidate, but the controller itself no longer keeps candidates or
35
+ * assignments reachable after their render lifecycle ends; a strong per-attempt
36
+ * map previously grew with every mount for the controller's lifetime.
37
+ */
38
+ export declare class BrowserExperimentCommittedRenderController {
39
+ #private;
40
+ constructor(store: BrowserExperimentCommittedRenderStore);
41
+ prepare(input: {
42
+ assignment: BrowserExperimentAssignmentReceipt;
43
+ exposureBoundaryId: string;
44
+ renderAttemptId: string;
45
+ renderKind: BrowserExperimentRenderKind;
46
+ sessionId: string;
47
+ }): BrowserExperimentCommittedRenderCandidate;
48
+ abandon(candidate: BrowserExperimentCommittedRenderCandidate): void;
49
+ commit<Result>(candidate: BrowserExperimentCommittedRenderCandidate, expose: (assignment: BrowserExperimentAssignmentReceipt, exposureBoundaryId: string) => Promise<Result>): Promise<BrowserExperimentCommittedRenderResult<Result>>;
50
+ }
51
+ /** Test/reference store only; browser adapters inject persistent storage. */
52
+ export declare class MemoryBrowserExperimentCommittedRenderStore implements BrowserExperimentCommittedRenderStore {
53
+ #private;
54
+ claim(deduplicationKey: string): Promise<"claimed" | "duplicate">;
55
+ complete(deduplicationKey: string): Promise<void>;
56
+ release(deduplicationKey: string): Promise<void>;
57
+ }
@@ -0,0 +1 @@
1
+ var e={};e.d=(t,r,s)=>{var i=(r,s)=>{for(var i in r)e.o(r,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,[s]:r[i]})};i(r,"get"),i(s,"value")},e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var t={};class r{#e=new Set;#t=new WeakMap;#r=new WeakSet;#s;constructor(e){this.#s=e}prepare(e){if(i(e.exposureBoundaryId,"exposureBoundaryId"),i(e.renderAttemptId,"renderAttemptId"),i(e.sessionId,"sessionId"),this.#e.has(e.renderAttemptId))throw Error("A render attempt identifier cannot be prepared twice.");if(e.assignment.record.sessionId!==e.sessionId)throw Error("Committed-render exposure must remain in the durable assignment session.");let t="ssr"===e.renderKind?"server-render":"fallback"===e.renderKind?"fallback-render":void 0,r=Object.freeze({assignment:e.assignment,deduplicationKey:JSON.stringify([e.sessionId,e.assignment.semanticIdentity.runId,e.assignment.exposureToken,e.exposureBoundaryId]),exposureBoundaryId:e.exposureBoundaryId,...t?{exclusionReason:t}:{},renderAttemptId:e.renderAttemptId,renderKind:e.renderKind,sessionId:e.sessionId});return this.#t.set(r,"prepared"),this.#e.add(e.renderAttemptId),r}abandon(e){let t=this.#i(e);"committing"===t?this.#r.add(e):"settled"!==t&&"abandoned"!==t&&this.#n(e,"abandoned")}async commit(e,t){let r=this.#i(e);if(e.exclusionReason)return"prepared"===r&&this.#n(e,"settled"),Object.freeze({reason:e.exclusionReason,status:"excluded"});if("abandoned"===r)return Object.freeze({reason:"abandoned-render",status:"excluded"});if("committing"===r||"settled"===r)return Object.freeze({status:"duplicate"});this.#t.set(e,"committing");let s=!1,i=!1;try{let r=await this.#s.claim(e.deduplicationKey);if("duplicate"===r)return this.#n(e,"settled"),Object.freeze({status:"duplicate"});s=!0;let n=await t(e.assignment,e.exposureBoundaryId);return i=!0,await this.#s.complete(e.deduplicationKey),this.#n(e,"settled"),Object.freeze({exposure:n,status:"exposed"})}catch(t){if(i)throw this.#n(e,"settled"),t;try{s&&await this.#s.release(e.deduplicationKey)}finally{this.#r.has(e)?this.#n(e,"abandoned"):this.#t.set(e,"prepared")}throw t}}#n(e,t){this.#t.set(e,t),this.#r.delete(e),this.#e.delete(e.renderAttemptId)}#i(e){let t=this.#t.get(e);if(!t)throw Error("Committed-render candidates are valid only for their preparing controller.");return t}}class s{#d=new Map;async claim(e){return this.#d.has(e)?"duplicate":(this.#d.set(e,"claimed"),"claimed")}async complete(e){if("claimed"!==this.#d.get(e))throw Error("A committed-render claim must exist before completion.");this.#d.set(e,"completed")}async release(e){"claimed"===this.#d.get(e)&&this.#d.delete(e)}}function i(e,t){if("string"!=typeof e||0===e.trim().length||e.length>512)throw Error(`${t} must be a bounded non-empty identifier.`)}e.d(t,{e:()=>s,o:()=>r});var n=t.o,d=t.e;export{n as BrowserExperimentCommittedRenderController,d as MemoryBrowserExperimentCommittedRenderStore};