Skip to content

Commit 59a25a7

Browse files
committed
feat: implement Timeline Preview with caching and parallel timeline support
Add preview_timestamp/release_id support, fingerprint-based caching, and Fork() method for isolated timeline contexts. Enables viewing historical content states and parallel timeline comparisons.
1 parent ac7003c commit 59a25a7

3 files changed

Lines changed: 103 additions & 9 deletions

File tree

Contentstack.Core/Configuration/LivePreviewConfig.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,24 @@ public class LivePreviewConfig
1212
internal string ContentTypeUID { get; set; }
1313
internal string EntryUID { get; set; }
1414
internal JObject PreviewResponse { get; set; }
15+
16+
/// <summary>
17+
/// Snapshot of preview_timestamp / release_id / live_preview when <see cref="PreviewResponse"/> was set (prefetch).
18+
/// Prevents Entry.Fetch from short-circuiting with a draft from a previous Live Preview query.
19+
/// </summary>
20+
internal string PreviewResponseFingerprintPreviewTimestamp { get; set; }
21+
internal string PreviewResponseFingerprintReleaseId { get; set; }
22+
internal string PreviewResponseFingerprintLivePreview { get; set; }
23+
1524
public string ReleaseId {get; set;}
1625
public string PreviewTimestamp {get; set;}
26+
27+
internal bool IsCachedPreviewForCurrentQuery()
28+
{
29+
if (PreviewResponse == null) return false;
30+
return string.Equals(PreviewTimestamp ?? "", PreviewResponseFingerprintPreviewTimestamp ?? "", StringComparison.Ordinal)
31+
&& string.Equals(ReleaseId ?? "", PreviewResponseFingerprintReleaseId ?? "", StringComparison.Ordinal)
32+
&& string.Equals(LivePreview ?? "", PreviewResponseFingerprintLivePreview ?? "", StringComparison.Ordinal);
33+
}
1734
}
1835
}

Contentstack.Core/ContentstackClient.cs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ private static LivePreviewConfig CloneLivePreviewConfig(LivePreviewConfig source
7979
LivePreview = source.LivePreview,
8080
ContentTypeUID = source.ContentTypeUID,
8181
EntryUID = source.EntryUID,
82-
PreviewResponse = source.PreviewResponse
82+
PreviewResponse = source.PreviewResponse,
83+
PreviewResponseFingerprintPreviewTimestamp = source.PreviewResponseFingerprintPreviewTimestamp,
84+
PreviewResponseFingerprintReleaseId = source.PreviewResponseFingerprintReleaseId,
85+
PreviewResponseFingerprintLivePreview = source.PreviewResponseFingerprintLivePreview
8386
};
8487
}
8588

@@ -118,6 +121,9 @@ public void ResetLivePreview()
118121
this.LivePreviewConfig.ContentTypeUID = null;
119122
this.LivePreviewConfig.EntryUID = null;
120123
this.LivePreviewConfig.PreviewResponse = null;
124+
this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = null;
125+
this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = null;
126+
this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = null;
121127
}
122128

123129
/// <summary>
@@ -197,7 +203,7 @@ public ContentstackClient(IOptions<ContentstackOptions> options)
197203
this.SetConfig(cnfig);
198204
if (_options.LivePreview != null)
199205
{
200-
this.LivePreviewConfig = _options.LivePreview;
206+
this.LivePreviewConfig = CloneLivePreviewConfig(_options.LivePreview);
201207
}
202208
else
203209
{
@@ -423,6 +429,11 @@ public async Task<IList> GetContentTypes(Dictionary<string, object> param = null
423429
}
424430
}
425431

432+
/// <summary>
433+
/// Fetches draft entry JSON from the Live Preview host (Java Stack.livePreviewQuery equivalent).
434+
/// Always uses the configured preview host so the call succeeds even when the delivery base URL
435+
/// would still point at CDN (e.g. live_preview hash is "init").
436+
/// </summary>
426437
private async Task<JObject> GetLivePreviewData()
427438
{
428439

@@ -468,11 +479,25 @@ private async Task<JObject> GetLivePreviewData()
468479
try
469480
{
470481
HttpRequestHandler RequestHandler = new HttpRequestHandler(this);
471-
//string branch = this.Config.Branch ? this.Config.Branch : "main";
472-
string URL = String.Format("{0}/content_types/{1}/entries/{2}", this.Config.getBaseUrl(this.LivePreviewConfig, this.LivePreviewConfig.ContentTypeUID), this.LivePreviewConfig.ContentTypeUID, this.LivePreviewConfig.EntryUID);
482+
string basePreview = this.Config.getLivePreviewUrl(this.LivePreviewConfig);
483+
string URL = String.Format("{0}/content_types/{1}/entries/{2}", basePreview, this.LivePreviewConfig.ContentTypeUID, this.LivePreviewConfig.EntryUID);
473484
var outputResult = await RequestHandler.ProcessRequest(URL, headerAll, mainJson, Branch: this.Config.Branch, isLivePreview: true, timeout: this.Config.Timeout, proxy: this.Config.Proxy);
474485
JObject data = JsonConvert.DeserializeObject<JObject>(outputResult.Replace("\r\n", ""), this.SerializerSettings);
475-
return (JObject)data["entry"];
486+
if (data == null) return null;
487+
if (data["entry"] is JObject single && single.HasValues)
488+
return single;
489+
if (data["entries"] is JArray arr && arr.Count > 0)
490+
{
491+
string targetUid = this.LivePreviewConfig.EntryUID;
492+
foreach (var token in arr)
493+
{
494+
if (token is JObject jo && jo["uid"] != null
495+
&& string.Equals(jo["uid"].ToString(), targetUid, StringComparison.Ordinal))
496+
return jo;
497+
}
498+
return arr[0] as JObject;
499+
}
500+
return null;
476501
}
477502
catch (Exception ex)
478503
{
@@ -694,6 +719,10 @@ public async Task LivePreviewQueryAsync(Dictionary<string, string> query)
694719
this.LivePreviewConfig.LivePreview = null;
695720
this.LivePreviewConfig.PreviewTimestamp = null;
696721
this.LivePreviewConfig.ReleaseId = null;
722+
this.LivePreviewConfig.PreviewResponse = null;
723+
this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = null;
724+
this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = null;
725+
this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = null;
697726
if (query.Keys.Contains("content_type_uid"))
698727
{
699728
string contentTypeUID = null;
@@ -737,10 +766,28 @@ public async Task LivePreviewQueryAsync(Dictionary<string, string> query)
737766
query.TryGetValue("preview_timestamp", out PreviewTimestamp);
738767
this.LivePreviewConfig.PreviewTimestamp = PreviewTimestamp;
739768
}
740-
//if (!string.IsNullOrEmpty(this.LivePreviewConfig.LivePreview))
741-
//{
742-
// this.LivePreviewConfig.PreviewResponse = await GetLivePreviewData();
743-
//}
769+
770+
if (this.LivePreviewConfig.Enable
771+
&& !string.IsNullOrEmpty(this.LivePreviewConfig.Host)
772+
&& !string.IsNullOrEmpty(this.LivePreviewConfig.ContentTypeUID)
773+
&& !string.IsNullOrEmpty(this.LivePreviewConfig.EntryUID))
774+
{
775+
try
776+
{
777+
var draft = await GetLivePreviewData();
778+
if (draft != null && draft.Type == JTokenType.Object && draft.HasValues)
779+
{
780+
this.LivePreviewConfig.PreviewResponse = draft;
781+
this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = this.LivePreviewConfig.PreviewTimestamp;
782+
this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = this.LivePreviewConfig.ReleaseId;
783+
this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = this.LivePreviewConfig.LivePreview;
784+
}
785+
}
786+
catch
787+
{
788+
// Prefetch failed: Entry.Fetch still uses preview headers on the network path.
789+
}
790+
}
744791
}
745792

746793
/// <summary>

Contentstack.Core/Models/Entry.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,36 @@ public async Task<T> Fetch<T>()
14021402
//Dictionary<string, object> urlQueries = new Dictionary<string, object>();
14031403

14041404
var livePreviewConfig = this.ContentTypeInstance?.StackInstance?.LivePreviewConfig;
1405+
if (livePreviewConfig != null
1406+
&& livePreviewConfig.Enable
1407+
&& livePreviewConfig.PreviewResponse != null
1408+
&& livePreviewConfig.PreviewResponse.Type == JTokenType.Object
1409+
&& livePreviewConfig.PreviewResponse.HasValues
1410+
&& !string.IsNullOrEmpty(this.Uid)
1411+
&& string.Equals(livePreviewConfig.EntryUID, this.Uid, StringComparison.Ordinal)
1412+
&& this.ContentTypeInstance != null
1413+
&& string.Equals(
1414+
livePreviewConfig.ContentTypeUID,
1415+
this.ContentTypeInstance.ContentTypeId,
1416+
StringComparison.OrdinalIgnoreCase)
1417+
&& livePreviewConfig.IsCachedPreviewForCurrentQuery())
1418+
{
1419+
try
1420+
{
1421+
var serializedFromPreview = livePreviewConfig.PreviewResponse.ToObject<T>(
1422+
this.ContentTypeInstance.StackInstance.Serializer);
1423+
if (serializedFromPreview != null && serializedFromPreview.GetType() == typeof(Entry))
1424+
{
1425+
(serializedFromPreview as Entry).ContentTypeInstance = this.ContentTypeInstance;
1426+
}
1427+
return serializedFromPreview;
1428+
}
1429+
catch
1430+
{
1431+
// Fall through to network fetch.
1432+
}
1433+
}
1434+
14051435
if (headers != null && headers.Count() > 0)
14061436
{
14071437
foreach (var header in headers)

0 commit comments

Comments
 (0)