@supersoniks/concorde 3.2.6 → 3.3.2
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/build-infos.json +1 -1
- package/concorde-core.bundle.js +249 -249
- package/concorde-core.es.js +2251 -1886
- package/dist/concorde-core.bundle.js +249 -249
- package/dist/concorde-core.es.js +2251 -1886
- package/docs/assets/{index-C0K6xugr.css → index-B669R8JF.css} +1 -1
- package/docs/assets/index-BTo6ly4d.js +4820 -0
- package/docs/index.html +2 -2
- package/docs/src/core/components/functional/fetch/fetch.md +6 -0
- package/docs/src/core/components/ui/menu/menu.md +46 -5
- package/docs/src/core/components/ui/modal/modal.md +0 -4
- package/docs/src/core/components/ui/toast/toast.md +166 -0
- package/docs/src/docs/_misc/ancestor-attribute.md +94 -0
- package/docs/src/docs/_misc/auto-subscribe.md +199 -0
- package/docs/src/docs/_misc/bind.md +362 -0
- package/docs/src/docs/_misc/on-assign.md +336 -0
- package/docs/src/docs/_misc/templates-demo.md +19 -0
- package/docs/src/docs/search/docs-search.json +550 -0
- package/docs/src/tsconfig-model.json +1 -1
- package/docs/src/tsconfig.json +28 -8
- package/package.json +8 -1
- package/src/core/components/functional/queue/queue.demo.ts +8 -11
- package/src/core/components/functional/sdui/sdui.ts +47 -22
- package/src/core/components/ui/form/input-autocomplete/input-autocomplete.ts +9 -1
- package/src/core/components/ui/tooltip/tooltip.ts +67 -2
- package/src/core/decorators/Subscriber.ts +5 -187
- package/src/core/decorators/subscriber/ancestorAttribute.ts +17 -0
- package/src/core/decorators/subscriber/autoFill.ts +28 -0
- package/src/core/decorators/subscriber/autoSubscribe.ts +54 -0
- package/src/core/decorators/subscriber/bind.ts +305 -0
- package/src/core/decorators/subscriber/common.ts +50 -0
- package/src/core/decorators/subscriber/onAssign.ts +318 -0
- package/src/core/mixins/Fetcher.ts +17 -8
- package/src/core/utils/HTML.ts +2 -0
- package/src/core/utils/PublisherProxy.ts +1 -1
- package/src/core/utils/api.ts +7 -0
- package/src/decorators.ts +9 -2
- package/src/docs/_misc/ancestor-attribute.md +94 -0
- package/src/docs/_misc/auto-subscribe.md +199 -0
- package/src/docs/_misc/bind.md +362 -0
- package/src/docs/_misc/on-assign.md +336 -0
- package/src/docs/_misc/templates-demo.md +19 -0
- package/src/docs/example/decorators-demo.ts +658 -0
- package/src/docs/navigation/navigation.ts +22 -3
- package/src/docs/search/docs-search.json +415 -0
- package/src/docs.ts +4 -0
- package/src/tsconfig-model.json +1 -1
- package/src/tsconfig.json +22 -2
- package/src/tsconfig.tsbuildinfo +1 -1
- package/vite.config.mts +0 -2
- package/docs/assets/index-Dgl1lJQo.js +0 -4861
- package/templates-test.html +0 -32
package/src/core/utils/HTML.ts
CHANGED
|
@@ -105,6 +105,7 @@ class HTML {
|
|
|
105
105
|
) as RequestCredentials) || undefined;
|
|
106
106
|
const cache = (node as Element).getAttribute("cache") as RequestCache;
|
|
107
107
|
const blockUntilDone = (node as Element).hasAttribute("blockUntilDone");
|
|
108
|
+
const keepAlive = (node as Element).hasAttribute("keepAlive");
|
|
108
109
|
return {
|
|
109
110
|
serviceURL,
|
|
110
111
|
token,
|
|
@@ -116,6 +117,7 @@ class HTML {
|
|
|
116
117
|
credentials,
|
|
117
118
|
cache,
|
|
118
119
|
blockUntilDone,
|
|
120
|
+
keepAlive,
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -455,9 +455,9 @@ export class PublisherProxy<T = any> {
|
|
|
455
455
|
*/
|
|
456
456
|
_cachedGet_?: T;
|
|
457
457
|
get(): T {
|
|
458
|
-
if (this._cachedGet_ !== undefined) return this._cachedGet_;
|
|
459
458
|
if (PublisherManager.modifiedCollectore.length > 0)
|
|
460
459
|
PublisherManager.modifiedCollectore[0].add(this);
|
|
460
|
+
if (this._cachedGet_ !== undefined) return this._cachedGet_;
|
|
461
461
|
if (Object.prototype.hasOwnProperty.call(this._value_, "__value")) {
|
|
462
462
|
const v = (this._value_ as any).__value;
|
|
463
463
|
return (this._cachedGet_ = (v != undefined ? v : null) as T);
|
package/src/core/utils/api.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type APIConfiguration = {
|
|
|
17
17
|
addHTTPResponse?: boolean;
|
|
18
18
|
credentials?: RequestCredentials;
|
|
19
19
|
cache?: RequestCache;
|
|
20
|
+
keepAlive?: boolean;
|
|
20
21
|
};
|
|
21
22
|
export type CallState = "loading" | "done" | "error" | undefined;
|
|
22
23
|
export type ResultTypeInterface = CoreJSType & {
|
|
@@ -128,6 +129,7 @@ class API {
|
|
|
128
129
|
lastResult?: Response;
|
|
129
130
|
isServiceSimulated = false;
|
|
130
131
|
blockUntilDone = false;
|
|
132
|
+
keepAlive = false;
|
|
131
133
|
constructor(config: APIConfiguration) {
|
|
132
134
|
this.serviceURL = config.serviceURL;
|
|
133
135
|
this.blockUntilDone = config.blockUntilDone || false;
|
|
@@ -143,6 +145,7 @@ class API {
|
|
|
143
145
|
this.addHTTPResponse = config.addHTTPResponse || false;
|
|
144
146
|
this.credentials = config.credentials;
|
|
145
147
|
this.cache = config.cache || "default";
|
|
148
|
+
this.keepAlive = config.keepAlive || false;
|
|
146
149
|
}
|
|
147
150
|
async handleResult(
|
|
148
151
|
fetchResult: Response,
|
|
@@ -235,6 +238,7 @@ class API {
|
|
|
235
238
|
{
|
|
236
239
|
headers: headers,
|
|
237
240
|
credentials: this.credentials,
|
|
241
|
+
keepalive: this.keepAlive,
|
|
238
242
|
}
|
|
239
243
|
);
|
|
240
244
|
|
|
@@ -384,6 +388,7 @@ class API {
|
|
|
384
388
|
headers: headers,
|
|
385
389
|
credentials: this.credentials,
|
|
386
390
|
cache: this.cache,
|
|
391
|
+
keepalive: this.keepAlive,
|
|
387
392
|
});
|
|
388
393
|
const handledResult = await this.handleResult(result, lastCall);
|
|
389
394
|
resolve(handledResult);
|
|
@@ -455,6 +460,7 @@ class API {
|
|
|
455
460
|
credentials: this.credentials,
|
|
456
461
|
method: method,
|
|
457
462
|
body: JSON.stringify(data),
|
|
463
|
+
keepalive: this.keepAlive,
|
|
458
464
|
});
|
|
459
465
|
return (await this.handleResult(result, lastCall)) as T &
|
|
460
466
|
ResultTypeInterface;
|
|
@@ -486,6 +492,7 @@ class API {
|
|
|
486
492
|
credentials: this.credentials,
|
|
487
493
|
method: method,
|
|
488
494
|
body: formData,
|
|
495
|
+
keepalive: this.keepAlive,
|
|
489
496
|
});
|
|
490
497
|
return (await this.handleResult(result, lastCall)) as T &
|
|
491
498
|
ResultTypeInterface;
|
package/src/decorators.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import * as mySubscriber from "@supersoniks/concorde/core/decorators/Subscriber";
|
|
2
2
|
export const bind = mySubscriber.bind;
|
|
3
3
|
export const onAssign = mySubscriber.onAssign;
|
|
4
|
+
export const ancestorAttribute = mySubscriber.ancestorAttribute;
|
|
5
|
+
export const autoSubscribe = mySubscriber.autoSubscribe;
|
|
6
|
+
export const autoFill = mySubscriber.autoFill;
|
|
4
7
|
|
|
5
|
-
import {ConcordeWindow} from "./core/_types/types";
|
|
8
|
+
import { ConcordeWindow } from "./core/_types/types";
|
|
6
9
|
declare const window: ConcordeWindow;
|
|
7
10
|
|
|
8
|
-
window["concorde-decorator-subscriber"] =
|
|
11
|
+
window["concorde-decorator-subscriber"] =
|
|
12
|
+
window["concorde-decorator-subscriber"] || {};
|
|
9
13
|
window["concorde-decorator-subscriber"] = {
|
|
10
14
|
bind: mySubscriber.bind,
|
|
11
15
|
onAssing: mySubscriber.onAssign,
|
|
16
|
+
ancestorAttribute: mySubscriber.ancestorAttribute,
|
|
17
|
+
autoSubscribe: mySubscriber.autoSubscribe,
|
|
18
|
+
autoFill: mySubscriber.autoFill,
|
|
12
19
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @ancestorAttribute
|
|
2
|
+
|
|
3
|
+
The `@ancestorAttribute` decorator automatically injects the value of an ancestor's attribute into a class property at the time of `connectedCallback`.
|
|
4
|
+
|
|
5
|
+
## Principle
|
|
6
|
+
|
|
7
|
+
This decorator uses `HTML.getAncestorAttributeValue` to traverse up the DOM tree from the current element and find the first ancestor that has the specified attribute. The value of this attribute is then assigned to the decorated property.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Import
|
|
12
|
+
|
|
13
|
+
<sonic-code language="typescript">
|
|
14
|
+
<template>
|
|
15
|
+
import { ancestorAttribute } from "@supersoniks/concorde/decorators";
|
|
16
|
+
</template>
|
|
17
|
+
</sonic-code>
|
|
18
|
+
|
|
19
|
+
### Basic example
|
|
20
|
+
Le composant lit les attributs `dataProvider` et `testAttribute` exposés par son conteneur ancêtre.
|
|
21
|
+
|
|
22
|
+
<sonic-code language="typescript">
|
|
23
|
+
<template>
|
|
24
|
+
//...
|
|
25
|
+
@customElement("demo-bind-reflect")
|
|
26
|
+
export class DemoBindReflect extends LitElement {
|
|
27
|
+
static styles = [tailwind];
|
|
28
|
+
//
|
|
29
|
+
@bind("bindReflectDemo.count", { reflect: true })
|
|
30
|
+
@state()
|
|
31
|
+
withReflect: number = 0;
|
|
32
|
+
//
|
|
33
|
+
@bind("bindReflectDemo.count")
|
|
34
|
+
@state()
|
|
35
|
+
withoutReflect: number = 0;
|
|
36
|
+
// initialize the publisher data
|
|
37
|
+
connectedCallback() {
|
|
38
|
+
super.connectedCallback();
|
|
39
|
+
this.resetData();
|
|
40
|
+
}
|
|
41
|
+
//
|
|
42
|
+
resetData() {
|
|
43
|
+
PublisherManager.get("bindReflectDemo").set({ count: 0 });
|
|
44
|
+
}
|
|
45
|
+
render() {
|
|
46
|
+
return html`
|
|
47
|
+
<div class="mb-3">
|
|
48
|
+
from publisher : ${sub("bindReflectDemo.count")} <br />
|
|
49
|
+
from component with reflect : ${this.withReflect} <br />
|
|
50
|
+
from component without reflect : ${this.withoutReflect}
|
|
51
|
+
</div>
|
|
52
|
+
<sonic-button @click=${() => this.withReflect++}
|
|
53
|
+
>Increment with reflect</sonic-button
|
|
54
|
+
>
|
|
55
|
+
<sonic-button @click=${() => this.withoutReflect++}
|
|
56
|
+
>Increment without reflect</sonic-button
|
|
57
|
+
>
|
|
58
|
+
<sonic-button @click=${this.resetData}>Reset publisher data</sonic-button>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</template>
|
|
63
|
+
</sonic-code>
|
|
64
|
+
|
|
65
|
+
<sonic-code>
|
|
66
|
+
<template>
|
|
67
|
+
<div dataProvider="demoDataProvider" testAttribute="test-value-123">
|
|
68
|
+
<demo-ancestor-attribute></demo-ancestor-attribute>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
</sonic-code>
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Use cases
|
|
75
|
+
|
|
76
|
+
This decorator is particularly useful for:
|
|
77
|
+
|
|
78
|
+
- **Retrieving the `dataProvider`** from an ancestor without having to pass it explicitly
|
|
79
|
+
- **Retrieving the `formDataProvider`** in form components
|
|
80
|
+
- **Retrieving the `wordingProvider`** for translation
|
|
81
|
+
- **Retrieving any other attribute** defined on an ancestor
|
|
82
|
+
|
|
83
|
+
## Behavior
|
|
84
|
+
|
|
85
|
+
- The search starts from the current element and traverses up the DOM tree
|
|
86
|
+
- If the attribute is not found, the property will be assigned `null`
|
|
87
|
+
- The injection happens automatically at the time of `connectedCallback`
|
|
88
|
+
- The value is not reactive: it is only updated once when the element is connected to the DOM
|
|
89
|
+
|
|
90
|
+
## Notes
|
|
91
|
+
|
|
92
|
+
- This decorator works with any component that has a `connectedCallback` method (such as `LitElement` or components extending `Subscriber`)
|
|
93
|
+
- The search also traverses Shadow DOM if necessary
|
|
94
|
+
- If multiple ancestors have the attribute, the closest one will be used
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# @autoSubscribe
|
|
2
|
+
|
|
3
|
+
The `@autoSubscribe` decorator automatically detects which publishers are accessed within a method and subscribes to them. When any of these publishers change, the method is automatically re-executed.
|
|
4
|
+
|
|
5
|
+
## Principle
|
|
6
|
+
|
|
7
|
+
This decorator wraps a method to track which publishers are accessed during its execution. It then subscribes to all accessed publishers, and when any of them change, the method is re-executed. This provides automatic reactivity without manually managing subscriptions.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Import
|
|
12
|
+
|
|
13
|
+
<sonic-code language="typescript">
|
|
14
|
+
<template>
|
|
15
|
+
import { autoSubscribe } from "@supersoniks/concorde/decorators";
|
|
16
|
+
</template>
|
|
17
|
+
</sonic-code>
|
|
18
|
+
|
|
19
|
+
### Basic example
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
<sonic-code language="typescript">
|
|
23
|
+
<template>
|
|
24
|
+
@customElement("demo-auto-subscribe")
|
|
25
|
+
export class DemoAutoSubscribe extends LitElement {
|
|
26
|
+
static styles = [tailwind];
|
|
27
|
+
//
|
|
28
|
+
@state() displayText: string = "";
|
|
29
|
+
@state() computedValue: number = 0;
|
|
30
|
+
//
|
|
31
|
+
@autoSubscribe()
|
|
32
|
+
updateDisplay() {
|
|
33
|
+
const value1 = PublisherManager.get("autoValue1").get() || 0;
|
|
34
|
+
const value2 = PublisherManager.get("autoValue2").get() || 0;
|
|
35
|
+
this.computedValue = value1 + value2;
|
|
36
|
+
this.displayText = `${value1} + ${value2} = ${this.computedValue}`;
|
|
37
|
+
}
|
|
38
|
+
//
|
|
39
|
+
render() {
|
|
40
|
+
return html`
|
|
41
|
+
<p><strong>${this.displayText}</strong></p>
|
|
42
|
+
<div>
|
|
43
|
+
<sonic-button @click=${() => this.randomizeValue("autoValue1")}>
|
|
44
|
+
Randomize Value 1
|
|
45
|
+
</sonic-button>
|
|
46
|
+
<sonic-button @click=${() => this.randomizeValue("autoValue2")}>
|
|
47
|
+
Randomize Value 2
|
|
48
|
+
</sonic-button>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
//
|
|
53
|
+
randomizeValue(publisherId: string) {
|
|
54
|
+
const value = PublisherManager.get(publisherId);
|
|
55
|
+
value.set(Math.floor(Math.random() * 100));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</template>
|
|
59
|
+
</sonic-code>
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
<sonic-code >
|
|
63
|
+
<template>
|
|
64
|
+
<demo-auto-subscribe></demo-auto-subscribe>
|
|
65
|
+
</template>
|
|
66
|
+
</sonic-code>
|
|
67
|
+
|
|
68
|
+
### Example with render method
|
|
69
|
+
|
|
70
|
+
<sonic-code language="typescript">
|
|
71
|
+
<template>
|
|
72
|
+
@customElement("reactive-view")
|
|
73
|
+
export class ReactiveView extends LitElement {
|
|
74
|
+
@autoSubscribe()
|
|
75
|
+
render() {
|
|
76
|
+
const data = PublisherManager.get("myData");
|
|
77
|
+
const config = PublisherManager.get("config");
|
|
78
|
+
//
|
|
79
|
+
// This render method will be automatically re-executed
|
|
80
|
+
// when myData or config change
|
|
81
|
+
const value = data.get()?.value || 0;
|
|
82
|
+
const multiplier = config.get()?.multiplier || 1;
|
|
83
|
+
//
|
|
84
|
+
return html`
|
|
85
|
+
<div>
|
|
86
|
+
<h1>Result: ${value * multiplier}</h1>
|
|
87
|
+
<p>Value: ${value}</p>
|
|
88
|
+
<p>Multiplier: ${multiplier}</p>
|
|
89
|
+
</div>
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
</template>
|
|
94
|
+
</sonic-code>
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
1. **First execution**: When the method is called, `PublisherManager.collectModifiedPublisher()` is used to track which publishers are accessed
|
|
99
|
+
2. **Subscription**: After execution, the decorator subscribes to all detected publishers
|
|
100
|
+
3. **Re-execution**: When any subscribed publisher changes, the method is automatically called again
|
|
101
|
+
4. **Cleanup**: On `disconnectedCallback`, all subscriptions are automatically removed
|
|
102
|
+
|
|
103
|
+
## Behavior
|
|
104
|
+
|
|
105
|
+
- The method is automatically called on `connectedCallback`
|
|
106
|
+
- The method is re-executed whenever any accessed publisher changes
|
|
107
|
+
- Subscriptions are managed automatically (no manual cleanup needed)
|
|
108
|
+
- Only publishers accessed during method execution are subscribed to
|
|
109
|
+
- The decorator uses `queueMicrotask` to batch multiple updates and avoid unnecessary re-renders
|
|
110
|
+
|
|
111
|
+
## Use cases
|
|
112
|
+
|
|
113
|
+
This decorator is particularly useful for:
|
|
114
|
+
|
|
115
|
+
- **Reactive rendering** where the render method depends on multiple publishers
|
|
116
|
+
- **Data transformation** that needs to update when source data changes
|
|
117
|
+
- **Computed properties** that depend on multiple data sources
|
|
118
|
+
- **Automatic synchronization** between publishers and component state
|
|
119
|
+
|
|
120
|
+
## Complete example
|
|
121
|
+
|
|
122
|
+
<sonic-code language="typescript">
|
|
123
|
+
<template>
|
|
124
|
+
import { html, LitElement } from "lit";
|
|
125
|
+
import { customElement } from "lit/decorators.js";
|
|
126
|
+
import { autoSubscribe } from "@supersoniks/concorde/decorators";
|
|
127
|
+
import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
|
|
128
|
+
//
|
|
129
|
+
@customElement("shopping-cart")
|
|
130
|
+
export class ShoppingCart extends LitElement {
|
|
131
|
+
items: any[] = [];
|
|
132
|
+
total: number = 0;
|
|
133
|
+
discount: number = 0;
|
|
134
|
+
//
|
|
135
|
+
@autoSubscribe()
|
|
136
|
+
calculateTotal() {
|
|
137
|
+
const cart = PublisherManager.get("cart");
|
|
138
|
+
const promo = PublisherManager.get("promo");
|
|
139
|
+
//
|
|
140
|
+
// Access cart items
|
|
141
|
+
this.items = cart.items.get() || [];
|
|
142
|
+
//
|
|
143
|
+
// Access promo code
|
|
144
|
+
const promoCode = promo.code.get() || "";
|
|
145
|
+
const discountPercent = promoCode === "SAVE10" ? 0.1 : 0;
|
|
146
|
+
//
|
|
147
|
+
// Calculate totals
|
|
148
|
+
const subtotal = this.items.reduce((sum, item) =>
|
|
149
|
+
sum + (item.price * item.quantity), 0
|
|
150
|
+
);
|
|
151
|
+
this.discount = subtotal * discountPercent;
|
|
152
|
+
this.total = subtotal - this.discount;
|
|
153
|
+
//
|
|
154
|
+
this.requestUpdate();
|
|
155
|
+
}
|
|
156
|
+
//
|
|
157
|
+
connectedCallback() {
|
|
158
|
+
super.connectedCallback();
|
|
159
|
+
this.calculateTotal();
|
|
160
|
+
}
|
|
161
|
+
//
|
|
162
|
+
render() {
|
|
163
|
+
return html`
|
|
164
|
+
<div class="cart">
|
|
165
|
+
<h2>Shopping Cart</h2>
|
|
166
|
+
${this.items.map(item => html`
|
|
167
|
+
<div>${item.name} x${item.quantity} - ${item.price}€</div>
|
|
168
|
+
`)}
|
|
169
|
+
<div class="total">
|
|
170
|
+
<p>Subtotal: ${this.total + this.discount}€</p>
|
|
171
|
+
<p>Discount: -${this.discount}€</p>
|
|
172
|
+
<p><strong>Total: ${this.total}€</strong></p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//
|
|
179
|
+
// When you update the publishers, calculateTotal is automatically called:
|
|
180
|
+
const cart = PublisherManager.get("cart");
|
|
181
|
+
cart.items.set([
|
|
182
|
+
{ name: "Product 1", price: 10, quantity: 2 },
|
|
183
|
+
{ name: "Product 2", price: 15, quantity: 1 }
|
|
184
|
+
]);
|
|
185
|
+
//
|
|
186
|
+
const promo = PublisherManager.get("promo");
|
|
187
|
+
promo.code.set("SAVE10");
|
|
188
|
+
// calculateTotal will be automatically called and the UI will update
|
|
189
|
+
</template>
|
|
190
|
+
</sonic-code>
|
|
191
|
+
|
|
192
|
+
## Notes
|
|
193
|
+
|
|
194
|
+
- This decorator works with any component that has `connectedCallback` and `disconnectedCallback` methods (such as `LitElement` or components extending `Subscriber`)
|
|
195
|
+
- The method is called automatically on `connectedCallback`
|
|
196
|
+
- Remember to call `this.requestUpdate()` if you're updating component properties
|
|
197
|
+
- The decorator uses debouncing via `queueMicrotask` to prevent excessive re-executions
|
|
198
|
+
- For more information about publishers, see the documentation on [Sharing data](#docs/_getting-started/pubsub.md/pubsub)
|
|
199
|
+
|