@tesana/sdk 1.0.0 → 1.0.1
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/dist/tesana.esm.js +73 -4
- package/dist/tesana.iife.js +74 -5
- package/package.json +1 -1
- package/src/tesana.js +73 -4
package/dist/tesana.esm.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// 1.0.0 ships six services together: saves, shop, leaderboards, achievements,
|
|
38
38
|
// a vanity address (`tesana.vanity`) and in-game ads (`tesana.ads`). These are
|
|
39
39
|
// the first release, so there is no earlier number to stay compatible with.
|
|
40
|
-
const VERSION = "1.0.
|
|
40
|
+
const VERSION = "1.0.1";
|
|
41
41
|
|
|
42
42
|
const LOCAL_CACHE = "tesana.play.v1";
|
|
43
43
|
|
|
@@ -444,7 +444,28 @@ export async function init(config = {}) {
|
|
|
444
444
|
startListening();
|
|
445
445
|
const boot = await waitBootstrap();
|
|
446
446
|
const client = new TesanaClient(buildConfig(mergedConfig()));
|
|
447
|
-
|
|
447
|
+
// An unreachable play service must cost a *feature*, never the game.
|
|
448
|
+
//
|
|
449
|
+
// This is the client half of a contract the server already keeps: Django's
|
|
450
|
+
// mint returns None rather than failing a request, precisely so that "the
|
|
451
|
+
// play service being slow, down, or unconfigured means the SDK keeps saves in
|
|
452
|
+
// the browser — worse, but the game still runs". Without this catch the
|
|
453
|
+
// client contradicted that. `ensureSession()` mints an identity over HTTP, so
|
|
454
|
+
// a failure here rejected `__TESANA_READY__`, and a game doing the documented
|
|
455
|
+
// `await window.__TESANA_READY__` threw on its first line — turning a service
|
|
456
|
+
// outage into a broken game for everyone using it.
|
|
457
|
+
//
|
|
458
|
+
// Falling back to offline is the correct degradation, not a swallow: every
|
|
459
|
+
// read already has a local or empty answer (`balance()` reports zero,
|
|
460
|
+
// `shop.read()` reports a shut shutter, saves read as missing), and a later
|
|
461
|
+
// bootstrap upgrade still re-runs the identity step through
|
|
462
|
+
// `onBootstrapUpgrade` below. The player loses cloud features for that
|
|
463
|
+
// session and keeps their game.
|
|
464
|
+
try {
|
|
465
|
+
await client.ensureSession();
|
|
466
|
+
} catch (err) {
|
|
467
|
+
client.goOffline(err);
|
|
468
|
+
}
|
|
448
469
|
window.__TESANA_PLAYER__ = client.player.me();
|
|
449
470
|
// A provisional "offline" answer can upgrade later. Re-point this client in
|
|
450
471
|
// place — the game holds a reference to it — and re-run the identity step,
|
|
@@ -778,9 +799,17 @@ class TesanaClient {
|
|
|
778
799
|
// Read this before drawing a lobby: when it is false the game is not
|
|
779
800
|
// allowed rooms yet and the calls below refuse.
|
|
780
801
|
enabled: this._mpEnabled,
|
|
802
|
+
// `_mpEnabled` alone is not enough. A game with no host has multiplayer
|
|
803
|
+
// "allowed" and no service to ask, so `list()` used to reject with a
|
|
804
|
+
// transport error — and a game that draws a lobby on load (which the
|
|
805
|
+
// skill tells games to do) got an unhandled rejection on its title
|
|
806
|
+
// screen. An empty lobby is the honest answer for a game that cannot
|
|
807
|
+
// reach the room service, and it is what the game can render.
|
|
781
808
|
list: () =>
|
|
782
|
-
this._mpEnabled
|
|
783
|
-
? this.request("GET", "/v1/mp/rooms")
|
|
809
|
+
this._mpEnabled && !this.offline
|
|
810
|
+
? this.request("GET", "/v1/mp/rooms")
|
|
811
|
+
.then((r) => r.rooms)
|
|
812
|
+
.catch(() => [])
|
|
784
813
|
: Promise.resolve([]),
|
|
785
814
|
create: (opts) =>
|
|
786
815
|
this._mpEnabled
|
|
@@ -1168,6 +1197,46 @@ class TesanaClient {
|
|
|
1168
1197
|
return this;
|
|
1169
1198
|
}
|
|
1170
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Carry on without the play service.
|
|
1202
|
+
*
|
|
1203
|
+
* The boot path calls this when the identity step fails — an unreachable
|
|
1204
|
+
* endpoint, a 500, a bad secret, a cert the browser will not accept. A game
|
|
1205
|
+
* must not die of it: the server side of this contract already returns no
|
|
1206
|
+
* token and lets the game run, so the client falls back to exactly the state
|
|
1207
|
+
* a game with no endpoint is in.
|
|
1208
|
+
*
|
|
1209
|
+
* Marking the client offline is the honest outcome rather than a silent
|
|
1210
|
+
* swallow. Every read already has an offline answer — `balance()` reports
|
|
1211
|
+
* zero coins and `canSpend: false`, the shop reports a shut shutter, saves
|
|
1212
|
+
* read as absent and write to the local cache — so the game draws its
|
|
1213
|
+
* title screen instead of throwing. A later bootstrap upgrade re-runs the
|
|
1214
|
+
* identity step and flips this back off.
|
|
1215
|
+
*
|
|
1216
|
+
* The token is cleared rather than kept: it is the thing that failed to
|
|
1217
|
+
* authenticate, and half an identity is worse than none — every call would
|
|
1218
|
+
* retry a credential the service just refused.
|
|
1219
|
+
*
|
|
1220
|
+
* @param {unknown} [reason] the failure, kept for diagnostics
|
|
1221
|
+
*/
|
|
1222
|
+
goOffline(reason) {
|
|
1223
|
+
this.offline = true;
|
|
1224
|
+
this.token = "";
|
|
1225
|
+
this.saveToken = "";
|
|
1226
|
+
this.canSaveToCloud = false;
|
|
1227
|
+
this._mpEnabled = false;
|
|
1228
|
+
this.multiplayer.enabled = false;
|
|
1229
|
+
// One deduped line rather than a wall: a page whose endpoint is
|
|
1230
|
+
// misconfigured would otherwise log this on every call.
|
|
1231
|
+
warnOnce(
|
|
1232
|
+
"play-session",
|
|
1233
|
+
`[tesana] play service unavailable; running without cloud saves or the shop${
|
|
1234
|
+
reason ? ` (${(reason && reason.message) || reason})` : ""
|
|
1235
|
+
}`,
|
|
1236
|
+
);
|
|
1237
|
+
return this;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1171
1240
|
/**
|
|
1172
1241
|
* Which origin and which credential a path uses.
|
|
1173
1242
|
*
|
package/dist/tesana.iife.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Tesana play-time SDK v1.0.
|
|
1
|
+
/* Tesana play-time SDK v1.0.1 */
|
|
2
2
|
(function (root) {
|
|
3
3
|
/**
|
|
4
4
|
* Tesana play-time SDK. Vanilla JS, no npm in games.
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// 1.0.0 ships six services together: saves, shop, leaderboards, achievements,
|
|
40
40
|
// a vanity address (`tesana.vanity`) and in-game ads (`tesana.ads`). These are
|
|
41
41
|
// the first release, so there is no earlier number to stay compatible with.
|
|
42
|
-
const VERSION = "1.0.
|
|
42
|
+
const VERSION = "1.0.1";
|
|
43
43
|
|
|
44
44
|
const LOCAL_CACHE = "tesana.play.v1";
|
|
45
45
|
|
|
@@ -446,7 +446,28 @@ async function init(config = {}) {
|
|
|
446
446
|
startListening();
|
|
447
447
|
const boot = await waitBootstrap();
|
|
448
448
|
const client = new TesanaClient(buildConfig(mergedConfig()));
|
|
449
|
-
|
|
449
|
+
// An unreachable play service must cost a *feature*, never the game.
|
|
450
|
+
//
|
|
451
|
+
// This is the client half of a contract the server already keeps: Django's
|
|
452
|
+
// mint returns None rather than failing a request, precisely so that "the
|
|
453
|
+
// play service being slow, down, or unconfigured means the SDK keeps saves in
|
|
454
|
+
// the browser — worse, but the game still runs". Without this catch the
|
|
455
|
+
// client contradicted that. `ensureSession()` mints an identity over HTTP, so
|
|
456
|
+
// a failure here rejected `__TESANA_READY__`, and a game doing the documented
|
|
457
|
+
// `await window.__TESANA_READY__` threw on its first line — turning a service
|
|
458
|
+
// outage into a broken game for everyone using it.
|
|
459
|
+
//
|
|
460
|
+
// Falling back to offline is the correct degradation, not a swallow: every
|
|
461
|
+
// read already has a local or empty answer (`balance()` reports zero,
|
|
462
|
+
// `shop.read()` reports a shut shutter, saves read as missing), and a later
|
|
463
|
+
// bootstrap upgrade still re-runs the identity step through
|
|
464
|
+
// `onBootstrapUpgrade` below. The player loses cloud features for that
|
|
465
|
+
// session and keeps their game.
|
|
466
|
+
try {
|
|
467
|
+
await client.ensureSession();
|
|
468
|
+
} catch (err) {
|
|
469
|
+
client.goOffline(err);
|
|
470
|
+
}
|
|
450
471
|
window.__TESANA_PLAYER__ = client.player.me();
|
|
451
472
|
// A provisional "offline" answer can upgrade later. Re-point this client in
|
|
452
473
|
// place — the game holds a reference to it — and re-run the identity step,
|
|
@@ -780,9 +801,17 @@ class TesanaClient {
|
|
|
780
801
|
// Read this before drawing a lobby: when it is false the game is not
|
|
781
802
|
// allowed rooms yet and the calls below refuse.
|
|
782
803
|
enabled: this._mpEnabled,
|
|
804
|
+
// `_mpEnabled` alone is not enough. A game with no host has multiplayer
|
|
805
|
+
// "allowed" and no service to ask, so `list()` used to reject with a
|
|
806
|
+
// transport error — and a game that draws a lobby on load (which the
|
|
807
|
+
// skill tells games to do) got an unhandled rejection on its title
|
|
808
|
+
// screen. An empty lobby is the honest answer for a game that cannot
|
|
809
|
+
// reach the room service, and it is what the game can render.
|
|
783
810
|
list: () =>
|
|
784
|
-
this._mpEnabled
|
|
785
|
-
? this.request("GET", "/v1/mp/rooms")
|
|
811
|
+
this._mpEnabled && !this.offline
|
|
812
|
+
? this.request("GET", "/v1/mp/rooms")
|
|
813
|
+
.then((r) => r.rooms)
|
|
814
|
+
.catch(() => [])
|
|
786
815
|
: Promise.resolve([]),
|
|
787
816
|
create: (opts) =>
|
|
788
817
|
this._mpEnabled
|
|
@@ -1170,6 +1199,46 @@ class TesanaClient {
|
|
|
1170
1199
|
return this;
|
|
1171
1200
|
}
|
|
1172
1201
|
|
|
1202
|
+
/**
|
|
1203
|
+
* Carry on without the play service.
|
|
1204
|
+
*
|
|
1205
|
+
* The boot path calls this when the identity step fails — an unreachable
|
|
1206
|
+
* endpoint, a 500, a bad secret, a cert the browser will not accept. A game
|
|
1207
|
+
* must not die of it: the server side of this contract already returns no
|
|
1208
|
+
* token and lets the game run, so the client falls back to exactly the state
|
|
1209
|
+
* a game with no endpoint is in.
|
|
1210
|
+
*
|
|
1211
|
+
* Marking the client offline is the honest outcome rather than a silent
|
|
1212
|
+
* swallow. Every read already has an offline answer — `balance()` reports
|
|
1213
|
+
* zero coins and `canSpend: false`, the shop reports a shut shutter, saves
|
|
1214
|
+
* read as absent and write to the local cache — so the game draws its
|
|
1215
|
+
* title screen instead of throwing. A later bootstrap upgrade re-runs the
|
|
1216
|
+
* identity step and flips this back off.
|
|
1217
|
+
*
|
|
1218
|
+
* The token is cleared rather than kept: it is the thing that failed to
|
|
1219
|
+
* authenticate, and half an identity is worse than none — every call would
|
|
1220
|
+
* retry a credential the service just refused.
|
|
1221
|
+
*
|
|
1222
|
+
* @param {unknown} [reason] the failure, kept for diagnostics
|
|
1223
|
+
*/
|
|
1224
|
+
goOffline(reason) {
|
|
1225
|
+
this.offline = true;
|
|
1226
|
+
this.token = "";
|
|
1227
|
+
this.saveToken = "";
|
|
1228
|
+
this.canSaveToCloud = false;
|
|
1229
|
+
this._mpEnabled = false;
|
|
1230
|
+
this.multiplayer.enabled = false;
|
|
1231
|
+
// One deduped line rather than a wall: a page whose endpoint is
|
|
1232
|
+
// misconfigured would otherwise log this on every call.
|
|
1233
|
+
warnOnce(
|
|
1234
|
+
"play-session",
|
|
1235
|
+
`[tesana] play service unavailable; running without cloud saves or the shop${
|
|
1236
|
+
reason ? ` (${(reason && reason.message) || reason})` : ""
|
|
1237
|
+
}`,
|
|
1238
|
+
);
|
|
1239
|
+
return this;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1173
1242
|
/**
|
|
1174
1243
|
* Which origin and which credential a path uses.
|
|
1175
1244
|
*
|
package/package.json
CHANGED
package/src/tesana.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// 1.0.0 ships six services together: saves, shop, leaderboards, achievements,
|
|
38
38
|
// a vanity address (`tesana.vanity`) and in-game ads (`tesana.ads`). These are
|
|
39
39
|
// the first release, so there is no earlier number to stay compatible with.
|
|
40
|
-
const VERSION = "1.0.
|
|
40
|
+
const VERSION = "1.0.1";
|
|
41
41
|
|
|
42
42
|
const LOCAL_CACHE = "tesana.play.v1";
|
|
43
43
|
|
|
@@ -444,7 +444,28 @@ export async function init(config = {}) {
|
|
|
444
444
|
startListening();
|
|
445
445
|
const boot = await waitBootstrap();
|
|
446
446
|
const client = new TesanaClient(buildConfig(mergedConfig()));
|
|
447
|
-
|
|
447
|
+
// An unreachable play service must cost a *feature*, never the game.
|
|
448
|
+
//
|
|
449
|
+
// This is the client half of a contract the server already keeps: Django's
|
|
450
|
+
// mint returns None rather than failing a request, precisely so that "the
|
|
451
|
+
// play service being slow, down, or unconfigured means the SDK keeps saves in
|
|
452
|
+
// the browser — worse, but the game still runs". Without this catch the
|
|
453
|
+
// client contradicted that. `ensureSession()` mints an identity over HTTP, so
|
|
454
|
+
// a failure here rejected `__TESANA_READY__`, and a game doing the documented
|
|
455
|
+
// `await window.__TESANA_READY__` threw on its first line — turning a service
|
|
456
|
+
// outage into a broken game for everyone using it.
|
|
457
|
+
//
|
|
458
|
+
// Falling back to offline is the correct degradation, not a swallow: every
|
|
459
|
+
// read already has a local or empty answer (`balance()` reports zero,
|
|
460
|
+
// `shop.read()` reports a shut shutter, saves read as missing), and a later
|
|
461
|
+
// bootstrap upgrade still re-runs the identity step through
|
|
462
|
+
// `onBootstrapUpgrade` below. The player loses cloud features for that
|
|
463
|
+
// session and keeps their game.
|
|
464
|
+
try {
|
|
465
|
+
await client.ensureSession();
|
|
466
|
+
} catch (err) {
|
|
467
|
+
client.goOffline(err);
|
|
468
|
+
}
|
|
448
469
|
window.__TESANA_PLAYER__ = client.player.me();
|
|
449
470
|
// A provisional "offline" answer can upgrade later. Re-point this client in
|
|
450
471
|
// place — the game holds a reference to it — and re-run the identity step,
|
|
@@ -778,9 +799,17 @@ class TesanaClient {
|
|
|
778
799
|
// Read this before drawing a lobby: when it is false the game is not
|
|
779
800
|
// allowed rooms yet and the calls below refuse.
|
|
780
801
|
enabled: this._mpEnabled,
|
|
802
|
+
// `_mpEnabled` alone is not enough. A game with no host has multiplayer
|
|
803
|
+
// "allowed" and no service to ask, so `list()` used to reject with a
|
|
804
|
+
// transport error — and a game that draws a lobby on load (which the
|
|
805
|
+
// skill tells games to do) got an unhandled rejection on its title
|
|
806
|
+
// screen. An empty lobby is the honest answer for a game that cannot
|
|
807
|
+
// reach the room service, and it is what the game can render.
|
|
781
808
|
list: () =>
|
|
782
|
-
this._mpEnabled
|
|
783
|
-
? this.request("GET", "/v1/mp/rooms")
|
|
809
|
+
this._mpEnabled && !this.offline
|
|
810
|
+
? this.request("GET", "/v1/mp/rooms")
|
|
811
|
+
.then((r) => r.rooms)
|
|
812
|
+
.catch(() => [])
|
|
784
813
|
: Promise.resolve([]),
|
|
785
814
|
create: (opts) =>
|
|
786
815
|
this._mpEnabled
|
|
@@ -1168,6 +1197,46 @@ class TesanaClient {
|
|
|
1168
1197
|
return this;
|
|
1169
1198
|
}
|
|
1170
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Carry on without the play service.
|
|
1202
|
+
*
|
|
1203
|
+
* The boot path calls this when the identity step fails — an unreachable
|
|
1204
|
+
* endpoint, a 500, a bad secret, a cert the browser will not accept. A game
|
|
1205
|
+
* must not die of it: the server side of this contract already returns no
|
|
1206
|
+
* token and lets the game run, so the client falls back to exactly the state
|
|
1207
|
+
* a game with no endpoint is in.
|
|
1208
|
+
*
|
|
1209
|
+
* Marking the client offline is the honest outcome rather than a silent
|
|
1210
|
+
* swallow. Every read already has an offline answer — `balance()` reports
|
|
1211
|
+
* zero coins and `canSpend: false`, the shop reports a shut shutter, saves
|
|
1212
|
+
* read as absent and write to the local cache — so the game draws its
|
|
1213
|
+
* title screen instead of throwing. A later bootstrap upgrade re-runs the
|
|
1214
|
+
* identity step and flips this back off.
|
|
1215
|
+
*
|
|
1216
|
+
* The token is cleared rather than kept: it is the thing that failed to
|
|
1217
|
+
* authenticate, and half an identity is worse than none — every call would
|
|
1218
|
+
* retry a credential the service just refused.
|
|
1219
|
+
*
|
|
1220
|
+
* @param {unknown} [reason] the failure, kept for diagnostics
|
|
1221
|
+
*/
|
|
1222
|
+
goOffline(reason) {
|
|
1223
|
+
this.offline = true;
|
|
1224
|
+
this.token = "";
|
|
1225
|
+
this.saveToken = "";
|
|
1226
|
+
this.canSaveToCloud = false;
|
|
1227
|
+
this._mpEnabled = false;
|
|
1228
|
+
this.multiplayer.enabled = false;
|
|
1229
|
+
// One deduped line rather than a wall: a page whose endpoint is
|
|
1230
|
+
// misconfigured would otherwise log this on every call.
|
|
1231
|
+
warnOnce(
|
|
1232
|
+
"play-session",
|
|
1233
|
+
`[tesana] play service unavailable; running without cloud saves or the shop${
|
|
1234
|
+
reason ? ` (${(reason && reason.message) || reason})` : ""
|
|
1235
|
+
}`,
|
|
1236
|
+
);
|
|
1237
|
+
return this;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1171
1240
|
/**
|
|
1172
1241
|
* Which origin and which credential a path uses.
|
|
1173
1242
|
*
|