@go-go-scope/adapter-svelte 2.7.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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/index.d.mts +280 -0
- package/dist/index.mjs +222 -0
- package/package.json +49 -0
- package/src/index.ts +609 -0
- package/tests/TestComponent.svelte +189 -0
- package/tests/svelte.test.ts +199 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
createScope,
|
|
4
|
+
createTask,
|
|
5
|
+
createParallel,
|
|
6
|
+
createChannel,
|
|
7
|
+
createBroadcast,
|
|
8
|
+
createPolling
|
|
9
|
+
} from "../src/index.js";
|
|
10
|
+
|
|
11
|
+
export let testType: string;
|
|
12
|
+
export let onDispose: () => void = () => {};
|
|
13
|
+
export let onBroadcast: (msg: string) => void = () => {};
|
|
14
|
+
|
|
15
|
+
// Create all possible stores at top level
|
|
16
|
+
const scope = createScope({ name: "test" });
|
|
17
|
+
scope.onDispose(onDispose);
|
|
18
|
+
|
|
19
|
+
const task = createTask(async () => "fetched-data", { immediate: true });
|
|
20
|
+
const manualTask = createTask(async () => "result-1", { immediate: false });
|
|
21
|
+
const errorTask = createTask(async () => { throw new Error("Task failed"); }, { immediate: false });
|
|
22
|
+
|
|
23
|
+
const delays = [50, 30, 70];
|
|
24
|
+
const factories = delays.map((delay, i) => async () => {
|
|
25
|
+
await new Promise(r => setTimeout(r, delay));
|
|
26
|
+
return `task-${i}`;
|
|
27
|
+
});
|
|
28
|
+
const parallel = createParallel(factories, { immediate: true });
|
|
29
|
+
|
|
30
|
+
const ch = createChannel<string>();
|
|
31
|
+
const chClose = createChannel<string>();
|
|
32
|
+
|
|
33
|
+
const bus = createBroadcast<string>();
|
|
34
|
+
bus.subscribe((msg) => onBroadcast(msg));
|
|
35
|
+
|
|
36
|
+
let pollCount1 = 0;
|
|
37
|
+
const poller = createPolling(async () => {
|
|
38
|
+
pollCount1++;
|
|
39
|
+
return `poll-${pollCount1}`;
|
|
40
|
+
}, { interval: 1000, immediate: true });
|
|
41
|
+
|
|
42
|
+
let pollCount2 = 0;
|
|
43
|
+
const pollerStop = createPolling(async () => {
|
|
44
|
+
pollCount2++;
|
|
45
|
+
return `poll-${pollCount2}`;
|
|
46
|
+
}, { interval: 1000, immediate: true });
|
|
47
|
+
|
|
48
|
+
// Subscribe to store values individually
|
|
49
|
+
let scopeActiveVal = false;
|
|
50
|
+
scope.isActive.subscribe(v => scopeActiveVal = v);
|
|
51
|
+
$: scopeActive = String(scopeActiveVal);
|
|
52
|
+
|
|
53
|
+
let taskDataVal: string | undefined;
|
|
54
|
+
task.data.subscribe(v => taskDataVal = v);
|
|
55
|
+
$: taskData = taskDataVal ?? "no-data";
|
|
56
|
+
|
|
57
|
+
let taskLoadingVal = false;
|
|
58
|
+
task.isLoading.subscribe(v => taskLoadingVal = v);
|
|
59
|
+
$: taskLoading = String(taskLoadingVal);
|
|
60
|
+
|
|
61
|
+
let manualTaskDataVal: string | undefined;
|
|
62
|
+
manualTask.data.subscribe(v => manualTaskDataVal = v);
|
|
63
|
+
$: manualTaskData = manualTaskDataVal ?? "no-data";
|
|
64
|
+
|
|
65
|
+
let errorTaskErrorVal: Error | undefined;
|
|
66
|
+
errorTask.error.subscribe(v => errorTaskErrorVal = v);
|
|
67
|
+
$: errorTaskError = errorTaskErrorVal?.message ?? "no-error";
|
|
68
|
+
|
|
69
|
+
let errorTaskDataVal: string | undefined;
|
|
70
|
+
errorTask.data.subscribe(v => errorTaskDataVal = v);
|
|
71
|
+
$: errorTaskData = errorTaskDataVal ?? "no-data";
|
|
72
|
+
|
|
73
|
+
let parallelProgressVal = 0;
|
|
74
|
+
parallel.progress.subscribe(v => parallelProgressVal = v);
|
|
75
|
+
$: parallelProgress = String(parallelProgressVal);
|
|
76
|
+
|
|
77
|
+
let parallelResultsVal: (string | undefined)[] = [];
|
|
78
|
+
parallel.results.subscribe(v => {
|
|
79
|
+
parallelResultsVal = v;
|
|
80
|
+
});
|
|
81
|
+
$: parallelResults = parallelResultsVal ? parallelResultsVal.filter((r): r is string => r !== undefined) : [];
|
|
82
|
+
|
|
83
|
+
let channelLatestVal: string | undefined;
|
|
84
|
+
ch.latest.subscribe(v => channelLatestVal = v);
|
|
85
|
+
$: channelLatest = channelLatestVal ?? "no-msg";
|
|
86
|
+
|
|
87
|
+
let channelHistoryVal: string[] = [];
|
|
88
|
+
ch.history.subscribe(v => channelHistoryVal = v);
|
|
89
|
+
$: channelHistory = [...channelHistoryVal];
|
|
90
|
+
|
|
91
|
+
let channelClosedVal = false;
|
|
92
|
+
chClose.isClosed.subscribe(v => channelClosedVal = v);
|
|
93
|
+
$: channelClosed = String(channelClosedVal);
|
|
94
|
+
|
|
95
|
+
let broadcastLatestVal: string | undefined;
|
|
96
|
+
bus.latest.subscribe(v => broadcastLatestVal = v);
|
|
97
|
+
$: broadcastLatest = broadcastLatestVal ?? "no-msg";
|
|
98
|
+
|
|
99
|
+
let pollDataVal: string | undefined;
|
|
100
|
+
poller.data.subscribe(v => pollDataVal = v);
|
|
101
|
+
$: pollData = pollDataVal ?? "no-data";
|
|
102
|
+
|
|
103
|
+
let pollCountVal = 0;
|
|
104
|
+
poller.pollCount.subscribe(v => pollCountVal = v);
|
|
105
|
+
$: pollCountStr = String(pollCountVal);
|
|
106
|
+
|
|
107
|
+
let isPollingVal = false;
|
|
108
|
+
pollerStop.isPolling.subscribe(v => isPollingVal = v);
|
|
109
|
+
$: isPolling = String(isPollingVal);
|
|
110
|
+
|
|
111
|
+
let pollStopDataVal: string | undefined;
|
|
112
|
+
pollerStop.data.subscribe(v => pollStopDataVal = v);
|
|
113
|
+
$: pollStopData = pollStopDataVal ?? "no-data";
|
|
114
|
+
|
|
115
|
+
// Action handlers
|
|
116
|
+
async function executeManualTask() {
|
|
117
|
+
await manualTask.execute();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function executeErrorTask() {
|
|
121
|
+
await errorTask.execute();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function sendMessages() {
|
|
125
|
+
await ch.send("msg-1");
|
|
126
|
+
await ch.send("msg-2");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function doBroadcast() {
|
|
130
|
+
bus.broadcast("hello");
|
|
131
|
+
bus.broadcast("world");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stopPolling() {
|
|
135
|
+
pollerStop.stop();
|
|
136
|
+
}
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
{#if testType === "scope"}
|
|
140
|
+
<span data-testid="active">{scopeActive}</span>
|
|
141
|
+
{/if}
|
|
142
|
+
|
|
143
|
+
{#if testType === "task-immediate"}
|
|
144
|
+
<span data-testid="data">{taskData}</span>
|
|
145
|
+
<span data-testid="loading">{taskLoading}</span>
|
|
146
|
+
{/if}
|
|
147
|
+
|
|
148
|
+
{#if testType === "task-manual"}
|
|
149
|
+
<span data-testid="data">{manualTaskData}</span>
|
|
150
|
+
<button data-testid="execute-btn" on:click={executeManualTask}>Execute</button>
|
|
151
|
+
{/if}
|
|
152
|
+
|
|
153
|
+
{#if testType === "task-error"}
|
|
154
|
+
<span data-testid="data">{errorTaskData}</span>
|
|
155
|
+
<span data-testid="error">{errorTaskError}</span>
|
|
156
|
+
<button data-testid="execute-btn" on:click={executeErrorTask}>Execute</button>
|
|
157
|
+
{/if}
|
|
158
|
+
|
|
159
|
+
{#if testType === "parallel"}
|
|
160
|
+
<span data-testid="progress">{parallelProgress}</span>
|
|
161
|
+
<span data-testid="results">{JSON.stringify(parallelResults)}</span>
|
|
162
|
+
{/if}
|
|
163
|
+
|
|
164
|
+
{#if testType === "channel"}
|
|
165
|
+
<span data-testid="latest">{channelLatest}</span>
|
|
166
|
+
<span data-testid="history">{JSON.stringify(channelHistory)}</span>
|
|
167
|
+
<button data-testid="send-btn" on:click={sendMessages}>Send</button>
|
|
168
|
+
{/if}
|
|
169
|
+
|
|
170
|
+
{#if testType === "channel-close"}
|
|
171
|
+
<span data-testid="closed">{channelClosed}</span>
|
|
172
|
+
<button data-testid="close-btn" on:click={() => chClose.close()}>Close</button>
|
|
173
|
+
{/if}
|
|
174
|
+
|
|
175
|
+
{#if testType === "broadcast"}
|
|
176
|
+
<span data-testid="latest">{broadcastLatest}</span>
|
|
177
|
+
<button data-testid="broadcast-btn" on:click={doBroadcast}>Broadcast</button>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
{#if testType === "polling"}
|
|
181
|
+
<span data-testid="data">{pollData}</span>
|
|
182
|
+
<span data-testid="count">{pollCountStr}</span>
|
|
183
|
+
{/if}
|
|
184
|
+
|
|
185
|
+
{#if testType === "polling-stop"}
|
|
186
|
+
<span data-testid="data">{pollStopData}</span>
|
|
187
|
+
<span data-testid="polling">{isPolling}</span>
|
|
188
|
+
<button data-testid="stop-btn" on:click={stopPolling}>Stop</button>
|
|
189
|
+
{/if}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for @go-go-scope/adapter-svelte
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import { render, screen, waitFor, fireEvent } from "@testing-library/svelte";
|
|
6
|
+
import { tick } from "svelte";
|
|
7
|
+
import Component from "./TestComponent.svelte";
|
|
8
|
+
|
|
9
|
+
describe("Svelte adapter integration", () => {
|
|
10
|
+
describe("createScope", () => {
|
|
11
|
+
test("creates and auto-disposes scope", async () => {
|
|
12
|
+
const onDispose = vi.fn();
|
|
13
|
+
|
|
14
|
+
const { unmount } = render(Component, {
|
|
15
|
+
props: { testType: "scope", onDispose },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Component should be active
|
|
19
|
+
expect(screen.getByTestId("active").textContent).toBe("true");
|
|
20
|
+
|
|
21
|
+
// Unmount
|
|
22
|
+
unmount();
|
|
23
|
+
await tick();
|
|
24
|
+
|
|
25
|
+
// Give time for async disposal
|
|
26
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
27
|
+
expect(onDispose).toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("createTask", () => {
|
|
32
|
+
test("executes task immediately when immediate is true", async () => {
|
|
33
|
+
render(Component, {
|
|
34
|
+
props: { testType: "task-immediate" },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Should start loading
|
|
38
|
+
expect(screen.getByTestId("loading").textContent).toBe("true");
|
|
39
|
+
|
|
40
|
+
// Wait for completion
|
|
41
|
+
await waitFor(() => {
|
|
42
|
+
expect(screen.getByTestId("data").textContent).toBe("fetched-data");
|
|
43
|
+
}, { timeout: 1000 });
|
|
44
|
+
|
|
45
|
+
expect(screen.getByTestId("loading").textContent).toBe("false");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("manual execution with execute()", async () => {
|
|
49
|
+
const { component } = render(Component, {
|
|
50
|
+
props: { testType: "task-manual" },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Should not have data initially
|
|
54
|
+
expect(screen.getByTestId("data").textContent).toBe("no-data");
|
|
55
|
+
|
|
56
|
+
// Execute manually
|
|
57
|
+
await fireEvent.click(screen.getByTestId("execute-btn"));
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
expect(screen.getByTestId("data").textContent).toBe("result-1");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("handles task errors", async () => {
|
|
65
|
+
render(Component, {
|
|
66
|
+
props: { testType: "task-error" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Execute failing task
|
|
70
|
+
await fireEvent.click(screen.getByTestId("execute-btn"));
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(screen.getByTestId("error").textContent).toBe("Task failed");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(screen.getByTestId("data").textContent).toBe("no-data");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("createParallel", () => {
|
|
81
|
+
test("executes tasks in parallel with progress", async () => {
|
|
82
|
+
render(Component, {
|
|
83
|
+
props: { testType: "parallel" },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Should show progress
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
const progress = screen.getByTestId("progress").textContent;
|
|
89
|
+
return progress === "100";
|
|
90
|
+
}, { timeout: 5000 });
|
|
91
|
+
|
|
92
|
+
// Wait a bit more for results to update
|
|
93
|
+
await new Promise(r => setTimeout(r, 100));
|
|
94
|
+
|
|
95
|
+
const results = JSON.parse(screen.getByTestId("results").textContent ?? "[]");
|
|
96
|
+
expect(results).toContain("task-0");
|
|
97
|
+
expect(results).toContain("task-1");
|
|
98
|
+
expect(results).toContain("task-2");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("createChannel", () => {
|
|
103
|
+
test("sends and receives messages", async () => {
|
|
104
|
+
render(Component, {
|
|
105
|
+
props: { testType: "channel" },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Send messages
|
|
109
|
+
await fireEvent.click(screen.getByTestId("send-btn"));
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(screen.getByTestId("latest").textContent).toBe("msg-2");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const history = JSON.parse(screen.getByTestId("history").textContent ?? "[]");
|
|
116
|
+
expect(history).toEqual(["msg-1", "msg-2"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("closes channel properly", async () => {
|
|
120
|
+
render(Component, {
|
|
121
|
+
props: { testType: "channel-close" },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(screen.getByTestId("closed").textContent).toBe("false");
|
|
125
|
+
|
|
126
|
+
// Close channel
|
|
127
|
+
await fireEvent.click(screen.getByTestId("close-btn"));
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(screen.getByTestId("closed").textContent).toBe("true");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("createBroadcast", () => {
|
|
136
|
+
test("broadcasts to subscribers", async () => {
|
|
137
|
+
render(Component, {
|
|
138
|
+
props: {
|
|
139
|
+
testType: "broadcast",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Initial state
|
|
144
|
+
expect(screen.getByTestId("latest").textContent).toBe("no-msg");
|
|
145
|
+
|
|
146
|
+
// Broadcast
|
|
147
|
+
await fireEvent.click(screen.getByTestId("broadcast-btn"));
|
|
148
|
+
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(screen.getByTestId("latest").textContent).toBe("world");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("createPolling", () => {
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
vi.useFakeTimers();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
vi.useRealTimers();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("polls at specified interval", async () => {
|
|
165
|
+
render(Component, {
|
|
166
|
+
props: { testType: "polling" },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// First poll
|
|
170
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
171
|
+
await tick();
|
|
172
|
+
|
|
173
|
+
expect(screen.getByTestId("data").textContent).toBe("poll-1");
|
|
174
|
+
|
|
175
|
+
// Second poll
|
|
176
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
177
|
+
await tick();
|
|
178
|
+
|
|
179
|
+
expect(screen.getByTestId("data").textContent).toBe("poll-2");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("stops polling when stop() called", async () => {
|
|
183
|
+
render(Component, {
|
|
184
|
+
props: { testType: "polling-stop" },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
188
|
+
await tick();
|
|
189
|
+
|
|
190
|
+
expect(screen.getByTestId("polling").textContent).toBe("true");
|
|
191
|
+
|
|
192
|
+
// Stop polling
|
|
193
|
+
await fireEvent.click(screen.getByTestId("stop-btn"));
|
|
194
|
+
await tick();
|
|
195
|
+
|
|
196
|
+
expect(screen.getByTestId("polling").textContent).toBe("false");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"lib": ["ES2022", "ES2022.Error", "ESNext.Disposable", "DOM"],
|
|
7
|
+
"verbatimModuleSyntax": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "dist"]
|
|
11
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [svelte()],
|
|
6
|
+
test: {
|
|
7
|
+
environment: "jsdom",
|
|
8
|
+
globals: true,
|
|
9
|
+
},
|
|
10
|
+
resolve: {
|
|
11
|
+
conditions: ['browser'],
|
|
12
|
+
},
|
|
13
|
+
});
|