pulse-framework 0.1.39__py3-none-any.whl → 0.1.41__py3-none-any.whl
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.
- pulse/__init__.py +14 -4
- pulse/app.py +176 -126
- pulse/channel.py +7 -7
- pulse/cli/cmd.py +81 -45
- pulse/cli/models.py +2 -0
- pulse/cli/processes.py +67 -22
- pulse/cli/uvicorn_log_config.py +1 -1
- pulse/codegen/codegen.py +14 -1
- pulse/codegen/templates/layout.py +10 -2
- pulse/decorators.py +132 -40
- pulse/form.py +9 -9
- pulse/helpers.py +75 -11
- pulse/hooks/core.py +4 -3
- pulse/hooks/states.py +91 -54
- pulse/messages.py +1 -1
- pulse/middleware.py +170 -119
- pulse/plugin.py +0 -3
- pulse/proxy.py +168 -147
- pulse/queries/__init__.py +0 -0
- pulse/queries/common.py +24 -0
- pulse/queries/mutation.py +142 -0
- pulse/queries/query.py +270 -0
- pulse/queries/query_observer.py +365 -0
- pulse/queries/store.py +60 -0
- pulse/reactive.py +146 -50
- pulse/render_session.py +5 -2
- pulse/routing.py +68 -10
- pulse/state.py +8 -7
- pulse/types/event_handler.py +2 -3
- pulse/user_session.py +3 -2
- {pulse_framework-0.1.39.dist-info → pulse_framework-0.1.41.dist-info}/METADATA +1 -1
- {pulse_framework-0.1.39.dist-info → pulse_framework-0.1.41.dist-info}/RECORD +34 -29
- pulse/query.py +0 -408
- {pulse_framework-0.1.39.dist-info → pulse_framework-0.1.41.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.39.dist-info → pulse_framework-0.1.41.dist-info}/entry_points.txt +0 -0
pulse/cli/cmd.py
CHANGED
|
@@ -12,7 +12,7 @@ import subprocess
|
|
|
12
12
|
import sys
|
|
13
13
|
from collections.abc import Sequence
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import cast
|
|
15
|
+
from typing import Callable, cast
|
|
16
16
|
|
|
17
17
|
import typer
|
|
18
18
|
from rich.console import Console
|
|
@@ -110,14 +110,14 @@ def run(
|
|
|
110
110
|
port = find_available_port(port)
|
|
111
111
|
|
|
112
112
|
console = Console()
|
|
113
|
-
console.
|
|
113
|
+
console.print(f"📁 Loading app from: {app_file}")
|
|
114
114
|
app_ctx = load_app_from_target(app_file)
|
|
115
115
|
_apply_app_context_to_env(app_ctx)
|
|
116
116
|
app_instance = app_ctx.app
|
|
117
117
|
|
|
118
118
|
is_single_server = app_instance.mode == "single-server"
|
|
119
119
|
if is_single_server:
|
|
120
|
-
console.
|
|
120
|
+
console.print("🔧 [cyan]Single-server mode[/cyan]")
|
|
121
121
|
|
|
122
122
|
# In single-server + server-only mode, require explicit React server address
|
|
123
123
|
if is_single_server and server_only:
|
|
@@ -168,41 +168,76 @@ def run(
|
|
|
168
168
|
web_args = extra_flags if web_only else []
|
|
169
169
|
|
|
170
170
|
commands: list[CommandSpec] = []
|
|
171
|
+
|
|
172
|
+
# Track readiness for announcement
|
|
173
|
+
server_ready = {"server": False, "web": False}
|
|
174
|
+
announced = False
|
|
175
|
+
|
|
176
|
+
def mark_web_ready() -> None:
|
|
177
|
+
server_ready["web"] = True
|
|
178
|
+
check_and_announce()
|
|
179
|
+
|
|
180
|
+
def mark_server_ready() -> None:
|
|
181
|
+
server_ready["server"] = True
|
|
182
|
+
check_and_announce()
|
|
183
|
+
|
|
184
|
+
def check_and_announce() -> None:
|
|
185
|
+
"""Announce when all required servers are ready."""
|
|
186
|
+
nonlocal announced
|
|
187
|
+
if announced:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
needs_server = not web_only
|
|
191
|
+
needs_web = not server_only
|
|
192
|
+
|
|
193
|
+
if needs_server and not server_ready["server"]:
|
|
194
|
+
return
|
|
195
|
+
if needs_web and not server_ready["web"]:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# All required servers are ready, show announcement
|
|
199
|
+
announced = True
|
|
200
|
+
protocol = "http" if address in ("127.0.0.1", "localhost") else "https"
|
|
201
|
+
server_url = f"{protocol}://{address}:{port}"
|
|
202
|
+
console.print("")
|
|
203
|
+
console.print(
|
|
204
|
+
f"✨ [bold green]Pulse running at:[/bold green] [bold cyan][link={server_url}]{server_url}[/link][/bold cyan]"
|
|
205
|
+
)
|
|
206
|
+
console.print(f" [dim]API: {server_url}/_pulse/...[/dim]")
|
|
207
|
+
console.print("")
|
|
208
|
+
|
|
171
209
|
# Build web command first (when needed) so we can set PULSE_REACT_SERVER_ADDRESS
|
|
172
210
|
# before building the uvicorn command, which needs that env var
|
|
173
211
|
if not server_only:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
os.environ[ENV_PULSE_REACT_SERVER_ADDRESS] = react_server_address
|
|
187
|
-
else:
|
|
188
|
-
commands.append(build_web_command(web_root=web_root, extra_args=web_args))
|
|
212
|
+
web_port = find_available_port(5173)
|
|
213
|
+
web_cmd = build_web_command(
|
|
214
|
+
web_root=web_root,
|
|
215
|
+
extra_args=web_args,
|
|
216
|
+
port=web_port,
|
|
217
|
+
mode=app_instance.env,
|
|
218
|
+
ready_pattern=r"localhost:\d+",
|
|
219
|
+
on_ready=mark_web_ready,
|
|
220
|
+
)
|
|
221
|
+
commands.append(web_cmd)
|
|
222
|
+
# Set env var so app can read the React server address (only used in single-server mode)
|
|
223
|
+
env.react_server_address = f"http://localhost:{web_port}"
|
|
189
224
|
|
|
190
225
|
if not web_only:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
226
|
+
server_cmd = build_uvicorn_command(
|
|
227
|
+
app_ctx=app_ctx,
|
|
228
|
+
address=address,
|
|
229
|
+
port=port,
|
|
230
|
+
reload_enabled=reload,
|
|
231
|
+
extra_args=server_args,
|
|
232
|
+
dev_secret=dev_secret,
|
|
233
|
+
server_only=server_only,
|
|
234
|
+
console=console,
|
|
235
|
+
web_root=web_root,
|
|
236
|
+
verbose=verbose,
|
|
237
|
+
ready_pattern=r"Application startup complete",
|
|
238
|
+
on_ready=mark_server_ready,
|
|
205
239
|
)
|
|
240
|
+
commands.append(server_cmd)
|
|
206
241
|
|
|
207
242
|
# Only add tags in dev mode to avoid breaking structured output (e.g., CloudWatch EMF metrics)
|
|
208
243
|
tag_colors = (
|
|
@@ -340,8 +375,9 @@ def build_uvicorn_command(
|
|
|
340
375
|
server_only: bool,
|
|
341
376
|
console: Console,
|
|
342
377
|
web_root: Path,
|
|
343
|
-
announce_url: bool,
|
|
344
378
|
verbose: bool = False,
|
|
379
|
+
ready_pattern: str | None = None,
|
|
380
|
+
on_ready: Callable[[], None] | None = None,
|
|
345
381
|
) -> CommandSpec:
|
|
346
382
|
app_import = f"{app_ctx.module_name}:{app_ctx.app_var}.asgi_factory"
|
|
347
383
|
args: list[str] = [
|
|
@@ -401,22 +437,13 @@ def build_uvicorn_command(
|
|
|
401
437
|
log_config_file.write_text(json.dumps(log_config))
|
|
402
438
|
args.extend(["--log-config", str(log_config_file)])
|
|
403
439
|
|
|
404
|
-
def _announce() -> None:
|
|
405
|
-
protocol = "http" if address in ("127.0.0.1", "localhost") else "https"
|
|
406
|
-
server_url = f"{protocol}://{address}:{port}"
|
|
407
|
-
console.log("")
|
|
408
|
-
console.log(
|
|
409
|
-
f"✨ [bold green]Pulse running at:[/bold green] [bold cyan][link={server_url}]{server_url}[/link][/bold cyan]"
|
|
410
|
-
)
|
|
411
|
-
console.log(f" [dim]API: {server_url}/_pulse/...[/dim]")
|
|
412
|
-
console.log("")
|
|
413
|
-
|
|
414
440
|
return CommandSpec(
|
|
415
441
|
name="server",
|
|
416
442
|
args=args,
|
|
417
443
|
cwd=cwd,
|
|
418
444
|
env=command_env,
|
|
419
|
-
|
|
445
|
+
ready_pattern=ready_pattern,
|
|
446
|
+
on_ready=on_ready,
|
|
420
447
|
)
|
|
421
448
|
|
|
422
449
|
|
|
@@ -426,6 +453,8 @@ def build_web_command(
|
|
|
426
453
|
extra_args: Sequence[str],
|
|
427
454
|
port: int | None = None,
|
|
428
455
|
mode: PulseEnv = "dev",
|
|
456
|
+
ready_pattern: str | None = None,
|
|
457
|
+
on_ready: Callable[[], None] | None = None,
|
|
429
458
|
) -> CommandSpec:
|
|
430
459
|
command_env = os.environ.copy()
|
|
431
460
|
if mode == "prod":
|
|
@@ -453,7 +482,14 @@ def build_web_command(
|
|
|
453
482
|
}
|
|
454
483
|
)
|
|
455
484
|
|
|
456
|
-
return CommandSpec(
|
|
485
|
+
return CommandSpec(
|
|
486
|
+
name="web",
|
|
487
|
+
args=args,
|
|
488
|
+
cwd=web_root,
|
|
489
|
+
env=command_env,
|
|
490
|
+
ready_pattern=ready_pattern,
|
|
491
|
+
on_ready=on_ready,
|
|
492
|
+
)
|
|
457
493
|
|
|
458
494
|
|
|
459
495
|
def _apply_app_context_to_env(app_ctx: AppLoadResult) -> None:
|
pulse/cli/models.py
CHANGED
pulse/cli/processes.py
CHANGED
|
@@ -3,25 +3,31 @@ from __future__ import annotations
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import os
|
|
5
5
|
import pty
|
|
6
|
+
import re
|
|
6
7
|
import select
|
|
7
8
|
import signal
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
from collections.abc import Mapping, Sequence
|
|
11
12
|
from io import TextIOBase
|
|
12
|
-
from typing import cast
|
|
13
|
+
from typing import TypeVar, cast
|
|
13
14
|
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
|
|
16
17
|
from pulse.cli.helpers import os_family
|
|
17
18
|
from pulse.cli.models import CommandSpec
|
|
18
19
|
|
|
20
|
+
_K = TypeVar("_K", int, str)
|
|
21
|
+
|
|
19
22
|
ANSI_CODES = {
|
|
20
23
|
"cyan": "\033[36m",
|
|
21
24
|
"orange1": "\033[38;5;208m",
|
|
22
25
|
"default": "\033[90m",
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
# Regex to strip ANSI escape codes
|
|
29
|
+
ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
def execute_commands(
|
|
27
33
|
commands: Sequence[CommandSpec],
|
|
@@ -45,6 +51,34 @@ def execute_commands(
|
|
|
45
51
|
return _run_with_pty(commands, console=console, colors=color_lookup)
|
|
46
52
|
|
|
47
53
|
|
|
54
|
+
def _call_on_spawn(spec: CommandSpec) -> None:
|
|
55
|
+
"""Call the on_spawn callback if it exists."""
|
|
56
|
+
if spec.on_spawn:
|
|
57
|
+
try:
|
|
58
|
+
spec.on_spawn()
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_on_ready(
|
|
64
|
+
spec: CommandSpec,
|
|
65
|
+
line: str,
|
|
66
|
+
ready_flags: dict[_K, bool],
|
|
67
|
+
key: _K,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Check if line matches ready_pattern and call on_ready if needed."""
|
|
70
|
+
if spec.ready_pattern and not ready_flags[key]:
|
|
71
|
+
# Strip ANSI codes before matching
|
|
72
|
+
clean_line = ANSI_ESCAPE.sub("", line)
|
|
73
|
+
if re.search(spec.ready_pattern, clean_line):
|
|
74
|
+
ready_flags[key] = True
|
|
75
|
+
if spec.on_ready:
|
|
76
|
+
try:
|
|
77
|
+
spec.on_ready()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
48
82
|
def _run_with_pty(
|
|
49
83
|
commands: Sequence[CommandSpec],
|
|
50
84
|
*,
|
|
@@ -52,8 +86,9 @@ def _run_with_pty(
|
|
|
52
86
|
colors: Mapping[str, str],
|
|
53
87
|
) -> int:
|
|
54
88
|
procs: list[tuple[str, int, int]] = []
|
|
55
|
-
|
|
89
|
+
fd_to_spec: dict[int, CommandSpec] = {}
|
|
56
90
|
buffers: dict[int, bytearray] = {}
|
|
91
|
+
ready_flags: dict[int, bool] = {}
|
|
57
92
|
|
|
58
93
|
try:
|
|
59
94
|
for spec in commands:
|
|
@@ -66,13 +101,10 @@ def _run_with_pty(
|
|
|
66
101
|
fcntl = __import__("fcntl")
|
|
67
102
|
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
|
68
103
|
procs.append((spec.name, pid, fd))
|
|
69
|
-
|
|
104
|
+
fd_to_spec[fd] = spec
|
|
70
105
|
buffers[fd] = bytearray()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
spec.on_spawn()
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
106
|
+
ready_flags[fd] = False
|
|
107
|
+
_call_on_spawn(spec)
|
|
76
108
|
|
|
77
109
|
while procs:
|
|
78
110
|
for tag, pid, fd in list(procs):
|
|
@@ -105,7 +137,9 @@ def _run_with_pty(
|
|
|
105
137
|
buffers[fd] = remainder
|
|
106
138
|
decoded = line.decode(errors="replace")
|
|
107
139
|
if decoded:
|
|
108
|
-
|
|
140
|
+
spec = fd_to_spec[fd]
|
|
141
|
+
_write_tagged_line(spec.name, decoded, colors)
|
|
142
|
+
_check_on_ready(spec, decoded, ready_flags, fd)
|
|
109
143
|
except OSError:
|
|
110
144
|
continue
|
|
111
145
|
|
|
@@ -144,9 +178,10 @@ def _run_without_pty(
|
|
|
144
178
|
) -> int:
|
|
145
179
|
from selectors import EVENT_READ, DefaultSelector
|
|
146
180
|
|
|
147
|
-
procs: list[tuple[str, subprocess.Popen[str]]] = []
|
|
181
|
+
procs: list[tuple[str, subprocess.Popen[str], CommandSpec]] = []
|
|
148
182
|
completed_codes: list[int] = []
|
|
149
183
|
selector = DefaultSelector()
|
|
184
|
+
ready_flags: dict[str, bool] = {}
|
|
150
185
|
|
|
151
186
|
try:
|
|
152
187
|
for spec in commands:
|
|
@@ -160,14 +195,11 @@ def _run_without_pty(
|
|
|
160
195
|
bufsize=1,
|
|
161
196
|
universal_newlines=True,
|
|
162
197
|
)
|
|
163
|
-
|
|
164
|
-
try:
|
|
165
|
-
spec.on_spawn()
|
|
166
|
-
except Exception:
|
|
167
|
-
pass
|
|
198
|
+
_call_on_spawn(spec)
|
|
168
199
|
if proc.stdout:
|
|
169
200
|
selector.register(proc.stdout, EVENT_READ, data=spec.name)
|
|
170
|
-
|
|
201
|
+
ready_flags[spec.name] = False
|
|
202
|
+
procs.append((spec.name, proc, spec))
|
|
171
203
|
|
|
172
204
|
while procs:
|
|
173
205
|
events = selector.select(timeout=0.1)
|
|
@@ -180,13 +212,16 @@ def _run_without_pty(
|
|
|
180
212
|
line = cast(TextIOBase, stream).readline()
|
|
181
213
|
if line:
|
|
182
214
|
_write_tagged_line(name, line.rstrip("\n"), colors)
|
|
215
|
+
spec = next((s for n, _, s in procs if n == name), None)
|
|
216
|
+
if spec:
|
|
217
|
+
_check_on_ready(spec, line, ready_flags, name)
|
|
183
218
|
else:
|
|
184
219
|
selector.unregister(stream)
|
|
185
|
-
remaining: list[tuple[str, subprocess.Popen[str]]] = []
|
|
186
|
-
for name, proc in procs:
|
|
220
|
+
remaining: list[tuple[str, subprocess.Popen[str], CommandSpec]] = []
|
|
221
|
+
for name, proc, spec in procs:
|
|
187
222
|
code = proc.poll()
|
|
188
223
|
if code is None:
|
|
189
|
-
remaining.append((name, proc))
|
|
224
|
+
remaining.append((name, proc, spec))
|
|
190
225
|
else:
|
|
191
226
|
completed_codes.append(code)
|
|
192
227
|
if proc.stdout:
|
|
@@ -195,12 +230,12 @@ def _run_without_pty(
|
|
|
195
230
|
proc.stdout.close()
|
|
196
231
|
procs = remaining
|
|
197
232
|
except KeyboardInterrupt:
|
|
198
|
-
for _name, proc in procs:
|
|
233
|
+
for _name, proc, _spec in procs:
|
|
199
234
|
with contextlib.suppress(Exception):
|
|
200
235
|
proc.terminate()
|
|
201
236
|
return 130
|
|
202
237
|
finally:
|
|
203
|
-
for _name, proc in procs:
|
|
238
|
+
for _name, proc, _spec in procs:
|
|
204
239
|
with contextlib.suppress(Exception):
|
|
205
240
|
proc.terminate()
|
|
206
241
|
with contextlib.suppress(Exception):
|
|
@@ -210,11 +245,21 @@ def _run_without_pty(
|
|
|
210
245
|
selector.unregister(key.fileobj)
|
|
211
246
|
selector.close()
|
|
212
247
|
|
|
213
|
-
exit_codes = completed_codes + [
|
|
248
|
+
exit_codes = completed_codes + [
|
|
249
|
+
proc.returncode or 0 for _name, proc, _spec in procs
|
|
250
|
+
]
|
|
214
251
|
return max(exit_codes) if exit_codes else 0
|
|
215
252
|
|
|
216
253
|
|
|
217
254
|
def _write_tagged_line(name: str, message: str, colors: Mapping[str, str]) -> None:
|
|
255
|
+
# Filter out unwanted web server messages
|
|
256
|
+
clean_message = ANSI_ESCAPE.sub("", message)
|
|
257
|
+
if (
|
|
258
|
+
"Network: use --host to expose" in clean_message
|
|
259
|
+
or "press h + enter to show help" in clean_message
|
|
260
|
+
):
|
|
261
|
+
return
|
|
262
|
+
|
|
218
263
|
# Only add tags if colors dict is not empty (i.e., tagging is enabled)
|
|
219
264
|
if colors:
|
|
220
265
|
color = ANSI_CODES.get(colors.get(name, ""), ANSI_CODES["default"])
|
pulse/cli/uvicorn_log_config.py
CHANGED
pulse/codegen/codegen.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from pulse.cli.helpers import ensure_gitignore_has
|
|
8
9
|
from pulse.codegen.templates.layout import LAYOUT_TEMPLATE
|
|
@@ -16,6 +17,9 @@ from pulse.css import CssImport, CssModule
|
|
|
16
17
|
from pulse.env import env
|
|
17
18
|
from pulse.routing import Layout, Route, RouteTree
|
|
18
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pulse.app import ConnectionStatusConfig
|
|
22
|
+
|
|
19
23
|
logger = logging.getLogger(__file__)
|
|
20
24
|
|
|
21
25
|
|
|
@@ -112,6 +116,7 @@ class Codegen:
|
|
|
112
116
|
server_address: str,
|
|
113
117
|
internal_server_address: str | None = None,
|
|
114
118
|
api_prefix: str = "",
|
|
119
|
+
connection_status: "ConnectionStatusConfig | None" = None,
|
|
115
120
|
):
|
|
116
121
|
# Ensure generated files are gitignored
|
|
117
122
|
ensure_gitignore_has(self.cfg.web_root, f"app/{self.cfg.pulse_dir}/")
|
|
@@ -124,7 +129,10 @@ class Codegen:
|
|
|
124
129
|
generated_files = set(
|
|
125
130
|
[
|
|
126
131
|
self.generate_layout_tsx(
|
|
127
|
-
server_address,
|
|
132
|
+
server_address,
|
|
133
|
+
internal_server_address,
|
|
134
|
+
api_prefix,
|
|
135
|
+
connection_status,
|
|
128
136
|
),
|
|
129
137
|
self.generate_routes_ts(),
|
|
130
138
|
self.generate_routes_runtime_ts(),
|
|
@@ -150,13 +158,18 @@ class Codegen:
|
|
|
150
158
|
server_address: str,
|
|
151
159
|
internal_server_address: str | None = None,
|
|
152
160
|
api_prefix: str = "",
|
|
161
|
+
connection_status: "ConnectionStatusConfig | None" = None,
|
|
153
162
|
):
|
|
154
163
|
"""Generates the content of _layout.tsx"""
|
|
164
|
+
from pulse.app import ConnectionStatusConfig
|
|
165
|
+
|
|
166
|
+
connection_status = connection_status or ConnectionStatusConfig()
|
|
155
167
|
content = str(
|
|
156
168
|
LAYOUT_TEMPLATE.render_unicode(
|
|
157
169
|
server_address=server_address,
|
|
158
170
|
internal_server_address=internal_server_address or server_address,
|
|
159
171
|
api_prefix=api_prefix,
|
|
172
|
+
connection_status=connection_status,
|
|
160
173
|
)
|
|
161
174
|
)
|
|
162
175
|
# The underscore avoids an eventual naming conflict with a generated
|
|
@@ -11,6 +11,11 @@ import { useLoaderData } from "react-router";
|
|
|
11
11
|
export const config: PulseConfig = {
|
|
12
12
|
serverAddress: "${server_address}",
|
|
13
13
|
apiPrefix: "${api_prefix}",
|
|
14
|
+
connectionStatus: {
|
|
15
|
+
initialConnectingDelay: ${int(connection_status.initial_connecting_delay * 1000)},
|
|
16
|
+
initialErrorDelay: ${int(connection_status.initial_error_delay * 1000)},
|
|
17
|
+
reconnectErrorDelay: ${int(connection_status.reconnect_error_delay * 1000)},
|
|
18
|
+
},
|
|
14
19
|
};
|
|
15
20
|
|
|
16
21
|
|
|
@@ -35,7 +40,10 @@ export async function loader(args: LoaderFunctionArgs) {
|
|
|
35
40
|
if (!res.ok) throw new Error("Failed to prerender batch:" + res.status);
|
|
36
41
|
const body = await res.json();
|
|
37
42
|
if (body.redirect) return new Response(null, { status: 302, headers: { Location: body.redirect } });
|
|
38
|
-
if (body.notFound)
|
|
43
|
+
if (body.notFound) {
|
|
44
|
+
console.error("Not found:", url.pathname);
|
|
45
|
+
throw new Response("Not Found", { status: 404 });
|
|
46
|
+
}
|
|
39
47
|
const prerenderData = deserialize(body) as PulsePrerender;
|
|
40
48
|
const setCookies =
|
|
41
49
|
(res.headers.getSetCookie?.() as string[] | undefined) ??
|
|
@@ -73,7 +81,7 @@ export async function clientLoader(args: ClientLoaderFunctionArgs) {
|
|
|
73
81
|
if (!res.ok) throw new Error("Failed to prerender batch:" + res.status);
|
|
74
82
|
const body = await res.json();
|
|
75
83
|
if (body.redirect) return new Response(null, { status: 302, headers: { Location: body.redirect } });
|
|
76
|
-
if (body.notFound)
|
|
84
|
+
if (body.notFound) throw new Response("Not Found", { status: 404 });
|
|
77
85
|
const prerenderData = deserialize(body) as PulsePrerender;
|
|
78
86
|
if (typeof window !== "undefined" && typeof sessionStorage !== "undefined" && prerenderData.directives) {
|
|
79
87
|
sessionStorage.setItem("__PULSE_DIRECTIVES", JSON.stringify(prerenderData.directives));
|