activeadmin-react 0.1.0.alpha1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +79 -4
- data/RELEASES.md +24 -14
- data/app/javascript/active_admin/react/index.d.ts +225 -0
- data/app/javascript/active_admin/react/index.js +1 -0
- data/app/javascript/active_admin/react/resumable.js +133 -0
- data/app/javascript/active_admin/react/runtime.js +12 -0
- data/docs/README.md +4 -1
- data/docs/cable-delivery.md +35 -0
- data/docs/releasing.md +1 -1
- data/docs/resumable-subscriptions.md +56 -0
- data/docs/typescript.md +46 -0
- data/lib/active_admin/react/cable.rb +58 -0
- data/lib/active_admin/react/version.rb +1 -1
- data/lib/active_admin/react.rb +1 -0
- data/sig/active_admin/react.rbs +13 -0
- metadata +13 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0250683b92e883cd5386b556c1b0b3ca2db12e9d27e2fb6401360575a1de33f5'
|
|
4
|
+
data.tar.gz: 66d6cfd94df946bda32b5f8ef8821a2a15f1ecdc6c211777c2b141e558e0e26a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b01d4e7a51fe68600e13bfc8c994d8e64b4efcfa68e5fb7e71ff3b887a39294d6e2f16e4c62fa1af9e9da5e2af132c600475a1e1c53b643c219f6b19920e19f0
|
|
7
|
+
data.tar.gz: fac7bcd73de4ded661e73c07eeb088d6058227ff79eaffe83c62a97531ecde687468ce0004dd6f9a81e6246cc41d27eee4279b9d509f88a23bdb919b11f8544e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@
|
|
|
4
4
|
|
|
5
5
|
All notable changes to ActiveAdmin React are recorded here. During ordinary pre-1.0 development, PATCH releases contain fixes and small compatible improvements; MINOR releases may contain new capabilities, meaningful API evolution, and documented breaking changes.
|
|
6
6
|
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
## 0.3.0 — 2026-09-19
|
|
10
|
+
|
|
11
|
+
This release advances the MINOR line because it adds a public TypeScript contract for
|
|
12
|
+
the packaged JavaScript API without changing runtime behavior.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Packaged `index.d.ts` declarations now cover every public registry, runtime,
|
|
17
|
+
protocol, Cable, resumable-subscription, and operation export.
|
|
18
|
+
- Strict independent-consumer verification now proves supported usage typechecks and
|
|
19
|
+
representative invalid usage fails without a host-owned ambient declaration.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The minimum supported ActiveAdmin version is now `4.0.0.beta23`, which raises
|
|
24
|
+
the minimum supported Ruby version to 3.3.
|
|
25
|
+
|
|
26
|
+
## 0.2.0 — 2026-09-19
|
|
27
|
+
|
|
28
|
+
First ordinary pre-1.0 release after the initial Rodeo dogfooding prerelease. This
|
|
29
|
+
release advances the MINOR line because it adds reusable Action Cable delivery and
|
|
30
|
+
resumable-subscription capabilities alongside compatibility and lifecycle fixes.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `subscribeResumable` now provides fixed resume, monotonic cursor, deduplication, protocol-error, and idempotent cleanup mechanics for application-owned Action Cable streams.
|
|
35
|
+
- `ActiveAdmin::React::Cable.broadcast` now provides an observable, payload-safe best-effort delivery boundary with an immutable `BroadcastResult`.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Ordinary pre-1.0 releases now use unsuffixed `0.MINOR.PATCH` versions; prerelease suffixes are reserved for explicitly authorized major-release stabilization trains.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- React islands now unmount before Turbo replaces rendered content and remount after `turbo:render`, including `422` form validation responses.
|
|
44
|
+
- Rails 8.1.3.1 integration and browser validation now constrain the test-only JSON dependency below 3 until Rails supports JSON 3 keyword arguments.
|
|
45
|
+
|
|
7
46
|
## 0.1.0.alpha1 — 2026-09-05
|
|
8
47
|
|
|
9
48
|
First integrated prerelease for Rodeo dogfooding. Public Ruby and JavaScript contracts
|
data/README.md
CHANGED
|
@@ -11,10 +11,10 @@ ActiveAdmin React keeps administrative pages Rails-first and server-rendered whi
|
|
|
11
11
|
Add the gem to a Rails application that uses ActiveAdmin:
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
|
-
gem "activeadmin-react", "0.
|
|
14
|
+
gem "activeadmin-react", "0.3.0", require: "active_admin/react"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Then run `bundle install`. The gem requires Ruby 3.
|
|
17
|
+
Then run `bundle install`. The gem requires Ruby 3.3 or newer, Rails 8.x, and ActiveAdmin `4.0.0.beta23` or newer within the 4.x line. The JavaScript runtime uses the React 18/19 `createRoot` API; the host supplies `react` and `react-dom` and remains responsible for compiling and serving browser assets.
|
|
18
18
|
|
|
19
19
|
## Render an island from Arbre
|
|
20
20
|
|
|
@@ -41,7 +41,7 @@ The mount owns the `data-react-component` and `data-react-props` attributes. Oth
|
|
|
41
41
|
|
|
42
42
|
## Register and start components
|
|
43
43
|
|
|
44
|
-
The packaged JavaScript entrypoint is `app/javascript/active_admin/react/index.js`. Configure the host's Vite, esbuild, or equivalent resolver so `active_admin/react` points to that file inside the installed gem. For example, Vite can derive the gem root with `bundle show activeadmin-react`:
|
|
44
|
+
The packaged JavaScript entrypoint is `app/javascript/active_admin/react/index.js`. Its sibling `index.d.ts` describes every public registry, runtime, protocol, Cable, and operation export. Configure the host's Vite, esbuild, or equivalent resolver so `active_admin/react` points to that file inside the installed gem. For example, Vite can derive the gem root with `bundle show activeadmin-react`:
|
|
45
45
|
|
|
46
46
|
```js
|
|
47
47
|
import { execFileSync } from "node:child_process"
|
|
@@ -61,6 +61,29 @@ export default defineConfig({
|
|
|
61
61
|
})
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
TypeScript must resolve the same import to the containing directory so it discovers
|
|
65
|
+
`index.d.ts`; do not maintain a host-owned ambient declaration. For a vendored or
|
|
66
|
+
otherwise stable gem path, add a compiler path such as:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"compilerOptions": {
|
|
71
|
+
"baseUrl": ".",
|
|
72
|
+
"paths": {
|
|
73
|
+
"active_admin/react": [
|
|
74
|
+
"vendor/bundle/ruby/4.0.0/gems/activeadmin-react-0.3.0/app/javascript/active_admin/react"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Generate or update that path from `bundle show activeadmin-react` when the bundle
|
|
82
|
+
location changes. The declarations reference the official React types, so TypeScript
|
|
83
|
+
hosts provide versions of `@types/react` and `@types/react-dom` matching their React
|
|
84
|
+
runtime. See the [TypeScript consumer contract](docs/typescript.md) for the supported
|
|
85
|
+
types and an independent strict-mode verification pattern.
|
|
86
|
+
|
|
64
87
|
Register every component before starting the runtime:
|
|
65
88
|
|
|
66
89
|
```js
|
|
@@ -71,7 +94,7 @@ registerComponent("OrdersTable", OrdersTable)
|
|
|
71
94
|
start()
|
|
72
95
|
```
|
|
73
96
|
|
|
74
|
-
`start()` mounts every `[data-react-component]` island, mounts newly rendered pages after `turbo:load`, and unmounts roots before Turbo caches the page. Repeated calls are safe. `stop()` removes the Turbo listeners and unmounts tracked roots. Duplicate component names, unknown components, and malformed JSON props fail loudly.
|
|
97
|
+
`start()` mounts every `[data-react-component]` island, mounts newly rendered pages after `turbo:load`, and unmounts roots before Turbo caches the page. It also unmounts before `turbo:before-render` and remounts after `turbo:render`, covering form validation responses that replace page content without a new Turbo visit. Repeated calls are safe. `stop()` removes the Turbo listeners and unmounts tracked roots. Duplicate component names, unknown components, and malformed JSON props fail loudly.
|
|
75
98
|
|
|
76
99
|
The shipped modules use package-style relative imports intended for a JavaScript build tool. Copying the directory directly into an importmap or serving it to browsers without a resolver is not currently a supported integration path.
|
|
77
100
|
|
|
@@ -102,6 +125,58 @@ end
|
|
|
102
125
|
|
|
103
126
|
`ActiveAdmin::React::Contributions.diagnostics` returns entries sorted by namespace, component name, and owner. An installer can query `ActiveAdmin::React::Contributions.registry.registered?("OrdersTable")` before registering inside a reload hook. `ActiveAdmin::React::Contributions.reset!` creates a fresh registry for test isolation. Metadata hashes, arrays, sets, and strings are recursively copied and frozen during registration, so later changes to caller-owned values cannot alter registered state and diagnostics cannot mutate it. Other metadata values must be immutable objects supplied by the contributor.
|
|
104
127
|
|
|
128
|
+
## Generic resumable Action Cable streams
|
|
129
|
+
|
|
130
|
+
`subscribeResumable` owns only the common client transport mechanics for an application-owned, monotonically sequenced stream. The server still owns authorization, replay queries, and ordering replay before buffered live delivery:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import { subscribeResumable } from "active_admin/react"
|
|
134
|
+
|
|
135
|
+
const cursor = {
|
|
136
|
+
current: () => latestSequence,
|
|
137
|
+
advance: (sequence) => { latestSequence = sequence }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const subscription = subscribeResumable({
|
|
141
|
+
consumer,
|
|
142
|
+
channel: "AuditEventsChannel",
|
|
143
|
+
params: { audit_id: auditId },
|
|
144
|
+
cursor,
|
|
145
|
+
parse(raw) {
|
|
146
|
+
const event = parseAuditEvent(raw)
|
|
147
|
+
return { event, sequence: event.sequence }
|
|
148
|
+
},
|
|
149
|
+
onEvent: (event) => applyAuditEvent(event),
|
|
150
|
+
onStatus: (status, details) => reportConnection(status, details),
|
|
151
|
+
onProtocolError: (error, raw) => reportMalformedDelivery(error, raw)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return () => subscription.unsubscribe()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Every connection performs the fixed `resume` action with `{ after_sequence: cursor.current() }`. Cursors and parsed sequences must be non-negative safe integers. Duplicate and stale sequences are ignored. A fresh event reaches `onEvent` before the cursor advances, so a failing handler leaves the delivery eligible for replay. Protocol errors throw when `onProtocolError` is omitted. Cleanup is idempotent and unsubscribes only this subscription; shared consumers are never disconnected.
|
|
158
|
+
|
|
159
|
+
The helper deliberately excludes domain reduction, terminal-state behavior, rendering, retry policy, configurable resume actions, reset/epoch semantics, authorization, and server replay implementation. See the [resumable subscription contract](docs/resumable-subscriptions.md).
|
|
160
|
+
|
|
161
|
+
## Best-effort Action Cable delivery
|
|
162
|
+
|
|
163
|
+
Persist durable application truth before broadcasting its live projection. `ActiveAdmin::React::Cable.broadcast` contains transport failures and returns an immutable result instead of allowing Action Cable availability to change committed domain state:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
result = ActiveAdmin::React::Cable.broadcast(
|
|
167
|
+
stream: operation.broadcast_key,
|
|
168
|
+
payload: event.envelope,
|
|
169
|
+
context: { workflow: "operation", event_id: event.id }
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
result.success? # true when Action Cable accepted the broadcast
|
|
173
|
+
result.error # the rescued transport error, or nil
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Failures emit `broadcast_failure.active_admin_react` through `ActiveSupport::Notifications` and write one Rails error log. Diagnostics contain the stream, caller-supplied context, and error, but never the payload. Keep context bounded and non-sensitive. Logger and notification-subscriber failures are themselves contained so live delivery cannot become authoritative through observability.
|
|
177
|
+
|
|
178
|
+
The helper does not provide persistence, transactions, jobs, retries, replay, or domain state. See the [Cable delivery contract](docs/cable-delivery.md) before adopting it.
|
|
179
|
+
|
|
105
180
|
## Asynchronous Action Cable operations
|
|
106
181
|
|
|
107
182
|
Action Cable transports operation state; application jobs and services own the expensive work. Each event uses a server-owned operation identifier, idempotency key, and monotonic sequence:
|
data/RELEASES.md
CHANGED
|
@@ -8,24 +8,33 @@ maturity determine when a release is ready.
|
|
|
8
8
|
|
|
9
9
|
## Pre-1.0 versions
|
|
10
10
|
|
|
11
|
-
Development uses ordinary `0.MINOR.PATCH` versions
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Increment PATCH for
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
Development uses ordinary `0.MINOR.PATCH` versions. The first integrated dogfooding
|
|
12
|
+
prerelease was `0.1.0.alpha1`; subsequent ordinary pre-1.0 development does not use
|
|
13
|
+
`alpha`, `beta`, or `rc` suffixes.
|
|
14
|
+
|
|
15
|
+
- Increment MINOR and reset PATCH to zero for new capabilities, meaningful API evolution,
|
|
16
|
+
and documented breaking changes while the public API remains unstable under Semantic
|
|
17
|
+
Versioning's `0.y.z` rules.
|
|
18
|
+
- Increment PATCH within the current MINOR line for fixes and small backward-compatible
|
|
19
|
+
improvements.
|
|
18
20
|
- Keep Rodeo-specific business behavior outside the gem. Rodeo dogfooding supplies the
|
|
19
21
|
primary evidence for whether generally useful contracts are ready to stabilize.
|
|
20
22
|
- Track ActiveAdmin 4 closely and consider generally useful ActiveAdmin or Arbre fixes for
|
|
21
23
|
upstream contribution instead of permanent private patches.
|
|
22
24
|
|
|
23
|
-
##
|
|
25
|
+
## Major-release stabilization
|
|
26
|
+
|
|
27
|
+
Reserve `alphaN`, `betaN`, and `rcN` suffixes for an explicitly authorized major-release
|
|
28
|
+
stabilization train. For the eventual path to `1.0.0`, begin that train only when Rodeo
|
|
29
|
+
dogfooding indicates that the Ruby API, JavaScript adapter protocol, security guidance,
|
|
30
|
+
packaging, and compatibility policy are ready to stabilize. Prerelease numbers start at
|
|
31
|
+
1 and have no leading zeroes. Publish `1.0.0` only after the authorized stabilization
|
|
32
|
+
phases are complete and only release-blocking defects remain.
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
Authorization of a major-release train includes reviewed updates to the tag guard,
|
|
35
|
+
workflow trigger, and protected `release` environment. The current executable policy
|
|
36
|
+
admits ordinary `v0.MINOR.PATCH` tags and the previously authorized future
|
|
37
|
+
`v1.0.0.rcN` shape; it does not admit suffixed `0.x` tags.
|
|
29
38
|
|
|
30
39
|
The stable release guarantees an Arbre-native mounting API, deterministic React lifecycle,
|
|
31
40
|
documented React and ActiveAdmin compatibility, Action Cable-friendly asynchronous
|
|
@@ -37,8 +46,9 @@ public APIs.
|
|
|
37
46
|
1. Merge the entire reviewed stack into `master`.
|
|
38
47
|
2. Ensure CI is green at the exact release commit.
|
|
39
48
|
3. Update `ActiveAdmin::React::VERSION` and release notes.
|
|
40
|
-
4. Tag
|
|
41
|
-
|
|
49
|
+
4. Tag an ordinary pre-1.0 release as exactly `v0.MINOR.PATCH`. Use `.alphaN`, `.betaN`,
|
|
50
|
+
or `.rcN` only for an explicitly authorized major-release stabilization train. Numeric
|
|
51
|
+
components and prerelease counters have no leading zeroes.
|
|
42
52
|
5. Let GitHub Actions publish through RubyGems Trusted Publishing and the `release`
|
|
43
53
|
environment.
|
|
44
54
|
6. Verify the gem is installable and its provenance is visible on RubyGems.org.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// app/javascript/active_admin/react/index.d.ts
|
|
2
|
+
|
|
3
|
+
import type { ComponentType } from "react"
|
|
4
|
+
import type { Root } from "react-dom/client"
|
|
5
|
+
|
|
6
|
+
export type ComponentName = string | number
|
|
7
|
+
export type ComponentProps = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
export function registerComponent<Props = ComponentProps>(
|
|
10
|
+
name: ComponentName,
|
|
11
|
+
component: ComponentType<Props>
|
|
12
|
+
): void
|
|
13
|
+
export function resolveComponent<Props = ComponentProps>(name: ComponentName): ComponentType<Props> | undefined
|
|
14
|
+
export function clearComponents(): void
|
|
15
|
+
|
|
16
|
+
export type MountRoot = Document | Element
|
|
17
|
+
|
|
18
|
+
export function mountElement(element: HTMLElement): Root
|
|
19
|
+
export function mountAll(root?: MountRoot): void
|
|
20
|
+
export function unmountElement(element: HTMLElement): void
|
|
21
|
+
export function unmountAll(root?: MountRoot): void
|
|
22
|
+
export function start(): void
|
|
23
|
+
export function stop(): void
|
|
24
|
+
|
|
25
|
+
export const OPERATION_STATES: readonly [
|
|
26
|
+
"pending",
|
|
27
|
+
"queued",
|
|
28
|
+
"running",
|
|
29
|
+
"retrying",
|
|
30
|
+
"completed",
|
|
31
|
+
"failed",
|
|
32
|
+
"cancelled"
|
|
33
|
+
]
|
|
34
|
+
export const TERMINAL_OPERATION_STATES: readonly ["completed", "failed", "cancelled"]
|
|
35
|
+
|
|
36
|
+
export type OperationStateName = (typeof OPERATION_STATES)[number]
|
|
37
|
+
export type TerminalOperationStateName = (typeof TERMINAL_OPERATION_STATES)[number]
|
|
38
|
+
export type OperationIgnoredReason = "duplicate" | "operation_mismatch" | "out_of_order" | "terminal"
|
|
39
|
+
|
|
40
|
+
export interface OperationError<Details = unknown> {
|
|
41
|
+
code: string | null
|
|
42
|
+
message: string | null
|
|
43
|
+
retryable: boolean
|
|
44
|
+
details: Details | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OperationEvent<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> {
|
|
48
|
+
operationId: string | null
|
|
49
|
+
id: string | null
|
|
50
|
+
idempotencyKey: string | null
|
|
51
|
+
sequence: number | null
|
|
52
|
+
state: OperationStateName | "unknown"
|
|
53
|
+
progress: number | null
|
|
54
|
+
message: string | null
|
|
55
|
+
result: Result | null
|
|
56
|
+
resultMetadata: ResultMetadata | null
|
|
57
|
+
error: OperationError<ErrorDetails> | null
|
|
58
|
+
occurredAt: string | null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface OperationEventInput<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> {
|
|
62
|
+
operation_id?: string | null
|
|
63
|
+
operationId?: string | null
|
|
64
|
+
event_id?: string | null
|
|
65
|
+
eventId?: string | null
|
|
66
|
+
id?: string | null
|
|
67
|
+
idempotency_key?: string | null
|
|
68
|
+
idempotencyKey?: string | null
|
|
69
|
+
sequence?: number | string | null
|
|
70
|
+
state?: string | null
|
|
71
|
+
progress?: number | string | null
|
|
72
|
+
message?: string | null
|
|
73
|
+
result?: Result | null
|
|
74
|
+
result_metadata?: ResultMetadata | null
|
|
75
|
+
resultMetadata?: ResultMetadata | null
|
|
76
|
+
error?: string | Partial<OperationError<ErrorDetails>> | null
|
|
77
|
+
occurred_at?: string | null
|
|
78
|
+
occurredAt?: string | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class OperationProtocolError extends Error {
|
|
82
|
+
constructor(issues: string[])
|
|
83
|
+
issues: string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function normalizeEvent<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown>(
|
|
87
|
+
event?: OperationEventInput<Result, ResultMetadata, ErrorDetails> | null
|
|
88
|
+
): OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
89
|
+
export function validateOperationEvent<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown>(
|
|
90
|
+
event: unknown
|
|
91
|
+
): OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
92
|
+
|
|
93
|
+
export interface OperationStateInitial<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown>
|
|
94
|
+
extends OperationEventInput<Result, ResultMetadata, ErrorDetails> {
|
|
95
|
+
occurredAt?: string | null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AppliedOperationEvent<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> {
|
|
99
|
+
applied: true
|
|
100
|
+
reason: null
|
|
101
|
+
value: OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface IgnoredOperationEvent<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> {
|
|
105
|
+
applied: false
|
|
106
|
+
reason: OperationIgnoredReason
|
|
107
|
+
value: OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type OperationEventOutcome<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> =
|
|
111
|
+
| AppliedOperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
112
|
+
| IgnoredOperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
113
|
+
|
|
114
|
+
export class OperationState<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> {
|
|
115
|
+
constructor(initial?: OperationStateInitial<Result, ResultMetadata, ErrorDetails>)
|
|
116
|
+
value: OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
117
|
+
readonly lastSequence: number | null
|
|
118
|
+
apply(event: OperationEventInput<Result, ResultMetadata, ErrorDetails>): OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
119
|
+
applyEvent(event: OperationEventInput<Result, ResultMetadata, ErrorDetails>): OperationEventOutcome<Result, ResultMetadata, ErrorDetails>
|
|
120
|
+
ignored(reason: OperationIgnoredReason): IgnoredOperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
121
|
+
operationMismatch(event: OperationEvent<Result, ResultMetadata, ErrorDetails>): boolean
|
|
122
|
+
outOfOrder(event: OperationEvent<Result, ResultMetadata, ErrorDetails>): boolean
|
|
123
|
+
terminal(): boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface OperationAccessibility {
|
|
127
|
+
role: "alert" | "status"
|
|
128
|
+
"aria-live": "assertive" | "polite"
|
|
129
|
+
"aria-busy": boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function operationAccessibility(
|
|
133
|
+
operation?: OperationState | Pick<OperationEvent, "state"> | null
|
|
134
|
+
): OperationAccessibility
|
|
135
|
+
|
|
136
|
+
export type CableIdentifier = { channel: string } & Record<string, unknown>
|
|
137
|
+
|
|
138
|
+
export interface CableSubscription {
|
|
139
|
+
perform(action: string, data?: Record<string, unknown>): unknown
|
|
140
|
+
unsubscribe(): void
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface CableCallbacks<Raw = unknown> {
|
|
144
|
+
connected(): void
|
|
145
|
+
disconnected(details?: unknown): void
|
|
146
|
+
rejected(): void
|
|
147
|
+
received(raw: Raw): void
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface CableConsumer<Raw = unknown> {
|
|
151
|
+
subscriptions: {
|
|
152
|
+
create(identifier: CableIdentifier, callbacks: CableCallbacks<Raw>): CableSubscription
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface SubscribeToOperationOptions<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown> {
|
|
157
|
+
consumer: CableConsumer
|
|
158
|
+
channel: string
|
|
159
|
+
params?: Record<string, unknown>
|
|
160
|
+
operationState?: OperationState<Result, ResultMetadata, ErrorDetails> | null
|
|
161
|
+
strict?: boolean
|
|
162
|
+
resume?: boolean
|
|
163
|
+
onEvent?: (
|
|
164
|
+
event: OperationEvent<Result, ResultMetadata, ErrorDetails>,
|
|
165
|
+
current: OperationEvent<Result, ResultMetadata, ErrorDetails>
|
|
166
|
+
) => void
|
|
167
|
+
onIgnoredEvent?: (
|
|
168
|
+
event: OperationEvent<Result, ResultMetadata, ErrorDetails>,
|
|
169
|
+
reason: OperationIgnoredReason
|
|
170
|
+
) => void
|
|
171
|
+
onProtocolError?: (error: unknown, raw: unknown) => void
|
|
172
|
+
onConnected?: (details: { resumeFrom: number | null }) => void
|
|
173
|
+
onDisconnected?: (details?: unknown) => void
|
|
174
|
+
onRejected?: () => void
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function subscribeToOperation<Result = unknown, ResultMetadata = unknown, ErrorDetails = unknown>(
|
|
178
|
+
options: SubscribeToOperationOptions<Result, ResultMetadata, ErrorDetails>
|
|
179
|
+
): CableSubscription
|
|
180
|
+
|
|
181
|
+
export interface RequestOperationCancellationOptions {
|
|
182
|
+
url: string
|
|
183
|
+
operationId: string
|
|
184
|
+
csrfToken?: string | null
|
|
185
|
+
fetchImpl?: typeof fetch
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function requestOperationCancellation<Response = unknown>(
|
|
189
|
+
options: RequestOperationCancellationOptions
|
|
190
|
+
): Promise<Response>
|
|
191
|
+
|
|
192
|
+
export class OperationCancellationError<Response = unknown> extends Error {
|
|
193
|
+
constructor(status: number, response: Response)
|
|
194
|
+
status: number
|
|
195
|
+
response: Response
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface ResumableCursor {
|
|
199
|
+
current(): number
|
|
200
|
+
advance(sequence: number): void
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface ParsedDelivery<Event> {
|
|
204
|
+
event: Event
|
|
205
|
+
sequence: number
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type ResumableStatus = "connected" | "disconnected" | "rejected"
|
|
209
|
+
|
|
210
|
+
export interface ResumableSubscription {
|
|
211
|
+
unsubscribe(): void
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface SubscribeResumableOptions<Event> {
|
|
215
|
+
consumer: CableConsumer
|
|
216
|
+
channel: string
|
|
217
|
+
params?: Record<string, unknown>
|
|
218
|
+
cursor: ResumableCursor
|
|
219
|
+
parse(raw: unknown): ParsedDelivery<Event>
|
|
220
|
+
onEvent(event: Event): void
|
|
221
|
+
onStatus?: (status: ResumableStatus, details?: unknown) => void
|
|
222
|
+
onProtocolError?: (error: Error, raw: unknown) => void
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function subscribeResumable<Event>(options: SubscribeResumableOptions<Event>): ResumableSubscription
|
|
@@ -4,6 +4,7 @@ export { clearComponents, registerComponent, resolveComponent } from "./registry
|
|
|
4
4
|
export { mountAll, mountElement, start, stop, unmountAll, unmountElement } from "./runtime"
|
|
5
5
|
export { OperationCancellationError, requestOperationCancellation, subscribeToOperation } from "./cable"
|
|
6
6
|
export { OperationState, operationAccessibility } from "./operation"
|
|
7
|
+
export { subscribeResumable } from "./resumable"
|
|
7
8
|
export {
|
|
8
9
|
normalizeEvent,
|
|
9
10
|
OPERATION_STATES,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// app/javascript/active_admin/react/resumable.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} ResumableCursor
|
|
5
|
+
* @property {() => number} current
|
|
6
|
+
* @property {(sequence: number) => void} advance
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @template Event
|
|
11
|
+
* @typedef {object} ParsedDelivery
|
|
12
|
+
* @property {Event} event
|
|
13
|
+
* @property {number} sequence
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @typedef {"connected" | "disconnected" | "rejected"} ResumableStatus */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} ResumableSubscription
|
|
20
|
+
* @property {() => void} unsubscribe
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to an application-owned, monotonically sequenced Action Cable stream.
|
|
25
|
+
*
|
|
26
|
+
* @template Event
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* @param {{ subscriptions: { create: Function } }} options.consumer
|
|
29
|
+
* @param {string} options.channel
|
|
30
|
+
* @param {Record<string, unknown>} [options.params]
|
|
31
|
+
* @param {ResumableCursor} options.cursor
|
|
32
|
+
* @param {(raw: unknown) => ParsedDelivery<Event>} options.parse
|
|
33
|
+
* @param {(event: Event) => void} options.onEvent
|
|
34
|
+
* @param {(status: ResumableStatus, details?: unknown) => void} [options.onStatus]
|
|
35
|
+
* @param {(error: Error, raw: unknown) => void} [options.onProtocolError]
|
|
36
|
+
* @returns {ResumableSubscription}
|
|
37
|
+
*/
|
|
38
|
+
export function subscribeResumable({
|
|
39
|
+
consumer,
|
|
40
|
+
channel,
|
|
41
|
+
params = {},
|
|
42
|
+
cursor,
|
|
43
|
+
parse,
|
|
44
|
+
onEvent,
|
|
45
|
+
onStatus,
|
|
46
|
+
onProtocolError
|
|
47
|
+
}) {
|
|
48
|
+
validateDependencies({ consumer, channel, cursor, parse, onEvent })
|
|
49
|
+
|
|
50
|
+
let subscription
|
|
51
|
+
let unsubscribed = false
|
|
52
|
+
|
|
53
|
+
const reportProtocolError = (error, raw) => {
|
|
54
|
+
const normalized = error instanceof Error ? error : new Error(String(error))
|
|
55
|
+
if (onProtocolError) {
|
|
56
|
+
onProtocolError(normalized, raw)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
throw normalized
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const readCursor = (raw) => {
|
|
63
|
+
try {
|
|
64
|
+
return safeSequence(cursor.current(), "cursor.current()")
|
|
65
|
+
} catch (error) {
|
|
66
|
+
reportProtocolError(error, raw)
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
subscription = consumer.subscriptions.create({ ...params, channel }, {
|
|
72
|
+
connected() {
|
|
73
|
+
const current = readCursor(undefined)
|
|
74
|
+
if (current === null) return
|
|
75
|
+
|
|
76
|
+
subscription.perform("resume", { after_sequence: current })
|
|
77
|
+
onStatus?.("connected")
|
|
78
|
+
},
|
|
79
|
+
disconnected(details) {
|
|
80
|
+
onStatus?.("disconnected", details)
|
|
81
|
+
},
|
|
82
|
+
rejected() {
|
|
83
|
+
onStatus?.("rejected")
|
|
84
|
+
},
|
|
85
|
+
received(raw) {
|
|
86
|
+
let parsed
|
|
87
|
+
let current
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
parsed = parse(raw)
|
|
91
|
+
safeSequence(parsed?.sequence, "parsed sequence")
|
|
92
|
+
current = safeSequence(cursor.current(), "cursor.current()")
|
|
93
|
+
} catch (error) {
|
|
94
|
+
reportProtocolError(error, raw)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (parsed.sequence <= current) return
|
|
99
|
+
|
|
100
|
+
onEvent(parsed.event)
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
cursor.advance(parsed.sequence)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
reportProtocolError(error, raw)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
unsubscribe() {
|
|
112
|
+
if (unsubscribed) return
|
|
113
|
+
|
|
114
|
+
unsubscribed = true
|
|
115
|
+
subscription.unsubscribe()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateDependencies({ consumer, channel, cursor, parse, onEvent }) {
|
|
121
|
+
if (typeof consumer?.subscriptions?.create !== "function") throw new Error("consumer is required")
|
|
122
|
+
if (typeof channel !== "string" || channel.trim().length === 0) throw new Error("channel is required")
|
|
123
|
+
if (typeof cursor?.current !== "function" || typeof cursor?.advance !== "function") {
|
|
124
|
+
throw new Error("cursor is required")
|
|
125
|
+
}
|
|
126
|
+
if (typeof parse !== "function") throw new Error("parse is required")
|
|
127
|
+
if (typeof onEvent !== "function") throw new Error("onEvent is required")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function safeSequence(value, label) {
|
|
131
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative safe integer`)
|
|
132
|
+
return value
|
|
133
|
+
}
|
|
@@ -16,10 +16,18 @@ function mountOnTurboLoad() {
|
|
|
16
16
|
mountAll()
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function mountAfterTurboRender() {
|
|
20
|
+
mountAll()
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
function unmountBeforeTurboCache() {
|
|
20
24
|
unmountAll()
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
function unmountBeforeTurboRender() {
|
|
28
|
+
unmountAll()
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
function propsFor(element) {
|
|
24
32
|
const raw = element.dataset.reactProps || "{}"
|
|
25
33
|
return JSON.parse(raw)
|
|
@@ -60,6 +68,8 @@ export function start() {
|
|
|
60
68
|
mountAll()
|
|
61
69
|
document.addEventListener("turbo:load", mountOnTurboLoad)
|
|
62
70
|
document.addEventListener("turbo:before-cache", unmountBeforeTurboCache)
|
|
71
|
+
document.addEventListener("turbo:before-render", unmountBeforeTurboRender)
|
|
72
|
+
document.addEventListener("turbo:render", mountAfterTurboRender)
|
|
63
73
|
started = true
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -69,5 +79,7 @@ export function stop() {
|
|
|
69
79
|
unmountAll()
|
|
70
80
|
document.removeEventListener("turbo:load", mountOnTurboLoad)
|
|
71
81
|
document.removeEventListener("turbo:before-cache", unmountBeforeTurboCache)
|
|
82
|
+
document.removeEventListener("turbo:before-render", unmountBeforeTurboRender)
|
|
83
|
+
document.removeEventListener("turbo:render", mountAfterTurboRender)
|
|
72
84
|
started = false
|
|
73
85
|
}
|
data/docs/README.md
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
# Documentation
|
|
4
4
|
|
|
5
5
|
- [Project README](../README.md) — installation, compatibility, public APIs, supported asset integration, security, development, and troubleshooting.
|
|
6
|
+
- [TypeScript consumer contract](typescript.md) — declaration discovery, public type coverage, and strict consumer verification.
|
|
7
|
+
- [Resumable subscription contract](resumable-subscriptions.md) — generic Action Cable resume, cursor, protocol-error, and ownership semantics.
|
|
8
|
+
- [Cable delivery contract](cable-delivery.md) — best-effort broadcast semantics, diagnostics, privacy, and host ownership.
|
|
6
9
|
- [Release process](releasing.md) — maintainer validation, RubyGems Trusted Publishing, failure handling, and post-release verification.
|
|
7
|
-
- [Release policy](../RELEASES.md) — ordinary pre-1.0
|
|
10
|
+
- [Release policy](../RELEASES.md) — unsuffixed ordinary pre-1.0 versions and explicitly authorized major-release stabilization trains.
|
|
8
11
|
- [Changelog](../CHANGELOG.md) — release notes for shipped and upcoming versions.
|
|
9
12
|
- [MIT License](../LICENSE.txt) — terms for using and distributing ActiveAdmin React.
|
|
10
13
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- docs/cable-delivery.md -->
|
|
2
|
+
|
|
3
|
+
# Best-effort Action Cable delivery
|
|
4
|
+
|
|
5
|
+
`ActiveAdmin::React::Cable.broadcast` is a narrow boundary for live projections of state that the host application has already committed. Action Cable improves responsiveness; it is never the source of truth.
|
|
6
|
+
|
|
7
|
+
## Contract
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
result = ActiveAdmin::React::Cable.broadcast(
|
|
11
|
+
stream: "operations:report-123",
|
|
12
|
+
payload: { type: "progress", sequence: 7 },
|
|
13
|
+
context: { workflow: "operation", event_id: 42 }
|
|
14
|
+
)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The method attempts exactly one `ActionCable.server.broadcast`. It returns a frozen `ActiveAdmin::React::Cable::BroadcastResult`: `success?` is true with a nil `error` after the adapter accepts the broadcast; otherwise `success?` is false and `error` is the rescued `StandardError`.
|
|
18
|
+
|
|
19
|
+
Broadcast after the transaction or lock that establishes durable truth. Never place persistence, enqueueing, or other authoritative work inside this failure boundary. The helper intentionally owns no retries, jobs, transactions, replay, event storage, or payload schema.
|
|
20
|
+
|
|
21
|
+
## Failure observability
|
|
22
|
+
|
|
23
|
+
A failed broadcast produces two best-effort diagnostics:
|
|
24
|
+
|
|
25
|
+
- one `broadcast_failure.active_admin_react` notification with `stream`, `context`, and `error`;
|
|
26
|
+
- one Rails error log with the same bounded diagnostic identity.
|
|
27
|
+
|
|
28
|
+
The payload is never attached or logged by the helper. Context is caller-controlled, so keep it small and exclude credentials, personal information, tenant secrets, and full records. If logging or a notification subscriber fails, that diagnostic failure is contained and the original broadcast error remains available on the returned result.
|
|
29
|
+
|
|
30
|
+
Success notifications, custom adapters, callbacks, configurable rescue classes, and retry options are outside the v1 contract. Add them only after independent dogfood evidence demonstrates a stable need.
|
|
31
|
+
|
|
32
|
+
—
|
|
33
|
+
Stan Carver II
|
|
34
|
+
Made in Texas 🤠
|
|
35
|
+
https://stancarver.com
|
data/docs/releasing.md
CHANGED
|
@@ -31,7 +31,7 @@ Before tagging, confirm every required GitHub Actions quality and package job is
|
|
|
31
31
|
|
|
32
32
|
## Publishing
|
|
33
33
|
|
|
34
|
-
Create
|
|
34
|
+
Create an ordinary pre-1.0 tag that exactly matches the gem version with a leading `v`: `v0.MINOR.PATCH`. The historical first integrated dogfooding prerelease was `v0.1.0.alpha1`; ordinary `0.x` development no longer uses `alpha`, `beta`, or `rc` suffixes. Reserve those suffixes for an explicitly authorized major-release stabilization train. The current guard also permits the previously authorized future `v1.0.0.rcN` shape. All numeric components have no leading zeroes, prerelease `N` starts at 1, and suffixed `0.x` versions, unauthorized major prereleases, arbitrary suffixes, and build metadata are rejected.
|
|
35
35
|
|
|
36
36
|
Push the tag only from the reviewed `master` commit. The tag-triggered `.github/workflows/release.yml` job checks out that commit and publishes through the protected `release` environment and RubyGems Trusted Publishing. Approving that environment deployment authorizes publication; local validation never does.
|
|
37
37
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- docs/resumable-subscriptions.md -->
|
|
2
|
+
|
|
3
|
+
# Resumable Action Cable subscriptions
|
|
4
|
+
|
|
5
|
+
`subscribeResumable` provides a narrow client-side lifecycle for application-owned streams with monotonically increasing integer sequences. It does not define a domain event schema.
|
|
6
|
+
|
|
7
|
+
## Public contract
|
|
8
|
+
|
|
9
|
+
The host supplies an Action Cable consumer, channel, optional identifier parameters, a cursor, a parser, and an event handler:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const result = subscribeResumable({
|
|
13
|
+
consumer,
|
|
14
|
+
channel: "EventsChannel",
|
|
15
|
+
params: { stream_id: streamId },
|
|
16
|
+
cursor: {
|
|
17
|
+
current: () => latestSequence,
|
|
18
|
+
advance: (sequence) => { latestSequence = sequence }
|
|
19
|
+
},
|
|
20
|
+
parse: (raw) => ({ event: validateEvent(raw), sequence: raw.sequence }),
|
|
21
|
+
onEvent: (event) => applyEvent(event)
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The channel name supplied by `channel` always wins over a `channel` key in `params`. On initial connection and every reconnect, the helper performs exactly:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
subscription.perform("resume", { after_sequence: cursor.current() })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `resume` action name and `after_sequence` parameter are fixed v1 protocol. The server authorizes the stream, queries durable events after that cursor, and orders replay before buffered live delivery.
|
|
32
|
+
|
|
33
|
+
## Cursor and delivery semantics
|
|
34
|
+
|
|
35
|
+
`cursor.current()` and every sequence returned by `parse` must be non-negative JavaScript safe integers. A parsed sequence less than or equal to the current cursor is ignored.
|
|
36
|
+
|
|
37
|
+
For a fresh sequence, the helper calls `onEvent(event)` first. It calls `cursor.advance(sequence)` only after the handler returns successfully. A handler exception remains loud and leaves the cursor unchanged, allowing the host's replay contract to redeliver the event.
|
|
38
|
+
|
|
39
|
+
Parsing failures, invalid cursor values, invalid parsed sequences, and cursor callback failures are protocol errors. When `onProtocolError` is supplied, it receives `(error, raw)`; otherwise the error is thrown. Domain handler and lifecycle callback failures are not converted into protocol errors.
|
|
40
|
+
|
|
41
|
+
## Lifecycle and ownership
|
|
42
|
+
|
|
43
|
+
One optional status callback receives `"connected"`, `"disconnected"`, or `"rejected"`; disconnect details are passed as its second argument. The returned `unsubscribe()` is idempotent and removes only the created subscription. It never disconnects the consumer because a host may share one consumer among many islands or streams.
|
|
44
|
+
|
|
45
|
+
The host calls `unsubscribe()` from its React effect cleanup. The helper relies on Action Cable's existing reconnect behavior and adds no retry or backoff policy.
|
|
46
|
+
|
|
47
|
+
## Deliberate exclusions
|
|
48
|
+
|
|
49
|
+
The v1 API does not own domain reducers, terminal states, React state, rendering, consumer construction, authorization, replay storage, transactions, configurable action names, ignored-event callbacks, per-lifecycle callbacks, or reset/epoch semantics. A workflow that resets its sequence must establish a new stream identity or adopt a durable monotonic sequence before using this helper.
|
|
50
|
+
|
|
51
|
+
`subscribeToOperation` remains a separate compatibility API with its existing operation-specific protocol.
|
|
52
|
+
|
|
53
|
+
—
|
|
54
|
+
Stan Carver II
|
|
55
|
+
Made in Texas 🤠
|
|
56
|
+
https://stancarver.com
|
data/docs/typescript.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!-- docs/typescript.md -->
|
|
2
|
+
|
|
3
|
+
# TypeScript consumer contract
|
|
4
|
+
|
|
5
|
+
ActiveAdmin React packages `index.d.ts` beside its supported
|
|
6
|
+
`app/javascript/active_admin/react/index.js` entrypoint. The declarations cover every
|
|
7
|
+
runtime export from that entrypoint and export the reusable option, event, result,
|
|
8
|
+
cursor, and Action Cable structural types used by those APIs.
|
|
9
|
+
|
|
10
|
+
## Declaration discovery
|
|
11
|
+
|
|
12
|
+
Keep the runtime build-tool alias pointed at `index.js`. Configure TypeScript's
|
|
13
|
+
`active_admin/react` path to the containing `app/javascript/active_admin/react`
|
|
14
|
+
directory so the compiler selects `index.d.ts`. A host should derive the installed gem
|
|
15
|
+
root from `bundle show activeadmin-react`; a vendored bundle may use its stable relative
|
|
16
|
+
path directly.
|
|
17
|
+
|
|
18
|
+
Do not copy the declaration into the host or add an ambient
|
|
19
|
+
`declare module "active_admin/react"` workaround. That would detach the application
|
|
20
|
+
contract from the installed gem version. The declaration imports official React types,
|
|
21
|
+
so the host provides `@types/react` and `@types/react-dom` versions compatible with its
|
|
22
|
+
React runtime.
|
|
23
|
+
|
|
24
|
+
## Public typing boundary
|
|
25
|
+
|
|
26
|
+
The declarations describe component registration and mounting, Turbo lifecycle
|
|
27
|
+
control, operation normalization and reduction, cancellation, operation subscriptions,
|
|
28
|
+
and generic resumable subscriptions. Known option and result shapes use named types;
|
|
29
|
+
application-owned payloads remain generic rather than being widened to a gem-owned
|
|
30
|
+
domain schema.
|
|
31
|
+
|
|
32
|
+
`CableConsumer` and `CableSubscription` are structural contracts. Hosts may pass the
|
|
33
|
+
corresponding Action Cable objects without an adapter. `subscribeResumable<Event>`
|
|
34
|
+
types the parsed delivery and handler with the host's event type while retaining the
|
|
35
|
+
fixed non-negative safe-integer cursor protocol documented in
|
|
36
|
+
[Resumable Action Cable subscriptions](resumable-subscriptions.md).
|
|
37
|
+
|
|
38
|
+
The repository's strict independent consumer imports only `active_admin/react`. Its
|
|
39
|
+
valid usage must typecheck, while separately compiled marked invalid calls must produce
|
|
40
|
+
TypeScript diagnostics. Gem package and isolated-install validation also require the
|
|
41
|
+
declaration file to be present.
|
|
42
|
+
|
|
43
|
+
—
|
|
44
|
+
Stan Carver II
|
|
45
|
+
Made in Texas 🤠
|
|
46
|
+
https://stancarver.com
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# lib/active_admin/react/cable.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'action_cable'
|
|
5
|
+
require 'active_support/notifications'
|
|
6
|
+
|
|
7
|
+
module ActiveAdmin
|
|
8
|
+
module React
|
|
9
|
+
# Delivers non-authoritative Action Cable projections without changing durable application truth.
|
|
10
|
+
module Cable
|
|
11
|
+
FAILURE_EVENT = 'broadcast_failure.active_admin_react'
|
|
12
|
+
|
|
13
|
+
# Immutable outcome of one best-effort broadcast attempt.
|
|
14
|
+
class BroadcastResult
|
|
15
|
+
attr_reader :error
|
|
16
|
+
|
|
17
|
+
def initialize(error:)
|
|
18
|
+
@error = error
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def success?
|
|
23
|
+
error.nil?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def broadcast(stream:, payload:, context: {})
|
|
30
|
+
ActionCable.server.broadcast(stream, payload)
|
|
31
|
+
BroadcastResult.new(error: nil)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
report_failure(stream:, context:, error: e)
|
|
34
|
+
BroadcastResult.new(error: e)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def report_failure(stream:, context:, error:)
|
|
38
|
+
safely do
|
|
39
|
+
Rails.logger.error(
|
|
40
|
+
"ActiveAdmin React Cable broadcast failed: stream=#{stream.inspect} " \
|
|
41
|
+
"context=#{context.inspect} error=#{error.class}: #{error.message}"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
safely do
|
|
45
|
+
ActiveSupport::Notifications.instrument(FAILURE_EVENT, stream:, context:, error:)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
private_class_method :report_failure
|
|
49
|
+
|
|
50
|
+
def safely
|
|
51
|
+
yield
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
private_class_method :safely
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/active_admin/react.rb
CHANGED
data/sig/active_admin/react.rbs
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveAdmin
|
|
4
4
|
module React
|
|
5
|
+
module Cable
|
|
6
|
+
FAILURE_EVENT: String
|
|
7
|
+
|
|
8
|
+
class BroadcastResult
|
|
9
|
+
attr_reader error: StandardError?
|
|
10
|
+
|
|
11
|
+
def initialize: (error: StandardError?) -> void
|
|
12
|
+
def success?: () -> bool
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.broadcast: (stream: String, payload: Hash[String | Symbol, untyped], ?context: Hash[String | Symbol, untyped]) -> BroadcastResult
|
|
16
|
+
end
|
|
17
|
+
|
|
5
18
|
class Registry
|
|
6
19
|
class Entry
|
|
7
20
|
attr_reader name: String
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeadmin-react
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stan Carver II
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 4.0.0.
|
|
18
|
+
version: 4.0.0.beta23
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '5'
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: 4.0.0.
|
|
28
|
+
version: 4.0.0.beta23
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '5'
|
|
@@ -62,15 +62,21 @@ files:
|
|
|
62
62
|
- README.md
|
|
63
63
|
- RELEASES.md
|
|
64
64
|
- app/javascript/active_admin/react/cable.js
|
|
65
|
+
- app/javascript/active_admin/react/index.d.ts
|
|
65
66
|
- app/javascript/active_admin/react/index.js
|
|
66
67
|
- app/javascript/active_admin/react/operation.js
|
|
67
68
|
- app/javascript/active_admin/react/protocol.js
|
|
68
69
|
- app/javascript/active_admin/react/registry.js
|
|
70
|
+
- app/javascript/active_admin/react/resumable.js
|
|
69
71
|
- app/javascript/active_admin/react/runtime.js
|
|
70
72
|
- docs/README.md
|
|
73
|
+
- docs/cable-delivery.md
|
|
71
74
|
- docs/releasing.md
|
|
75
|
+
- docs/resumable-subscriptions.md
|
|
76
|
+
- docs/typescript.md
|
|
72
77
|
- lib/active_admin/react.rb
|
|
73
78
|
- lib/active_admin/react/arbre.rb
|
|
79
|
+
- lib/active_admin/react/cable.rb
|
|
74
80
|
- lib/active_admin/react/contributions.rb
|
|
75
81
|
- lib/active_admin/react/mount.rb
|
|
76
82
|
- lib/active_admin/react/registry.rb
|
|
@@ -81,11 +87,11 @@ licenses:
|
|
|
81
87
|
- MIT
|
|
82
88
|
metadata:
|
|
83
89
|
bug_tracker_uri: https://github.com/scarver2/activeadmin-react/issues
|
|
84
|
-
changelog_uri: https://github.com/scarver2/activeadmin-react/blob/v0.
|
|
85
|
-
documentation_uri: https://github.com/scarver2/activeadmin-react/blob/v0.
|
|
90
|
+
changelog_uri: https://github.com/scarver2/activeadmin-react/blob/v0.3.0/CHANGELOG.md
|
|
91
|
+
documentation_uri: https://github.com/scarver2/activeadmin-react/blob/v0.3.0/README.md
|
|
86
92
|
homepage_uri: https://github.com/scarver2/activeadmin-react
|
|
87
93
|
rubygems_mfa_required: 'true'
|
|
88
|
-
source_code_uri: https://github.com/scarver2/activeadmin-react/tree/v0.
|
|
94
|
+
source_code_uri: https://github.com/scarver2/activeadmin-react/tree/v0.3.0
|
|
89
95
|
rdoc_options: []
|
|
90
96
|
require_paths:
|
|
91
97
|
- lib
|
|
@@ -93,7 +99,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
93
99
|
requirements:
|
|
94
100
|
- - ">="
|
|
95
101
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '3.
|
|
102
|
+
version: '3.3'
|
|
97
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
104
|
requirements:
|
|
99
105
|
- - ">="
|