lakebed 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/package.json +1 -1
- package/src/anonymous-server.js +883 -36
- package/src/anonymous.js +73 -3
- package/src/cli.js +106 -35
- package/src/client.js +15 -0
- package/src/version.js +1 -1
package/src/anonymous.js
CHANGED
|
@@ -6,6 +6,14 @@ export const ANONYMOUS_ARTIFACT_FORMAT = "lakebed.capsule.artifact.v1";
|
|
|
6
6
|
export const ANONYMOUS_ARTIFACT_MEDIA_TYPE = "application/vnd.lakebed.artifact+json";
|
|
7
7
|
export { LAKEBED_VERSION };
|
|
8
8
|
|
|
9
|
+
export const SERVER_ENV_FILE = ".env.lakebed.server";
|
|
10
|
+
export const SERVER_ENV_LIMITS = {
|
|
11
|
+
maxKeyBytes: 128,
|
|
12
|
+
maxKeys: 64,
|
|
13
|
+
maxTotalBytes: 64 * 1024,
|
|
14
|
+
maxValueBytes: 16 * 1024
|
|
15
|
+
};
|
|
16
|
+
|
|
9
17
|
export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
10
18
|
artifactBytes: 1024 * 1024,
|
|
11
19
|
stateBytes: 1024 * 1024,
|
|
@@ -67,6 +75,65 @@ function isPlainObject(value) {
|
|
|
67
75
|
return Boolean(value) && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
export function validateServerEnvValues(values, path = "serverEnv.values") {
|
|
79
|
+
if (!isPlainObject(values)) {
|
|
80
|
+
throw new Error(`${path} must be a JSON object.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right));
|
|
84
|
+
if (entries.length > SERVER_ENV_LIMITS.maxKeys) {
|
|
85
|
+
throw new Error(`${path} may include at most ${SERVER_ENV_LIMITS.maxKeys} keys.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const env = {};
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
for (const [key, value] of entries) {
|
|
91
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
92
|
+
throw new Error(`${path}.${key} is not a valid server env key.`);
|
|
93
|
+
}
|
|
94
|
+
if (key.startsWith("LAKEBED_")) {
|
|
95
|
+
throw new Error(`${path}.${key} uses the reserved LAKEBED_ prefix.`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof value !== "string") {
|
|
98
|
+
throw new Error(`${path}.${key} must be a string.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const keyBytes = byteLength(key);
|
|
102
|
+
const valueBytes = byteLength(value);
|
|
103
|
+
if (keyBytes > SERVER_ENV_LIMITS.maxKeyBytes) {
|
|
104
|
+
throw new Error(`${path}.${key} exceeds ${SERVER_ENV_LIMITS.maxKeyBytes} bytes.`);
|
|
105
|
+
}
|
|
106
|
+
if (valueBytes > SERVER_ENV_LIMITS.maxValueBytes) {
|
|
107
|
+
throw new Error(`${path}.${key} exceeds ${SERVER_ENV_LIMITS.maxValueBytes} bytes.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
totalBytes += keyBytes + valueBytes;
|
|
111
|
+
env[key] = value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (totalBytes > SERVER_ENV_LIMITS.maxTotalBytes) {
|
|
115
|
+
throw new Error(`${path} exceeds ${SERVER_ENV_LIMITS.maxTotalBytes} bytes.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return env;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function validateServerEnvPayload(payload) {
|
|
122
|
+
if (payload === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!isPlainObject(payload)) {
|
|
127
|
+
throw new Error("serverEnv must be a JSON object.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (payload.mode !== "replace") {
|
|
131
|
+
throw new Error("serverEnv.mode must be replace.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return validateServerEnvValues(payload.values);
|
|
135
|
+
}
|
|
136
|
+
|
|
70
137
|
function isExpression(value) {
|
|
71
138
|
return Array.isArray(value) && expressionOps.has(value[0]);
|
|
72
139
|
}
|
|
@@ -317,7 +384,7 @@ function diagnostic(file, message) {
|
|
|
317
384
|
}
|
|
318
385
|
|
|
319
386
|
async function readSourceFiles(sourceStore) {
|
|
320
|
-
const paths = (await sourceStore.listFiles()).filter((path) => !path.startsWith("__lakebed/"));
|
|
387
|
+
const paths = (await sourceStore.listFiles()).filter((path) => !path.startsWith("__lakebed/") && path !== SERVER_ENV_FILE);
|
|
321
388
|
const files = [];
|
|
322
389
|
|
|
323
390
|
for (const path of paths) {
|
|
@@ -843,12 +910,14 @@ export function validateAnonymousDeployPayload(payload, options = {}) {
|
|
|
843
910
|
}
|
|
844
911
|
|
|
845
912
|
const artifactHash = sha256(stableStringify(payload.artifact));
|
|
913
|
+
const serverEnv = validateServerEnvPayload(payload.serverEnv);
|
|
846
914
|
return {
|
|
847
915
|
artifact: cloneJson(payload.artifact),
|
|
848
916
|
artifactHash,
|
|
849
917
|
clientBundle,
|
|
850
918
|
clientBundleBase64: clientBundle.toString("base64"),
|
|
851
|
-
clientBundleHash
|
|
919
|
+
clientBundleHash,
|
|
920
|
+
serverEnv
|
|
852
921
|
};
|
|
853
922
|
}
|
|
854
923
|
|
|
@@ -1102,6 +1171,7 @@ function createSourceQuery(rows, tableName) {
|
|
|
1102
1171
|
async function createSourceContext({ artifact, auth, deployId, state }) {
|
|
1103
1172
|
const rows = {};
|
|
1104
1173
|
const operations = [];
|
|
1174
|
+
const env = typeof state.getServerEnv === "function" ? await state.getServerEnv(deployId) : {};
|
|
1105
1175
|
for (const tableName of Object.keys(artifact.server.schema ?? {})) {
|
|
1106
1176
|
rows[tableName] = await state.listRows(deployId, tableName);
|
|
1107
1177
|
}
|
|
@@ -1143,7 +1213,7 @@ async function createSourceContext({ artifact, auth, deployId, state }) {
|
|
|
1143
1213
|
ctx: {
|
|
1144
1214
|
auth,
|
|
1145
1215
|
db,
|
|
1146
|
-
env
|
|
1216
|
+
env,
|
|
1147
1217
|
log: {
|
|
1148
1218
|
error() {},
|
|
1149
1219
|
info() {},
|
package/src/cli.js
CHANGED
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
ANONYMOUS_ARTIFACT_MEDIA_TYPE,
|
|
11
11
|
AnonymousCompilerError,
|
|
12
12
|
LAKEBED_VERSION,
|
|
13
|
+
SERVER_ENV_FILE,
|
|
13
14
|
createAnonymousArtifact,
|
|
14
15
|
createClaimedArtifact,
|
|
15
16
|
parseTtlSeconds,
|
|
16
|
-
stableStringify
|
|
17
|
+
stableStringify,
|
|
18
|
+
validateServerEnvValues
|
|
17
19
|
} from "./anonymous.js";
|
|
18
20
|
import { startAnonymousServer } from "./anonymous-server.js";
|
|
19
21
|
import { authFromUrl as resolveAuthFromUrl, createGuestAuth, requestOrigin, shooBaseUrlFromEnv } from "./auth.js";
|
|
@@ -221,29 +223,45 @@ function createSourcePlugin(sourceStore, target) {
|
|
|
221
223
|
};
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return env;
|
|
226
|
+
function unquoteServerEnvValue(value) {
|
|
227
|
+
if (value.length < 2) {
|
|
228
|
+
return value;
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
const
|
|
231
|
-
|
|
231
|
+
const quote = value[0];
|
|
232
|
+
if ((quote !== `"` && quote !== `'`) || value[value.length - 1] !== quote) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return value.slice(1, -1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseServerEnvFile(contents) {
|
|
240
|
+
const env = {};
|
|
241
|
+
for (const [index, rawLine] of contents.split(/\r?\n/).entries()) {
|
|
232
242
|
const line = rawLine.trim();
|
|
233
243
|
if (!line || line.startsWith("#")) {
|
|
234
244
|
continue;
|
|
235
245
|
}
|
|
236
246
|
|
|
237
|
-
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
247
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
238
248
|
if (!match) {
|
|
239
|
-
|
|
249
|
+
throw new Error(`Invalid ${SERVER_ENV_FILE} line ${index + 1}. Use KEY=value.`);
|
|
240
250
|
}
|
|
241
251
|
|
|
242
252
|
const [, key, rawValue] = match;
|
|
243
|
-
env[key] = rawValue.
|
|
253
|
+
env[key] = unquoteServerEnvValue(rawValue.trim());
|
|
244
254
|
}
|
|
245
255
|
|
|
246
|
-
return env;
|
|
256
|
+
return validateServerEnvValues(env, SERVER_ENV_FILE);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function readCapsuleServerEnv(sourceStore) {
|
|
260
|
+
if (!sourceStore.hasFile(SERVER_ENV_FILE)) {
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return parseServerEnvFile(await sourceStore.readFile(SERVER_ENV_FILE));
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev" } = {}) {
|
|
@@ -301,7 +319,7 @@ export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev"
|
|
|
301
319
|
app: capsuleModule.default,
|
|
302
320
|
buildDir,
|
|
303
321
|
clientOut,
|
|
304
|
-
env: await
|
|
322
|
+
env: await readCapsuleServerEnv(workingStore),
|
|
305
323
|
serverOut,
|
|
306
324
|
sourceStore: workingStore
|
|
307
325
|
};
|
|
@@ -579,9 +597,9 @@ async function dev(args) {
|
|
|
579
597
|
});
|
|
580
598
|
}
|
|
581
599
|
|
|
582
|
-
async function buildAnonymousEnvelope(capsuleArg) {
|
|
600
|
+
async function buildAnonymousEnvelope(capsuleArg, sourceStore) {
|
|
583
601
|
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
584
|
-
|
|
602
|
+
sourceStore ??= await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
585
603
|
const built = await buildCapsule({
|
|
586
604
|
capsuleDir,
|
|
587
605
|
capsuleId: `anonymous-${Date.now()}`,
|
|
@@ -615,7 +633,7 @@ function canDeployAfterClaim(error) {
|
|
|
615
633
|
);
|
|
616
634
|
}
|
|
617
635
|
|
|
618
|
-
async function buildClaimRequiredEnvelope({ capsuleDir }) {
|
|
636
|
+
async function buildClaimRequiredEnvelope({ capsuleDir, feature = "claimed server features" }) {
|
|
619
637
|
const sourceStore = new MemorySourceStore();
|
|
620
638
|
await sourceStore.writeFile(
|
|
621
639
|
"server/index.ts",
|
|
@@ -638,7 +656,7 @@ export default capsule({
|
|
|
638
656
|
<p className="text-sm font-semibold uppercase tracking-wide text-cyan-300">Lakebed deploy</p>
|
|
639
657
|
<h1 className="mt-3 text-3xl font-semibold">Claim required</h1>
|
|
640
658
|
<p className="mt-4 text-neutral-300">
|
|
641
|
-
This capsule uses
|
|
659
|
+
This capsule uses ${feature}. Claim this deploy, then run lakebed deploy again to publish the app.
|
|
642
660
|
</p>
|
|
643
661
|
</section>
|
|
644
662
|
</main>
|
|
@@ -667,9 +685,9 @@ export default capsule({
|
|
|
667
685
|
};
|
|
668
686
|
}
|
|
669
687
|
|
|
670
|
-
async function buildClaimedEnvelope(capsuleArg) {
|
|
688
|
+
async function buildClaimedEnvelope(capsuleArg, sourceStore) {
|
|
671
689
|
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
672
|
-
|
|
690
|
+
sourceStore ??= await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
673
691
|
const built = await buildCapsule({
|
|
674
692
|
capsuleDir,
|
|
675
693
|
capsuleId: `claimed-${Date.now()}`,
|
|
@@ -743,13 +761,21 @@ function claimUrlFromDeployMetadata(metadata) {
|
|
|
743
761
|
return `${String(metadata.api).replace(/\/+$/g, "")}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
|
|
744
762
|
}
|
|
745
763
|
|
|
746
|
-
function deployRequestBody(envelope, ttl) {
|
|
747
|
-
|
|
764
|
+
function deployRequestBody(envelope, ttl, { serverEnv } = {}) {
|
|
765
|
+
const body = {
|
|
748
766
|
artifact: envelope.artifact,
|
|
749
767
|
clientBundle: envelope.clientBundle,
|
|
750
768
|
clientVersion: LAKEBED_VERSION,
|
|
751
769
|
requestedTtlSeconds: ttl
|
|
752
|
-
}
|
|
770
|
+
};
|
|
771
|
+
if (serverEnv !== undefined) {
|
|
772
|
+
body.serverEnv = {
|
|
773
|
+
mode: "replace",
|
|
774
|
+
values: serverEnv
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return JSON.stringify(body);
|
|
753
779
|
}
|
|
754
780
|
|
|
755
781
|
async function buildCommand(args) {
|
|
@@ -801,6 +827,11 @@ async function readResponseJson(response) {
|
|
|
801
827
|
async function deployCommand(args) {
|
|
802
828
|
const [capsuleArg] = positionals(args);
|
|
803
829
|
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
830
|
+
const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
831
|
+
const serverEnvFileExists = sourceStore.hasFile(SERVER_ENV_FILE);
|
|
832
|
+
const serverEnv = await readCapsuleServerEnv(sourceStore);
|
|
833
|
+
const serverEnvKeys = Object.keys(serverEnv).sort();
|
|
834
|
+
const hasServerEnvValues = serverEnvKeys.length > 0;
|
|
804
835
|
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
805
836
|
const api = deployApiUrl(args);
|
|
806
837
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
@@ -815,27 +846,45 @@ async function deployCommand(args) {
|
|
|
815
846
|
}
|
|
816
847
|
|
|
817
848
|
let envelope;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
} catch (error) {
|
|
821
|
-
if (error instanceof AnonymousCompilerError && canUpdate && currentDeploy && !currentDeploy.claimed) {
|
|
849
|
+
if (!currentDeploy?.claimed && hasServerEnvValues) {
|
|
850
|
+
if (canUpdate && currentDeploy) {
|
|
822
851
|
throw new Error(
|
|
823
|
-
|
|
852
|
+
`This capsule defines server env in ${SERVER_ENV_FILE}.\n\nThis deploy is still anonymous. Claim it first, then run lakebed deploy again to sync server env.`
|
|
824
853
|
);
|
|
825
854
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
855
|
+
try {
|
|
856
|
+
await buildAnonymousEnvelope(capsuleDir, sourceStore);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (!canDeployAfterClaim(error)) {
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir, feature: "server env" });
|
|
863
|
+
} else {
|
|
864
|
+
try {
|
|
865
|
+
envelope = currentDeploy?.claimed ? await buildClaimedEnvelope(capsuleDir, sourceStore) : await buildAnonymousEnvelope(capsuleDir, sourceStore);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error instanceof AnonymousCompilerError && canUpdate && currentDeploy && !currentDeploy.claimed) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
`${error.message}\n\nThis deploy is still anonymous. Claim it first, then run lakebed deploy again to use server-side fetch.`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
if ((!canUpdate || !currentDeploy) && canDeployAfterClaim(error)) {
|
|
873
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir, feature: "server-side fetch" });
|
|
874
|
+
} else {
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
830
877
|
}
|
|
831
878
|
}
|
|
832
|
-
const
|
|
879
|
+
const syncServerEnv = Boolean(currentDeploy?.claimed && serverEnvFileExists);
|
|
880
|
+
const serverEnvForUpdate = syncServerEnv ? serverEnv : undefined;
|
|
881
|
+
let serverEnvSynced = false;
|
|
833
882
|
let mode = "created";
|
|
834
883
|
let response;
|
|
835
884
|
|
|
836
885
|
if (canUpdate) {
|
|
837
886
|
response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
|
|
838
|
-
body,
|
|
887
|
+
body: deployRequestBody(envelope, ttl, { serverEnv: serverEnvForUpdate }),
|
|
839
888
|
headers: {
|
|
840
889
|
"Authorization": `Bearer ${metadata.claimToken}`,
|
|
841
890
|
"Content-Type": "application/json"
|
|
@@ -846,13 +895,17 @@ async function deployCommand(args) {
|
|
|
846
895
|
if (response.status === 404 || response.status === 410) {
|
|
847
896
|
mode = "created";
|
|
848
897
|
response = null;
|
|
898
|
+
if (serverEnvForUpdate !== undefined && hasServerEnvValues) {
|
|
899
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir, feature: "server env" });
|
|
900
|
+
}
|
|
849
901
|
} else {
|
|
850
902
|
mode = "updated";
|
|
903
|
+
serverEnvSynced = serverEnvForUpdate !== undefined;
|
|
851
904
|
}
|
|
852
905
|
}
|
|
853
906
|
|
|
854
907
|
response ??= await fetch(`${api}/v1/anonymous-deploys`, {
|
|
855
|
-
body,
|
|
908
|
+
body: deployRequestBody(envelope, ttl),
|
|
856
909
|
headers: {
|
|
857
910
|
"Content-Type": "application/json"
|
|
858
911
|
},
|
|
@@ -871,7 +924,16 @@ async function deployCommand(args) {
|
|
|
871
924
|
}
|
|
872
925
|
|
|
873
926
|
if (hasFlag(args, "--json")) {
|
|
874
|
-
console.log(
|
|
927
|
+
console.log(
|
|
928
|
+
JSON.stringify(
|
|
929
|
+
{
|
|
930
|
+
...(envelope.claimRequired ? { ...deployed, claimRequired: true } : deployed),
|
|
931
|
+
...(serverEnvSynced ? { serverEnv: { keys: serverEnvKeys, mode: "replace", synced: true } } : {})
|
|
932
|
+
},
|
|
933
|
+
null,
|
|
934
|
+
2
|
|
935
|
+
)
|
|
936
|
+
);
|
|
875
937
|
return;
|
|
876
938
|
}
|
|
877
939
|
|
|
@@ -892,8 +954,14 @@ async function deployCommand(args) {
|
|
|
892
954
|
console.log(` requests: ${deployed.limits.requestsPerDay} / day`);
|
|
893
955
|
console.log(` mutations: ${deployed.limits.mutationsPerDay} / day`);
|
|
894
956
|
console.log(` outbound fetch: ${envelope.artifact.deployTarget === "claimed-source" ? "enabled" : "disabled"}`);
|
|
957
|
+
if (serverEnvSynced) {
|
|
958
|
+
console.log(`\nServer env: ${serverEnvKeys.length} synced`);
|
|
959
|
+
for (const key of serverEnvKeys) {
|
|
960
|
+
console.log(` ${key}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
895
963
|
if (envelope.claimRequired) {
|
|
896
|
-
console.log("\nThis app needs a claimed deploy before server-side fetch can run.");
|
|
964
|
+
console.log("\nThis app needs a claimed deploy before server-side fetch or server env can run.");
|
|
897
965
|
console.log("Open the claim URL, then run lakebed deploy again.");
|
|
898
966
|
}
|
|
899
967
|
}
|
|
@@ -1106,6 +1174,7 @@ lakebed logs --port 3000
|
|
|
1106
1174
|
- Use Tailwind classes directly in JSX.
|
|
1107
1175
|
- Do not add a CSS, PostCSS, or Tailwind build pipeline.
|
|
1108
1176
|
- Use auth through \`ctx.auth\` on the server and \`useAuth()\` on the client.
|
|
1177
|
+
- Read server-only environment variables through \`ctx.env\`; define them in \`.env.lakebed.server\`.
|
|
1109
1178
|
- Add Google sign-in with \`<SignInWithGoogle />\` or \`signInWithGoogle()\` from \`lakebed/client\`.
|
|
1110
1179
|
- Keep \`shared/\` free of DOM, Node, env, and Lakebed runtime imports.
|
|
1111
1180
|
|
|
@@ -1116,6 +1185,7 @@ lakebed logs --port 3000
|
|
|
1116
1185
|
- Guest auth locally, with built-in Google sign-in through Shoo.
|
|
1117
1186
|
- No file storage.
|
|
1118
1187
|
- No outbound fetch in anonymous deploys. Claim the deploy before using server-side fetch.
|
|
1188
|
+
- Non-empty \`.env.lakebed.server\` files sync only after a deploy is claimed.
|
|
1119
1189
|
- Local state resets when \`lakebed dev\` restarts.
|
|
1120
1190
|
`;
|
|
1121
1191
|
}
|
|
@@ -1223,6 +1293,7 @@ export function cleanTodoText(value: string): string {
|
|
|
1223
1293
|
}
|
|
1224
1294
|
`,
|
|
1225
1295
|
".gitignore": `.lakebed/
|
|
1296
|
+
.env.lakebed.server
|
|
1226
1297
|
`,
|
|
1227
1298
|
"README.md": `# ${title}
|
|
1228
1299
|
|
package/src/client.js
CHANGED
|
@@ -19,6 +19,7 @@ const pending = new Map();
|
|
|
19
19
|
const activeSubscriptions = new Set();
|
|
20
20
|
let authInitPromise = null;
|
|
21
21
|
let authInitialized = false;
|
|
22
|
+
let refreshRequested = false;
|
|
22
23
|
|
|
23
24
|
function toGuestName(name) {
|
|
24
25
|
return (
|
|
@@ -68,6 +69,15 @@ function emitQuery(name, value) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function refreshPage() {
|
|
73
|
+
if (refreshRequested) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
refreshRequested = true;
|
|
78
|
+
window.location.reload();
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
function send(message) {
|
|
72
82
|
const ws = connect();
|
|
73
83
|
const payload = JSON.stringify(message);
|
|
@@ -438,6 +448,11 @@ function connect() {
|
|
|
438
448
|
return;
|
|
439
449
|
}
|
|
440
450
|
|
|
451
|
+
if (message.op === "refresh") {
|
|
452
|
+
refreshPage();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
441
456
|
if (message.id && pending.has(message.id)) {
|
|
442
457
|
const handlers = pending.get(message.id);
|
|
443
458
|
pending.delete(message.id);
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.7";
|