@posthog/browser-common 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -6,16 +6,17 @@ runtime, but it is not a public API surface and does not provide compatibility
6
6
  guarantees outside PostHog SDK packages.
7
7
 
8
8
  The shared extension contract includes the interface an extension implements
9
- (`Extension`), the host capabilities it is handed (`Client`), and small shared
10
- runtime primitives such as `Publisher`.
9
+ (`Extension`), the host services it is handed (`Client`), the core analytics
10
+ capability (`CoreExtension`), and small shared runtime primitives such as
11
+ `Publisher`.
11
12
 
12
- An extension written against this contract runs unchanged across major versions of the web SDK:
13
+ This contract is designed so an extension can run unchanged across major
14
+ versions of the web SDK. Concrete host adapters remain owned by their SDK
15
+ packages; browser-v1 and browser-v2 composition and loading integration are
16
+ separate from this shared runtime.
13
17
 
14
- - **v1** is synchronous; extensions are registered statically.
15
- - **v2** is asynchronous; extensions are loaded dynamically.
16
-
17
- Each SDK provides a _client adapter_ that implements `Client` over its own
18
- internals, so extension code never depends on a specific SDK.
18
+ A conforming SDK provides a _client adapter_ that implements `Client` over its
19
+ own internals, so extension code never depends on a specific SDK.
19
20
 
20
21
  ## Concepts
21
22
 
@@ -24,7 +25,7 @@ internals, so extension code never depends on a specific SDK.
24
25
  What you implement. The host calls only `setup` and `dispose`:
25
26
 
26
27
  ```ts
27
- import type { Disposable, Extension } from '@posthog/browser-common'
28
+ import { CoreExtension, type Disposable, type Extension } from '@posthog/browser-common'
28
29
 
29
30
  export function webContext(): Extension {
30
31
  let removeProperties: Disposable | undefined
@@ -32,7 +33,11 @@ export function webContext(): Extension {
32
33
  return {
33
34
  name: 'webContext',
34
35
  setup(client) {
35
- removeProperties = client.registerDynamicEventProperties(() => ({
36
+ const core = client.getExtension(CoreExtension)
37
+ if (!core) {
38
+ throw new Error('CoreExtension is required')
39
+ }
40
+ removeProperties = core.registerDynamicEventProperties(() => ({
36
41
  $current_url: window.location.href,
37
42
  }))
38
43
  },
@@ -48,23 +53,44 @@ may be async (final flush). Static config the app sets goes in your constructor,
48
53
  not on the `Client`.
49
54
 
50
55
  Anything in `setup` that returns a `Disposable` must be held by the extension
51
- and disposed in `dispose()`.
56
+ and disposed in `dispose()`. Use `createDisposable(teardown)` when adapting a
57
+ callback into idempotent teardown.
52
58
 
53
59
  ### `Client`
54
60
 
55
- What an extension is given in `setup` — the host's capability surface:
61
+ What an extension is given in `setup` — the host's extension services:
56
62
 
57
- - **identity & session** (synchronous reads): `distinctId`, `anonymousId`, `groups`, `session`
58
- - **events**: `capture(...)`, `registerDynamicEventProperties(...)` (contribute properties), `onEvent(...)` (observe)
59
- - **transport**: `apiRequest(path, init?)`
60
- - **server config**: `getRemoteConfig()` (current), `onRemoteConfig(...)` (changes)
61
- - **lifecycle**: `onNewSession(...)`
63
+ - **transport**: `projectToken`, `sendRequest(path, init?)`
62
64
  - **registry**: `getExtension(token)`
63
65
  - **storage & logging**: `kv`, `logger`
64
66
 
65
- Synchronous members are always-ready in-memory reads; everything that does I/O
66
- or waits for readiness (`capture`, `apiRequest`, `kv`, `getRemoteConfig`) is
67
- asynchronous.
67
+ ### `CoreExtension`
68
+
69
+ A conforming host must register one `CoreExtension` before setting up product
70
+ extensions. Resolve it through `client.getExtension(CoreExtension)` for behavior
71
+ owned by the PostHog client's analytics core:
72
+
73
+ - **identity & session**: `distinctId`, `anonymousId`, `groups`, `session`
74
+ - **events**: `capture(...)`, `registerDynamicEventProperties(...)`, `onEvent(...)`
75
+ - **lifecycle**: `onNewSession(...)`
76
+ - **server config**: `getRemoteConfig()` (current), `onRemoteConfig(...)` (changes)
77
+
78
+ Identity, session, and the public project token are always-ready synchronous
79
+ reads. Operations that perform I/O, including `capture`, `sendRequest`, `kv`,
80
+ and `getRemoteConfig`, are awaitable.
81
+
82
+ ### Host runtime
83
+
84
+ PostHog browser SDK implementations share extension registration and teardown
85
+ through `ExtensionRuntime`, imported from the dedicated
86
+ `@posthog/browser-common/extension-runtime` subpath. It reserves names and
87
+ capability tokens during setup, publishes providers only after successful
88
+ readiness, and disposes extensions once in reverse registration order. Concrete
89
+ SDKs still own the `Client` adapter, Core implementation, and SDK lifecycle
90
+ hooks.
91
+
92
+ `ExtensionRuntime` is host infrastructure, not part of the extension-author
93
+ surface exported from the package root.
68
94
 
69
95
  ### `Publisher`
70
96
 
@@ -127,7 +153,5 @@ v1 → `Client` porting map.
127
153
  ## Status
128
154
 
129
155
  Early and internal. The package currently defines the extension contract, the
130
- shared `Publisher` helper, and directly imported browser utilities under
131
- `utils/*` subpaths. Additional shared runtime helpers — key-value stores, the
132
- registry implementation, and a test `Client` — will land alongside the first
133
- ported extension.
156
+ core analytics capability, a shared host runtime, shared lifecycle helpers, and
157
+ directly imported browser utilities under `utils/*` subpaths.
package/dist/client.d.ts CHANGED
@@ -1,135 +1,62 @@
1
1
  import type { Logger } from '@posthog/core';
2
- import type { Disposable } from './disposable';
3
2
  import type { KeyValueStore } from './persistence';
4
- import type { Listener } from './pubsub';
5
3
  import type { ExtensionToken } from './token';
6
- /** The current session, stamped on events to tie them to a session and a browser tab. */
7
- export interface SessionContext {
8
- /** The stable session identifier attached to events captured during this session. */
9
- sessionId: string;
10
- /** The logical browser tab/window identifier attached alongside the session id. */
11
- windowId: string;
12
- /** When the session started, as a Unix timestamp in milliseconds. */
13
- sessionStartTimestamp: number;
14
- }
15
- /** Why a new session started (a `reset` also starts a new session). */
16
- export type NewSessionReason = 'initial' | 'reset' | 'idleTimeout' | 'maxLength' | 'crossTabAdoption';
17
- /** Details emitted when the client starts or adopts a new session. */
18
- export interface NewSessionInfo extends SessionContext {
19
- /** The condition that caused this session to begin. */
20
- reason: NewSessionReason;
21
- }
22
- /** A captured event, as observed by `onEvent`. */
23
- export interface CapturedEventInfo {
24
- /** The event name supplied to {@link Client.capture}. */
25
- event: string;
26
- /** The final event properties after client defaults and dynamic properties are applied. */
27
- properties: Record<string, unknown>;
28
- }
29
- /** Per-call capture overrides, mirroring the client's public capture options. */
30
- export interface CaptureOptions {
31
- /** Override the event timestamp sent to PostHog. */
32
- timestamp?: Date;
33
- /** Override the event UUID used for de-duplication. */
34
- uuid?: string;
35
- /** Person properties to set, emitted as `$set`. */
36
- set?: Record<string, unknown>;
37
- /** Person properties to set if unset, emitted as `$set_once`. */
38
- setOnce?: Record<string, unknown>;
39
- }
40
- /** A minimal response from {@link Client.apiRequest}. */
4
+ /** A minimal response from {@link Client.sendRequest}. */
41
5
  export interface ApiResponse {
42
- /** Whether the request completed with a 2xx status, or was queued for best-effort unload transport. */
43
- ok: boolean;
44
- /** The HTTP status code returned by the transport, or a client-defined best-effort status for unload sends. */
45
- status: number;
46
- /** Parse the response body as JSON; may be unavailable for best-effort unload requests. */
47
- json(): Promise<unknown>;
48
- /** Read the response body as text; may be unavailable for best-effort unload requests. */
49
- text(): Promise<string>;
6
+ /** The HTTP status code returned by the transport, or a client-defined best-effort status for sendBeacon sends. */
7
+ statusCode: number;
8
+ /** The response body parsed as JSON when available. */
9
+ json?: unknown;
10
+ /** The response body as text when available. */
11
+ text?: string;
12
+ /** The transport error when the request failed before receiving an HTTP response. */
13
+ error?: unknown;
50
14
  }
51
- /** Options for sending a request through {@link Client.apiRequest}. */
52
- export interface ApiRequestInit {
53
- /** HTTP method to use; defaults to the client's normal request method for the endpoint. */
15
+ /** Configured host used to resolve a relative request path. */
16
+ export type RequestTarget = 'api' | 'flags' | 'assets';
17
+ /** Browser transport requested for a send. */
18
+ export type RequestTransport = 'XHR' | 'fetch' | 'sendBeacon';
19
+ /** Options for sending a request through {@link Client.sendRequest}. */
20
+ export interface SendRequestInit {
21
+ /** Configured host to send through; defaults to the regular API host. */
22
+ target?: RequestTarget;
23
+ /** HTTP method to use; the host transport's default applies when omitted. */
54
24
  method?: 'GET' | 'POST';
55
25
  /** JSON-serialized by the client. */
56
26
  body?: unknown;
57
27
  /** Query string parameters appended to the request URL. */
58
28
  query?: Record<string, string>;
59
- /**
60
- * Mark this as a teardown send (pagehide / shutdown): the client picks the
61
- * most reliable fire-and-forget transport available `sendBeacon`, fetch
62
- * `keepalive`, or sync XHR. The response is best-effort: `.json()` may be
63
- * unusable (e.g. `sendBeacon` only reports "queued"), so callers must not
64
- * depend on it.
65
- */
66
- unload?: boolean;
29
+ /** Additional headers merged with the host SDK's configured request headers. */
30
+ headers?: Record<string, string>;
31
+ /** Browser transport to prefer. `sendBeacon` returns a best-effort response immediately. */
32
+ transport?: RequestTransport;
67
33
  /** Abort the request if it does not complete within this many milliseconds. */
68
34
  timeoutMs?: number;
69
35
  }
70
- /**
71
- * Server-provided configuration, as returned by the remote config response
72
- * (sampling rates, suppression rules, feature enablement, quotas, …). A loose
73
- * record by design — each extension reads only the keys it owns.
74
- */
75
- export type RemoteConfig = Record<string, unknown>;
76
36
  /**
77
37
  * The host SDK's capability surface as seen by an extension — the client an
78
- * extension is handed in `setup`. Each SDK (v1, v2) provides it as a client
79
- * adapter over its own internals.
38
+ * extension is handed in `setup`. A conforming host provides it as an adapter
39
+ * over its own internals.
80
40
  *
81
- * Synchronous members are always-ready in-memory reads (identity, session);
82
- * asynchronous members do I/O or wait for something to become ready
83
- * (`capture`, `apiRequest`, `kv`, `getRemoteConfig`).
41
+ * Host services that may do I/O are awaitable; a host can complete them
42
+ * synchronously when its underlying implementation supports that. Core
43
+ * analytics behavior is provided separately by the core extension.
84
44
  */
85
45
  export interface Client {
86
- /** The id events are currently attributed to the anonymous id, or the identified user's id after `identify`. */
87
- readonly distinctId: string;
88
- /** The anonymous device id; used before `identify` and carried on identify events as `$anon_distinct_id`. */
89
- readonly anonymousId: string;
90
- /** Active group memberships (group type → group key), attached to events as `$groups`. */
91
- readonly groups: Record<string, string>;
92
- /** The current session, created on first read if needed; reading does not extend or rotate it. */
93
- readonly session: SessionContext;
94
- /** Records an analytics event through the client's normal pipeline. */
95
- capture(event: string, properties?: Record<string, unknown> | null, options?: CaptureOptions): Promise<void>;
96
- /**
97
- * Registers a producer of properties merged into every captured event.
98
- * Returns a {@link Disposable} that removes it; an extension disposes it in
99
- * its own `dispose`. May be called more than once. The producer runs inline
100
- * during event build, so it must be cheap and synchronous; it may return
101
- * different properties each time (e.g. the current URL), and is recomputed
102
- * per event rather than stored.
103
- */
104
- registerDynamicEventProperties(producer: () => Record<string, unknown>): Disposable;
105
- /**
106
- * Sends a request to a PostHog endpoint; the client owns auth, headers, and
107
- * transport (fetch / XHR / keepalive). `path` is relative to the configured
108
- * API host, e.g. `/s/`, `/flags/`, `/api/surveys/`.
109
- */
110
- apiRequest(path: string, init?: ApiRequestInit): Promise<ApiResponse>;
46
+ /** Public project token used to authenticate endpoint-specific requests. */
47
+ readonly projectToken: string;
111
48
  /**
112
- * Resolves with the client's remote config once available, or `undefined`
113
- * if the fetch failed (never rejects, never hangs).
114
- * Re-readable: each call resolves with the current config, awaiting the
115
- * first fetch if none has landed. `await` it in `setup` to block until
116
- * config is known, or `.then()` it to reconfigure once it arrives; later
117
- * changes arrive via `onRemoteConfig`.
49
+ * Sends a request through the host SDK's transport. The extension owns the
50
+ * endpoint-specific path, method, authentication shape, body, and headers.
118
51
  */
119
- getRemoteConfig(): Promise<RemoteConfig | undefined>;
120
- /** Fires when server-provided config arrives or changes. */
121
- readonly onRemoteConfig: Listener<RemoteConfig>;
122
- /** Fires for every captured event — hot path, keep handlers cheap and synchronous. */
123
- readonly onEvent: Listener<CapturedEventInfo>;
124
- /** Fires when a new session starts, including on reset (discriminate via `reason`). */
125
- readonly onNewSession: Listener<NewSessionInfo>;
52
+ sendRequest(path: string, init?: SendRequestInit): Promise<ApiResponse>;
126
53
  /**
127
54
  * Resolves another registered extension by a capability token it provides, or
128
55
  * `undefined` if nothing registered provides it (not installed, or not loaded
129
56
  * yet). Lets one extension use another without importing its implementation.
130
57
  */
131
58
  getExtension<T>(token: ExtensionToken<T>): T | undefined;
132
- /** Async key-value storage scoped to this client instance and extension. */
59
+ /** Awaitable key-value storage backed by the host client's persistence. */
133
60
  readonly kv: KeyValueStore;
134
61
  /** Logger that follows the host client's debug/noise policy. */
135
62
  readonly logger: Logger;
package/dist/config.js CHANGED
@@ -26,7 +26,7 @@ __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
27
  default: ()=>__WEBPACK_DEFAULT_EXPORT__
28
28
  });
29
- const packageVersion = "0.2.1";
29
+ const packageVersion = "0.2.3";
30
30
  const Config = {
31
31
  DEBUG: false,
32
32
  LIB_VERSION: packageVersion,
package/dist/config.mjs CHANGED
@@ -1,4 +1,4 @@
1
- const packageVersion = "0.2.1";
1
+ const packageVersion = "0.2.3";
2
2
  const Config = {
3
3
  DEBUG: false,
4
4
  LIB_VERSION: packageVersion,
@@ -0,0 +1,80 @@
1
+ import type { JsonRecord, Properties } from '@posthog/types';
2
+ import type { Disposable } from './disposable';
3
+ import type { Extension } from './extension';
4
+ import type { Listener } from './pubsub';
5
+ import type { RemoteConfig } from './types/remote-config';
6
+ import type { ExtensionToken } from './token';
7
+ /** Recursively marks object properties as readonly while preserving callable values. */
8
+ export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends object ? {
9
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
10
+ } : T;
11
+ /** The current session, stamped on events to tie them to a session and a browser tab. */
12
+ export interface SessionContext {
13
+ /** The stable session identifier attached to events captured during this session. */
14
+ readonly sessionId: string;
15
+ /** The logical browser tab/window identifier attached alongside the session id. */
16
+ readonly windowId: string;
17
+ /** When the session started, as a Unix timestamp in milliseconds. */
18
+ readonly sessionStartTimestamp: number;
19
+ }
20
+ /** Why a new session started (a `reset` also starts a new session). */
21
+ export type NewSessionReason = 'initial' | 'reset' | 'idleTimeout' | 'maxLength' | 'crossTabAdoption';
22
+ /** Details emitted when the client starts or adopts a new session. */
23
+ export interface NewSessionInfo extends SessionContext {
24
+ /** The condition that caused this session to begin. */
25
+ readonly reason: NewSessionReason;
26
+ }
27
+ /** A captured event, as observed by `onEvent`. */
28
+ export interface CapturedEventInfo {
29
+ /** The finalized captured event name. */
30
+ readonly event: string;
31
+ /** The final event properties after client defaults and dynamic properties are applied. */
32
+ readonly properties: DeepReadonly<JsonRecord>;
33
+ }
34
+ /** Per-call capture overrides, mirroring the client's public capture options. */
35
+ export interface CaptureOptions {
36
+ /** Override the event timestamp sent to PostHog. */
37
+ timestamp?: Date;
38
+ /** Override the event UUID used for de-duplication. */
39
+ uuid?: string;
40
+ /** Person properties to set, emitted as `$set`. */
41
+ set?: Record<string, unknown>;
42
+ /** Person properties to set if unset, emitted as `$set_once`. */
43
+ setOnce?: Record<string, unknown>;
44
+ }
45
+ /**
46
+ * The host SDK's core analytics behavior, exposed as an extension so shared
47
+ * extensions can depend on the event pipeline without depending on a concrete
48
+ * PostHog client implementation.
49
+ */
50
+ export interface CoreExtension extends Extension {
51
+ /** The id events are currently attributed to. */
52
+ readonly distinctId: string;
53
+ /** The anonymous device id carried across identify calls. */
54
+ readonly anonymousId: string;
55
+ /** Active group memberships attached to events as `$groups`. */
56
+ readonly groups: DeepReadonly<Record<string, string>>;
57
+ /** The current session, created on first read if needed. */
58
+ readonly session: SessionContext;
59
+ /** Records an analytics event through the client's normal pipeline. */
60
+ capture(event: string, properties?: Properties | null, options?: CaptureOptions): Promise<void>;
61
+ /**
62
+ * Registers a producer of properties merged into every captured event.
63
+ * The producer runs inline while the event is built and must be synchronous.
64
+ */
65
+ registerDynamicEventProperties(producer: () => Record<string, unknown>): Disposable;
66
+ /** Fires for every captured event through a deeply readonly view. */
67
+ readonly onEvent: Listener<CapturedEventInfo>;
68
+ /** Fires when a new session starts, including on reset. */
69
+ readonly onNewSession: Listener<NewSessionInfo>;
70
+ /**
71
+ * Resolves with the current remote config, awaiting the first outcome when
72
+ * necessary. A failed outcome resolves to `undefined`; later successful
73
+ * changes are published through `onRemoteConfig`.
74
+ */
75
+ getRemoteConfig(): Promise<DeepReadonly<RemoteConfig> | undefined>;
76
+ /** Fires through a deeply readonly view when server-provided config arrives or changes successfully. */
77
+ readonly onRemoteConfig: Listener<DeepReadonly<RemoteConfig>>;
78
+ }
79
+ /** Capability token used to resolve the host SDK's core analytics extension. */
80
+ export declare const CoreExtension: ExtensionToken<CoreExtension>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ CoreExtension: ()=>CoreExtension
28
+ });
29
+ const CoreExtension = 'posthog.core';
30
+ exports.CoreExtension = __webpack_exports__.CoreExtension;
31
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
32
+ "CoreExtension"
33
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
34
+ Object.defineProperty(exports, '__esModule', {
35
+ value: true
36
+ });
@@ -0,0 +1,2 @@
1
+ const CoreExtension = 'posthog.core';
2
+ export { CoreExtension };
@@ -11,3 +11,5 @@ export interface Disposable {
11
11
  */
12
12
  dispose(): void | Promise<void>;
13
13
  }
14
+ /** Invokes teardown at most once and returns its first result to every caller. */
15
+ export declare function createDisposable(dispose: () => void | Promise<void>): Disposable;
@@ -1,5 +1,16 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
3
14
  (()=>{
4
15
  __webpack_require__.r = (exports1)=>{
5
16
  if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
@@ -12,7 +23,26 @@ var __webpack_require__ = {};
12
23
  })();
13
24
  var __webpack_exports__ = {};
14
25
  __webpack_require__.r(__webpack_exports__);
15
- for(var __webpack_i__ in __webpack_exports__)exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ createDisposable: ()=>createDisposable
28
+ });
29
+ function createDisposable(dispose) {
30
+ let active = true;
31
+ let result;
32
+ return {
33
+ dispose: ()=>{
34
+ if (active) {
35
+ active = false;
36
+ result = dispose();
37
+ }
38
+ return result;
39
+ }
40
+ };
41
+ }
42
+ exports.createDisposable = __webpack_exports__.createDisposable;
43
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
44
+ "createDisposable"
45
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
16
46
  Object.defineProperty(exports, '__esModule', {
17
47
  value: true
18
48
  });
@@ -0,0 +1,14 @@
1
+ function createDisposable(dispose) {
2
+ let active = true;
3
+ let result;
4
+ return {
5
+ dispose: ()=>{
6
+ if (active) {
7
+ active = false;
8
+ result = dispose();
9
+ }
10
+ return result;
11
+ }
12
+ };
13
+ }
14
+ export { createDisposable };
@@ -0,0 +1,34 @@
1
+ import { type Logger } from '@posthog/core';
2
+ import type { Client } from './client';
3
+ import type { Disposable } from './disposable';
4
+ import type { Extension } from './extension';
5
+ import type { ExtensionToken } from './token';
6
+ /**
7
+ * Shared lifecycle and capability registry for browser extension hosts.
8
+ *
9
+ * Hosts provide the concrete Client adapter while this runtime coordinates
10
+ * names, capability readiness, setup failures, and reverse-order teardown.
11
+ */
12
+ export declare class ExtensionRuntime implements Disposable {
13
+ private readonly _logger;
14
+ private readonly _extensions;
15
+ private readonly _registrationOrder;
16
+ private readonly _providerReservations;
17
+ private readonly _providers;
18
+ private _disposePromise;
19
+ constructor(_logger: Logger);
20
+ /**
21
+ * Sets up an extension and publishes its capabilities once setup succeeds.
22
+ * Names and tokens remain reserved while asynchronous setup is pending.
23
+ */
24
+ add(extension: Extension, client: Client): Promise<void>;
25
+ /** Resolves a capability only after its provider has completed setup. */
26
+ getExtension<T>(token: ExtensionToken<T>): T | undefined;
27
+ /** Disposes every registered extension once, in reverse registration order. */
28
+ dispose(): Promise<void>;
29
+ private _disposeAll;
30
+ private _handleSetupFailure;
31
+ private _disposeRegistration;
32
+ private _removeRegistration;
33
+ private _publishRegistration;
34
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ ExtensionRuntime: ()=>ExtensionRuntime
28
+ });
29
+ const core_namespaceObject = require("@posthog/core");
30
+ class ExtensionRuntime {
31
+ constructor(_logger){
32
+ this._logger = _logger;
33
+ this._extensions = new Map();
34
+ this._registrationOrder = [];
35
+ this._providerReservations = new Map();
36
+ this._providers = new Map();
37
+ }
38
+ async add(extension, client) {
39
+ if (this._disposePromise) throw new Error('Cannot add an extension to a disposed ExtensionRuntime');
40
+ if (this._extensions.has(extension.name)) throw new Error(`Browser extension "${extension.name}" is already registered`);
41
+ for (const token of extension.provides ?? [])if (this._providerReservations.has(token)) throw new Error(`Browser extension token "${token}" is already registered`);
42
+ const registered = {
43
+ extension,
44
+ setupPromise: Promise.resolve()
45
+ };
46
+ this._extensions.set(extension.name, registered);
47
+ this._registrationOrder.push(registered);
48
+ for (const token of extension.provides ?? [])this._providerReservations.set(token, registered);
49
+ let setupResult;
50
+ try {
51
+ setupResult = extension.setup(client);
52
+ } catch (error) {
53
+ registered.setupPromise = this._handleSetupFailure(registered, error);
54
+ return registered.setupPromise;
55
+ }
56
+ if (setupResult && (0, core_namespaceObject.isFunction)(setupResult.then)) registered.setupPromise = setupResult.then(()=>this._publishRegistration(registered)).catch((error)=>this._handleSetupFailure(registered, error));
57
+ else this._publishRegistration(registered);
58
+ return registered.setupPromise;
59
+ }
60
+ getExtension(token) {
61
+ return this._providers.get(token);
62
+ }
63
+ dispose() {
64
+ if (!this._disposePromise) this._disposePromise = this._disposeAll();
65
+ return this._disposePromise;
66
+ }
67
+ async _disposeAll() {
68
+ for (const registered of this._registrationOrder.slice().reverse()){
69
+ await registered.setupPromise;
70
+ await this._disposeRegistration(registered);
71
+ }
72
+ this._extensions.clear();
73
+ this._registrationOrder.length = 0;
74
+ this._providerReservations.clear();
75
+ this._providers.clear();
76
+ }
77
+ async _handleSetupFailure(registered, error) {
78
+ this._removeRegistration(registered);
79
+ this._logger.error(`Failed to set up browser extension "${registered.extension.name}"`, error);
80
+ if (this._disposePromise) return;
81
+ await this._disposeRegistration(registered);
82
+ const index = this._registrationOrder.indexOf(registered);
83
+ if (-1 !== index) this._registrationOrder.splice(index, 1);
84
+ }
85
+ _disposeRegistration(registered) {
86
+ if (!registered.disposalPromise) registered.disposalPromise = Promise.resolve().then(()=>registered.extension.dispose()).catch((error)=>{
87
+ this._logger.error(`Failed to dispose browser extension "${registered.extension.name}"`, error);
88
+ });
89
+ return registered.disposalPromise;
90
+ }
91
+ _removeRegistration(registered) {
92
+ if (this._extensions.get(registered.extension.name) === registered) this._extensions.delete(registered.extension.name);
93
+ for (const token of registered.extension.provides ?? []){
94
+ if (this._providerReservations.get(token) === registered) this._providerReservations.delete(token);
95
+ if (this._providers.get(token) === registered.extension) this._providers.delete(token);
96
+ }
97
+ }
98
+ _publishRegistration(registered) {
99
+ if (this._disposePromise || this._extensions.get(registered.extension.name) !== registered) return;
100
+ for (const token of registered.extension.provides ?? [])this._providers.set(token, registered.extension);
101
+ }
102
+ }
103
+ exports.ExtensionRuntime = __webpack_exports__.ExtensionRuntime;
104
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
105
+ "ExtensionRuntime"
106
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
107
+ Object.defineProperty(exports, '__esModule', {
108
+ value: true
109
+ });